Exit Full View
Up

/Games/WordGames.foocad

WordGames

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
FooCAD Source Code
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" )
        }
    }
    */

}