/Games/PuzzleOfEvil.foocad
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/]
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) } }