Exit Full View
Up

/Games/Solitaire.foocad

Solitaire

The classic game of Solitaire (not the card game). The script include designs for a box to hold the board and pieces.

I used ball bearings, but you could use the more traditional marbles.

FooCAD Source Code
import static uk.co.nickthecoder.foocad.extras.v1.Extras.*
import static uk.co.nickthecoder.foocad.layout.v1.Layout2d.*
import static uk.co.nickthecoder.foocad.layout.v1.Layout3d.*
import static uk.co.nickthecoder.foocad.chamferedextrude.v1.ChamferedExtrude.*
import static uk.co.nickthecoder.foocad.changefilament.v1.ChangeFilament.*

include PuzzleBox.foocad

class Solitaire : Model {
    
    // Diameter of the balls/marbles, plus a little slack.
    // 21 is suitable for 20mm balls.
    var ballSize = 21

    // My first print used gap=2, but there needs to be bigger gaps, so that it
    // is easier to get your fingers in.
    // Also, this gap makes the pieces the same width as the "ring" and "cups"
    // so it will fit nicely in the box.
    var gap = 6.5

    var dist = 40

    var thickness = 4.0

    var thinnest = 0.8

    var chamfer = 0.8

    var piece = ""

    var slack = 0.5


    fun nobble() : Shape2d { 
        val size = ballSize * 300
        val xa = size* 0.11
        val xb = size* 0.2
        val yb = size* 0.2
        val yc = size* 0.35
        val delta = size * 0.1

        return PolygonBuilder().apply {
            moveTo( xa, -slack*100 )
            bezierTo( Vector2(xa-delta, delta), Vector2(xb, yb-delta), Vector2(xb, yb) )
            bezierTo( Vector2(xb, yb+delta), Vector2(delta, yc), Vector2(0, yc) )
            bezierTo( Vector2(-delta,yc), Vector2(-xb,yb+delta), Vector2(-xb, yb) )
            bezierTo( Vector2(-xb,yb-delta), Vector2(-xa+delta, delta), Vector2(-xa, -slack*100) ) 
            
        }.build().scale(0.01)
    }

    @Piece
    fun center() : Shape3d {
        val size = (ballSize + gap ) * 3

        val hollows = Sphere( ballSize / 2 )
            .tileX( 3, gap )
            .tileY( 3, gap )
            .center()
            .translateZ( ballSize/2 + thinnest )
            .color("Yellow").darker()        

        val nibble = nobble().offset(slack).mirrorY().translateY(size/2)

        val square = Square( size-slack ).center().roundAllCorners( 2 )
        val solid = ( square - nibble.repeatAround(4) )
            .chamferedExtrude( thickness, chamfer )
            .color( "Yellow" )

        println("Size of solid ${solid.size}" )
        return solid - hollows //+ hollows.previewOnly()

    }

    @Piece
    fun side() : Shape3d {
        val sizeX = (ballSize+gap)*3
        val sizeY = (ballSize+gap)*2

        val hollows = Sphere( ballSize / 2 )
            .tileX( 3, gap )
            .tileY( 3, gap )
            .centerX()
            .translateY( -sizeY/4 )
            .translateZ( ballSize/2 + thinnest )
            .color( "Blue" )
        
        val nobble : Shape2d = nobble().translateY(sizeY/2)
        val rect = Square( sizeX-slack, sizeY-slack ).center()
            .roundCorner( 3, 2 ).roundCorner( 2, 2 )
            .roundCorner( 1, 5 ).roundCorner( 0, 5 )
        val solid = (rect + nobble)
            .chamferedExtrude( thickness, chamfer )
            .color("RoyalBlue")

        return solid - hollows // + hollows.previewOnly()
            
    }

    @Piece
    fun triangular() : Shape3d {
        val size = (ballSize + gap) * 5 + ballSize / 2
        val delta = gap
        val solid = Triangle( size )
            .translateY( -ballSize/2 )
            .roundAllCorners( ballSize /2 )
            .chamferedExtrude( thickness, chamfer )
            .color( "RoyalBlue" )

        val dy = Degrees.sin(60) * ( ballSize + gap )
        val hollow = Sphere( ballSize / 2 )
            .translateZ( ballSize/2 + thinnest )
        val hollows = hollow.tileX( 5, gap ).centerX() +
            hollow.tileX( 4, gap ).centerX().translateY( dy ) +
            hollow.tileX( 3, gap ).centerX().translateY( dy*2 ) +
            hollow.tileX( 2, gap ).centerX().translateY( dy*3 ) +
            hollow.translateY( dy*4 )

        return solid - hollows // + hollows.previewOnly()
    }

    @Piece
    fun rings() : Shape3d {
        return Square(ballSize*4).center().roundAllCorners(5)
            .chamferedExtrude( thickness*2, 0.6 ) -
            // The 0.5 is so that the balls do not touch each other.
            Sphere(ballSize/2).tileX(4).tileY(4).center().translateZ(ballSize/2 + thickness +0.5) -
            Sphere(ballSize/2).tileX(4).tileY(4).center().translateZ(-ballSize/2 + thickness -0.5) -
            // Rather than do the maths, I've hard coded 0.3 as a resonable radius for the holes.
            // But if you change the @Custom parameters, then this may not work ;-(
            Cylinder( ballSize, ballSize*0.3 ).repeatX(4,ballSize).repeatY(4,ballSize).center()
    }

    fun createBox( part : String ) : PuzzleBox {
        return PuzzleBox().apply {
            width = ballSize * 4 + 1
            // Two layers of balls in the "rings" holder a shelf, and 25 for the
            // jigsaw pieces.
            depth = (ballSize + thickness * 2) * 2 + thickness + thickness * 4 + 2
            bottomHeight = ballSize * 4 - ballSize/2
            topHeight = ballSize / 2 + 4
            radius = 1
            cornerEars = 15
            pattern = "none"
        }
    }

    @Piece
    fun box() : Shape3d {
        val box = createBox( "bottom" )
        val chamfer = box.thickness / 4
        val shelf = Square( box.width, box.bottomHeight + thickness + chamfer )
            .chamferedExtrude( thickness, thickness/4 )
            .center()
            .rotateX(90)
            .toOriginZ().toOriginY().translateY( ballSize * 2 + thickness * 2 + 4)
            .translateZ( box.thickness - chamfer )

        val builtBox = box.build()
        val finger = Cylinder( 1000, 14 ).center().rotateX(90)
            .translateZ( box.thickness + box.bottomHeight + 4)


        val previewPieces = Cube( ballSize * 4, 24 /*thickness * 5 + 4*/, ballSize * 4 )
            .centerX().translateZ( box.thickness )
            .translateY(box.thickness*2 + ballSize*2 + 9 )

        val previewStack = Cube( ballSize * 4, 49 /*ballSize*2 + 7*/, ballSize * 4 )
            .centerX().translateZ( box.thickness )
            .translateY(box.thickness)

        return builtBox.translateY(box.thickness + box.depth/2) + shelf - finger +
            previewStack .previewOnly() +
            previewPieces.previewOnly() +
            lid()
                .mirrorZ().toOriginZ().translateZ(box.bottomHeight-4 + 30)
                .toOriginY()
                .previewOnly() 
    }

    @Piece
    fun lid() : Shape3d {
        val box = createBox( "top" )
        val finger = (Circle( 14-0.5 ) - Square(40).mirrorY().centerX()).extrude( box.thickness )
            .rotateX(90)
            .translateY( - box.depth/2 )
            .translateZ( box.thickness + box.topHeight - 4 )

        return box.build() + finger.mirrorY().also()
    }

    @Piece
    fun logo() : Shape3d {

        val dist = ballSize*0.48
        val lineThickness = 0.6
        val thickness = 1.0
        val width = 1

        val circ = Circle( 3 )
        val circs = circ.repeatX(7, dist).repeatY(3,dist).center().rotate(90).also()
        val lines = Square(6*dist,width).repeatY(3,dist).center().rotate(90).also() +
            Square(width,2*dist).repeatX(7,dist).center().rotate(90).also()

        return circs.extrude( thickness ) + lines.extrude( lineThickness )

    }

    @Piece
    fun logo2() : Shape3d {

        val backing = Square( 87,82 ).center().roundAllCorners(2).extrude(0.6)
        val logo = Text("Solitaire", BOLD, 13).centerX()           
            .translateY(15)
            .extrude(1.2) +
        Text( "nickthecoder\n.co.uk", 8).hAlign(CENTER).centerX()
            .translateY(-20)
            .extrude(1.2) +
        Circle( 5 ).tileX( 3, 4 ).center().extrude(1.0)

        return backing.color("Blue") + logo.color("Yellow")

    }

    @Piece
    fun logo3() : Shape3d {
        val backing = Square( 87,82 ).center().roundAllCorners(2).extrude(0.6)

        return backing.color( "Blue" ) + logo().translateZ(0.6).color("Yellow")
    }

    override fun build() : Shape3d {
        val center : Shape3d = center()
        val side : Shape3d = side()
            .rotateZ(180)
            .translateY((ballSize + gap) * 2.5)

        return center + side.repeatAroundZ(4)

    }

    /*
    // This is the main focus of this example! Also not that the class must implement PostProcessor.
    override fun postProcess(gcode: GCode) {
        if (piece == "logo2" || piece == "logo3" ) {
            pauseAtHeight( gcode, 0.6, "Change Filament" )
        }
    }
    */
}