/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 : AbstractModel() {
@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)
@Piece( printable = false )
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)
}
}

