/Games/WordGames.foocad
Create pieces and boards for various word games, such as Scrabble, Bananagram and Raid Word.
Print Notes
The tiles should be printed in two colours (the printer will pause when the filament needs changing)
I recommend printing a second set using a different colour, so that it is easy to play scrabble with one set, as well as other games which need more than one set.
Scrabble
1 set of tiles.
It took about 3hrs 30mins to get to the "change filament" stage for half a set of tiles.
9xscrabbleBoard I Used light grey Use a brim and/or ears to prevent lifting. Let the bed full cool, otherwise you may bend it when removing.
8xtws (triple word score) in RED
17xdws (double word score) in PINK
12xtls (tripple letter score) in DARK BLUE
24xdls (double letter score) in LIGHT BLUE
Glue these in place on the boards
The box.
I used 20mm ears 0.6mm height with ribs and 6mm brim. 2 Perimeters using a translucent filament gives a pleasing effect (the infill makes visible patterns)
Notes
Here's the guide I used to sew a plain draw string bag : [https://www.youtube.com/watch?v=0OuhGPFVlro]
Cut 3cm wider + 4cm at the top Fold 1cm, then another 3cm for the strings.
Raid Word
Use 5+ perimeters. Otherwise the rim around the tile slots may be nearly an exact multiple of perimeter width, and it then prints tiny dots, and the retractions may grind the filament, causing the print to completely fail.
- 2 sets of scrabbleLetters
- 4x raid5
- 24x raid4
- 24x raid3
- 8x raid2
Bananagram
- 2 sets of scrabbleLetters
import static uk.co.nickthecoder.foocad.along.v2.Along.* import static uk.co.nickthecoder.foocad.changefilament.v1.ChangeFilament.* import static uk.co.nickthecoder.foocad.chamferedextrude.v1.ChamferedExtrude.* import static uk.co.nickthecoder.foocad.layout.v1.Layout2d.* import static uk.co.nickthecoder.foocad.layout.v1.Layout3d.* include ThickWalledBox.foocad class RoundedBox : ThickWalledBox { override fun profile() : Shape2d = Square( width, depth ).center().roundAllCorners( 1, 4 ) override fun inset( amount : double ) : Shape2d { return Square( width-amount*2, depth-amount*2 ).center().roundAllCorners( 1-amount,4 ) } override fun build() : Shape3d { return super.build() } } class WordGames : Model { // The final two blanks. Use the Customiser to reprint and failed/lost letters. @Custom( required = false ) var letters = "??" @Custom var pieceSize = Vector2(20, 4) @Custom var radius = 2 @Custom var chamfer = 0.4 @Custom var style = TextStyle( "Lato BLACK", 10 ) @Custom( about="Negative for inset, positive for raised text" ) var letterHeight = -0.6 @Custom( about="Should we include the tile's points at the bottom right of each tile?" ) var includePoints = true @Custom var pointsStyle = TextStyle( "Lato Black", 4 ) @Custom var romanNumerals = true var boardWallWidth = 2.0 var boardSlack = 0.5 var boardThickness = 2.0 var boardWallHeight = 1.0 var bonusThickness = 0.8 // For Raid Word's "fields" - where you build your words. var raidMargin = 3.0 fun numberAsString( value : int ) : String { if ( romanNumerals ) { return listOf<String>( "", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X" )[value] } else { return "$value" } } fun points( letter : char ) = if ( "ZQ".contains("$letter") ) 10 else if ( "JX".contains( "$letter") ) 8 else if ( "K".contains("$letter") ) 5 else if ( "FHVWY".contains( "$letter") ) 4 else if ( "BCMP".contains( "$letter") ) 3 else if ( "DG".contains( "$letter") ) 2 else if ( ! letter.isLetter() ) 0 else 1 fun square() = Square( pieceSize.x ).roundAllCorners( radius ).center() fun tab() = Square(pieceSize.x / 3, pieceSize.x - 5).roundAllCorners(1).center() fun piece( letter : char ) : Shape3d { val main = square().chamferedExtrude( pieceSize.y, chamfer ) var text : Shape3d = Text( "$letter", style ).toPolygon().center() .extrude( Math.abs(letterHeight) ) .translateY(1.5) .color( "Grey" ) val points : int = points( letter ) if ( includePoints ) { val pointsText = Text ( numberAsString( points ), pointsStyle ) .toPolygon() .rightTo( main.right-1.8) .frontTo( main.front+1.8) .extrude( Math.abs(letterHeight) ) .color( "Grey" ) text = text + pointsText } val tile = if ( letterHeight < 0 ) { main - text.translateZ( pieceSize.y + letterHeight + 0.01 ) } else { main + text.translateZ( pieceSize.y ) } return tile } fun pieces ( str : String ) = pieces( str, 1.0 ) fun pieces ( str : String, spacing : double ) : Shape3d { val pieces = listOf<Shape3d>() for ( i in 0 until str.length() ) { val c = str.charAt(i) pieces.add( piece(c).translateX( (pieceSize.x+spacing) * i ) ) } return Union3d( pieces ) } @Piece fun tiles1() : Shape3d { // These are the distribution of letters in english scrabble. val all = layoutY( 1, pieces( "AAAAAAA" ), pieces( "AABBCCD" ), pieces( "DDDEEEE" ), pieces( "EEEEEEE" ), pieces( "EFFGGGH" ), pieces( "HIIIIII" ), pieces( "IIIJKLL" ) ) val firstLayerSheet = Square( all.size.x, all.size.y ) .roundAllCorners( radius ) .offset( -chamfer ) .extrude( 0.2 ) .leftTo( all.left + chamfer ).frontTo( all.front + chamfer ) return all + firstLayerSheet } @Piece fun tiles2() : Shape3d { // These are the distribution of letters in english scrabble. val all = layoutY( 1, pieces( "LLMMNNN" ), pieces( "NNNOOOO" ), pieces( "OOOOPPQ" ), pieces( "RRRRRRS" ), pieces( "SSSTTTT" ), pieces( "TTUUUUV" ), pieces( "VWWXYYZ" ) ) val firstLayerSheet = Square( all.size.x, all.size.y ) .offset( -chamfer ) .roundAllCorners( radius ).extrude( 0.2 ) .leftTo( all.left ).frontTo( all.front ) return all + firstLayerSheet } @Piece fun tiles() : Shape3d { val all = pieces( letters ) val firstLayerSheet = Square( all.size.x, all.size.y ) .roundAllCorners( radius ) .offset( -chamfer ) .extrude( 0.2 ) .leftTo( all.left + chamfer ).frontTo( all.front + chamfer ) return all + firstLayerSheet } fun scrabbleBox( isTop : bool ) : Shape3d { val x = 188 val y = 145 val z = 60 val boxy = RoundedBox().apply { thickness = 3.0 width = x depth = y bottomHeight = z topHeight = 15 } val box = boxy.build().frontTo(-boxy.thickness).leftTo(-boxy.thickness) if (isTop) return box val boards = Cube( 115, 115, 5 ) .bottomTo(boxy.thickness) .leftTo( slack ) .frontTo( slack ) .tileZ( 9, 0.4 ) .previewOnly() val divHeight = 55 val divider = Cube( boards.size.x + boxy.thickness + slack *2, boxy.thickness, divHeight ) .frontTo( boards.back + slack) .bottomTo(boxy.thickness) .color("Orange") val divider2 = Square( divHeight/2, divider.size.z ).roundCorner(3, 20) .extrude( divider.size.y ).alongX() .backTo( divider.back ) .bottomTo(boxy.thickness) .rightTo( divider.right ) .color("Orange") val racks = Cube( 185, 20, 50 ) .bottomTo(boxy.thickness) .leftTo( slack ) .frontTo( divider2.back + slack ) .previewOnly() // We could add the game's name to the front and back of the box // But I prefer to print some spare letters, and glue them on. val title = Text( "Scrabble", 30 ).extrude(0.4).center().rotateX(90) .backTo(box.front) .translateZ( z / 2 ) val title2 = title.rotateZ(180) .frontTo(box.back) return box + divider + divider2 + boards + racks // + title + title2 } @Piece fun scrabbleBox() = scrabbleBox( false ) @Piece fun scrabbleBoxLid() = scrabbleBox( true ) @Custom var slack = 1.0 var dividerHeight = 35 fun raidBox( isTop : bool ) : Shape3d { val x = 175 val y = 175 val z = 35 val boxy = RoundedBox().apply { thickness = 3.0 width = x depth = y bottomHeight = z topHeight = 15 } val box = boxy.build().frontTo(-boxy.thickness).leftTo(-boxy.thickness) if (isTop) return box val dividerT = boxy.thickness val four = raid4() .leftTo( slack ) .frontTo( slack ) val repeatY = four.size.y + boxy.thickness + slack*2 val fours = four.repeatY( 3, repeatY ) val three = raid3() .leftTo( fours.right + dividerT + slack*2 ) .frontTo( slack ) val threes = three.repeatY( 3, repeatY ) val fives = raid5() .leftTo( slack ) .frontTo( fours.back + dividerT + slack*2 ) val twos = raid(2) .bottomTo(boxy.thickness) .leftTo( fives.right + dividerT + slack*2 ) .frontTo( fives.front ) // We could add the game's name to the front and back of the box // But I prefer to print some spare letters, and glue them on. val title = Text( "Scrabble", 30 ).extrude(0.4).center().rotateX(90) .backTo(box.front) .translateZ( z / 2 ) val title2 = title.rotateZ(180) .frontTo(box.back) val pieces = (twos + threes + fours + fives) .bottomTo(boxy.thickness) .previewOnly() val dividersX = raidBoxDividerX( boxy, four ).repeatY( 3, repeatY ) + raidBoxDividerX( boxy, three ).repeatY( 2, repeatY ) + raidBoxDividerX( boxy, fives ) + raidBoxDividerX( boxy, twos ).repeatY( 2, -repeatY ) + Cube( 25, boxy.thickness, dividerHeight ) .leftTo( three.left - boxy.thickness - slack ) .frontTo( threes.back + slack ) .bottomTo( boxy.thickness ) val dividersY = raidBoxDividerY( boxy, fives ) + raidBoxDividerY( boxy, four ).repeatY( 3, repeatY ) return box + pieces + dividersX + dividersY } fun raidBoxDividerY( boxy : RoundedBox, pieces : Shape3d ) : Shape3d { return Cube( boxy.thickness, pieces.size.y + slack * 2 + boxy.thickness, dividerHeight ) .leftTo( pieces.right + slack ) .bottomTo( boxy.thickness ) .frontTo( pieces.front - slack - boxy.thickness/2 ) } fun raidBoxDividerX( boxy : RoundedBox, pieces : Shape3d ) : Shape3d { val a = Square( pieces.size.x / 2 - 15, dividerHeight ) .roundCorner( 1, 7 ) .extrude( boxy.thickness ) .alongY2() .frontTo( pieces.back + slack ) .bottomTo( boxy.thickness ) return a.leftTo( pieces.left - boxy.thickness ) + a.mirrorX().rightTo( pieces.right + boxy.thickness ) } @Piece fun raidBox() = raidBox( false ) @Piece fun raidBoxLid() = raidBox( true ) @Custom var addFirstLayerSheet = true /* Glue these to the front of the box. Consider using a batton clamped in place to help line the pieces up. If leaving gaps between the letters, use a spacer (such as another tile). */ fun tilesTitle( letters : String ) : Shape3d { chamfer = 0 pieceSize = Vector2(pieceSize.x, 0.8 ) letterHeight = -0.4 val all = pieces( letters ) return if (addFirstLayerSheet) { val firstLayerSheet = Square( all.size.x, all.size.y ) .roundAllCorners( radius ) .offset( -chamfer ) .extrude( 0.2 ) .leftTo( all.left + chamfer ).frontTo( all.front + chamfer ) .color( "Orange" ) all + firstLayerSheet } else { all } } @Piece fun tilesSCRABBLE() = tilesTitle( "SCRABBLE" ) @Piece fun tilesRAID() = tilesTitle( "RAID" ) /* One ninth of a scrabble board! This is a 5x5 grid (the whole board is 15x15) */ @Piece fun scrabbleBoard() : Shape3d { return scrabblePlace(boardThickness).tileX(5).tileY(5) } fun scrabblePlace( thickness : double ) : Shape3d { val inside = pieceSize.x + boardSlack val outside = inside + boardWallWidth val place = Cube( outside, outside, thickness ).centerXY().color( "LightGrey" ) val wallP = Square( inside + boardWallWidth).center() - Square( inside ).center() val wall = wallP.extrude( thickness + boardWallHeight ) .color( "Green" ) return (place + wall) } @Piece fun tws() = scrabbleBonus(true, true).tileX(4,3).tileY(2,3).color( "Red" ) @Piece fun dws() = scrabbleBonus(false, true).tileX(6,1).tileY(3,1).color( "Pink" ) // One spare! @Piece fun tls() = scrabbleBonus(true, false).tileX(4,1).tileY(3,1).color("Blue" ) @Piece fun dls() = scrabbleBonus(false, false).tileX(6,1).tileY(4,1).color("LightBlue") fun scrabbleBonus( isTripple : bool, isWord : bool ) : Shape3d { //val lug = Cube( pieceSize.x, pieceSize.x, boardWallHeight - 0.2 ).topTo(0).centerXY() val piece = scrabblePlace(bonusThickness) val text = Text( numberAsString( if (isTripple) 3 else 2 ), style ) .scale (if (isWord) 1.5 else 1.0 ) .extrude(pieceSize.y).center() return piece - text.bottomTo(Math.max(bonusThickness/2, 0.4)) } @Piece fun scrabbleBonusPadding() : Shape3d { val lug = Square( pieceSize.x, pieceSize.x ) .roundAllCorners(1) // Without this, I had to manually shave the corners. .center() .extrude( boardWallHeight - 0.2 ) return lug } /** I can't print the "scrabbleBonus" pieces as I'd like, so I print them in two parts. This "spacer" is glued to the board, and then the bonus is glued ontop of this. In hind slight, maybe I should have made the board solid where bonuses appear, and then glued directly onto that. */ fun scrabbleBonusSpacer() : Shape3d { val piece = scrabblePlace(bonusThickness) val lug = Cube( pieceSize.x, pieceSize.x, piece.size.z + boardWallHeight - 0.2 ).centerXY() return piece + lug } /* A place to put completed words in the Raid Words game. */ fun raid( n : int ) : Shape3d { val holes = Square( pieceSize.x + boardSlack ) //.roundAllCorners( radius ) .tileX( n, boardSlack + boardWallWidth ) .center() .extrude( boardWallHeight+1 ) .translateZ( boardThickness ) .color("Red") val main = Square( (pieceSize.x + boardSlack*2) * n + boardWallWidth * (n-1) + raidMargin*2 - boardSlack, pieceSize.x + boardSlack + raidMargin*2 ).center() .roundAllCorners( radius + raidMargin/2) .extrude( boardThickness + boardWallHeight ) .color("Orange") val result = main - holes val pieces = piece('A').tileX( n, boardWallWidth + boardSlack*2 ).centerXY().translateZ(boardThickness).previewOnly() return result //+ pieces } @Piece fun raid2() = raid(2) @Piece fun raid3() = raid(3) @Piece fun raid4() = raid(4) @Piece fun raid5() = raid(5) @Piece fun rack() : Shape3d { val profile = SVGParser().parseFile( "wordGamesRack.svg" ).shapes["rack"] return profile.extrude( (pieceSize.x + 3)* 8 ).alongX().toOrigin() } @Piece fun bonusSpacer() = scrabbleBonusSpacer().tileX(2,1).tileY(2,1) @Piece fun bonusPadding() = scrabbleBonusPadding().tileX( 1, 1 ).tileY( 1, 1 ) override fun build() : Shape3d { val board = scrabbleBoard().translateY(50) val tws = scrabbleBonus(true, true).color("Red").translate(0,50,boardThickness + boardWallHeight ) val dws = scrabbleBonus(false, true).color("Pink").translateZ( boardThickness + boardWallHeight ) .leftTo(tws.right).frontTo(tws.back) val tls = scrabbleBonus(true,false).color("Blue").translateZ( boardThickness + boardWallHeight ) .leftTo(dws.right).frontTo(dws.back) val dls = scrabbleBonus(true,false).color("LightBlue").translateZ( boardThickness + boardWallHeight ) .leftTo(tls.right).frontTo(tls.back) return raid(2) + raid(3).translateX( pieceSize.x * 3.5 ) + board + tws + dws + tls + dls } /* Change filament colour when printing the tiles. */ /* override fun postProcess(gcode: GCode) { if ( piece.startsWith("tiles" ) ) { val height = if (letterHeight < 0) { pieceSize.y + letterHeight } else { pieceSize.y } pauseAtHeight( gcode, height, "Change Filament" ) } } */ }