Exit Full View
Up

/Games/PuzzleOfEvil.foocad

PuzzleOfEvil

A four piece jigsaw puzzle that is very challenging! I can confirm that both puzzles are possible. The "hard" side is particularly satisfying - don't give up!

SPOLIER ALERT

(Don't read this if you don't want any clues)... If you write a program to solve this by brute force, it will find the "easy" solution, but probably won't find the hard one. If so, your program is wrong. What assumptions is it making?

Read more about the puzzle and its origins here : [https://blog.khanacademy.org/how-wooden-puzzles-can-destroy-dev-teams/]

FooCAD Source Code
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.*
import static uk.co.nickthecoder.foocad.extras.v1.Extras.*

include Hinge.feather

class PuzzleOfEvil : Model {
    
    @Custom( about="The unit side of the triangles" )
    var size = 16.0

    // You cannot make thickness+boxThickness too small, otherwise the hinge
    // will be too small, and likely to fail/break.
    @Custom( about = "Thickness of the pieces")
    var thickness = 4

    @Custom( about = "Thickness of the base and lid" )
    var boxThickness = 2

    @Custom( about="Chamfer of the box and playing pieces" )
    var chamfer = 0.4

    // On my printer using low quality settings, 0.2 is just enough
    // for them to fit together quite snuggly. (I can turn the solved puzzled
    // upside down and the pieces don't fall out). You may prefer a larger slack.
    @Custom( about = "The pieces are shrunk slightly so that they fit together" )
    var slack = 0.2

    @Custom( about = "Thickness of the magnets which hold the case closed. 0 for no magnets" )
    var magnetT = 1.2

    @Custom( about = "Diameter of the magnets which hold the case closed" )
    var magnetD = 11.0

    fun move( shape : Shape2d, x : double, y : double ) =
        shape.translate( x*size, y * size * Degrees.sin(60) )

    fun move( shape : Shape3d, x : double, y : double ) =
        shape.translate( x*size, y * size * Degrees.sin(60), 0 )

    fun triangle() = Triangle( size ).toOrigin()

    fun line( length : int ) = triangle().hull(triangle().translateX((length-1)*size))

    fun profile( n : int ) : Shape2d {
        val one = triangle().offset(1)
        val two = line(2).offset(1)
        val three = line(3).offset(1)
        val four = line(4).offset(1)
        val five = line(5).offset(1)
        val six = line(6).offset(1)
        val seven = line(7).offset(1)
    
        val shape = if ( n == 1 ) {
            move(three,0,0) +
            move(three.rotate(-60),2, 0) +
            move(two.rotate(60),3,-2) +
            move(two.rotate(180),4,-2)
        } else if (n == 2) {
            two.rotate(180) +
            move(three.rotate(-120), -2,0) +
            move(two.rotate(-60), -3.5, -3 ) +
            move(two.rotate(180),-1,-4)
        } else if (n == 3) {
            three +
            move(three.rotate(120),4,-2) +
            move(one.rotate(60),3.5,-1) +
            move(one.rotate(60),4,-2)
        } else if (n == 4) {
            two +
            move(three.rotate(-60),0.5,1) +
            move(three.rotate(60),1,-4) +
            move(two.rotate(-60),0.5,-3)

        } else if (n == -1) {             // Easy puzzle outline
            val twoByFive = five.mirrorY().also()
            val twoByFour = four.mirrorY().also()
            val hex2 = Hull2d(move(three,-1.5,1).mirrorY().repeatAround(3))

            move(twoByFive, 0, -1.5) +
            move(twoByFour.rotate(-60),0.5,-2.5) +
            move(twoByFour.rotate(-60),2, -1.5) +
            move(hex2,5,-3.5)
            
        } else if (n == -2) {            // Hard Puzzle outline
                val twoByFive = five.mirrorY().also()
                val twoByFour = four.mirrorY().also()

                move(four,1,5.5) +
                move(five,0.5,4.5) +
                move(six,0,3.5) +
                move(seven.mirrorY().also(),-0.5,2.5) +
                move(two.mirrorY(),4,1.5) +
                move(four.mirrorY(),0,1.5)

        } else if (n == -3) {            // Box
            move(three.rotate(60),-0.5,-3).hull(
                move(four.rotate(-60),5,0)
            ).hull(
                move(four,1.5,-7)
            )
        } else {
            one
        }

        return shape.offset(-1)
    }

    fun piece( n : int ) =
        profile(n)
            .offset(-slack)
            .chamferedExtrude( thickness, chamfer ).color("Red")

    @Piece
    fun testHinge() : Shape3d {
        val hinge : Shape3d = hinge( 30 ).centerX()
        val side = Cube( 30, 6, boxThickness + thickness )
            .centerX().frontTo(hinge.back).topTo(0)

        return hinge + side.mirrorY().also()
    }

    fun hinge( length : double ) : Shape3d {

        val hingeGap = chamfer
        val hinge = Hinge(
            boxThickness + thickness + slack,
            (boxThickness + thickness + slack)/2,
            length- hingeGap*2, 3
        )
        hinge.profile = hinge.angledProfile( boxThickness + thickness - chamfer )
            
        return hinge.build()
            .rotateZ(90)
            .leftTo(hingeGap)

    }

    @Piece
    fun box() : Shape3d {

        val hinge : Shape3d = hinge( size * 6)
        
        val profile : Shape2d = profile( -3 )

        val bottom = (
            profile
                .chamferedExtrude( boxThickness, chamfer, 0 ).color("Green") +
            (profile - profile( -1 )).extrude( thickness ).translateZ(boxThickness)
        ).backTo(hinge.back+chamfer).topTo(0)
        val top = (
            profile.mirrorY()
                .chamferedExtrude( boxThickness, chamfer, 0 ).color("Green") +
            ( profile.mirrorY() - profile(-2)).chamferedExtrude( thickness,0 ).translateZ(boxThickness)
        ).frontTo(hinge.back).topTo(0)

        val magnets = move(Cylinder(magnetT,magnetD/2),5.25,6.5).mirrorY().also()
            .topTo(0)

        val hard = move(
            Text("HARD", BOLD).extrude(thickness/2).topTo(top.top)
                .rotateZ(-60), 5.3, 5.7 )
            

        return bottom + top - hard + hinge - magnets.color("Red")
    }

    @Piece
    fun piece1() = piece(1)

    @Piece
    fun piece2() = piece(2)

    @Piece
    fun piece3() = piece(3)

    @Piece
    fun piece4() = piece(4)

    fun hard() = profile( -2 ).extrude(0.6)

    fun easy() = profile( -1 ).extrude(0.6)

    override fun build() : Shape3d {

    return box().translateZ(-boxThickness) +
        (
            move(piece(1).rotateZ(-60),-3,-2) +
            move(piece(2),3.5, -1) +
            move(piece(3).rotateZ(-120),4.5, -1) +
            move(piece(4).rotateZ(-60),4.5,-3)
        ).translate(0.1,3.1,0)
    }
}