package coffee.liz.rookery.puzzle import coffee.liz.rookery.model.Puzzle import kotlin.math.sqrt /** Ready-made [TwistyPuzzle]s, each matching tnoodle's (WCA) orientation and notation. */ object Puzzles { private val cache = HashMap() /** The 3D model for [p], or null if it has none yet (Square-1, Clock, FTO, Megaminx). */ fun of(p: Puzzle): TwistyPuzzle? = cache.getOrPut(p) { when (p) { Puzzle.CUBE2, Puzzle.CUBE3, Puzzle.CUBE4, Puzzle.CUBE5, Puzzle.CUBE6, Puzzle.CUBE7 -> cube(p.cubeSize!!) Puzzle.PYRAMINX -> pyraminx() Puzzle.SKEWB -> skewb() else -> null } } /** * Face colours as 0xRRGGBB, indexed like each puzzle's faces. * Cubes and Skewb: U R F D L B = white red green yellow orange blue (WCA scheme). * Pyraminx: F L R D = green red blue yellow. */ fun colors(p: Puzzle): List = when (p) { Puzzle.PYRAMINX -> listOf(0x2DB84D, 0xE02424, 0x2266E0, 0xFFD500) else -> listOf(0xF4F4F4, 0xE02424, 0x2DB84D, 0xFFD500, 0xFF8A00, 0x2266E0) }.map { it.toLong() } private fun tokens(alg: String) = alg.replace("(", " ").replace(")", " ").split(Regex("\\s+")).filter { it.isNotEmpty() } // ---- NxN ---- fun cube(n: Int): TwistyPuzzle { val v = { x: Int, y: Int, z: Int -> V3(x.toDouble(), y.toDouble(), z.toDouble()) } // U R F D L B, counter-clockwise seen from outside val faces = listOf( Polygon(listOf(v(-1, 1, -1), v(-1, 1, 1), v(1, 1, 1), v(1, 1, -1))), Polygon(listOf(v(1, 1, 1), v(1, -1, 1), v(1, -1, -1), v(1, 1, -1))), Polygon(listOf(v(-1, 1, 1), v(-1, -1, 1), v(1, -1, 1), v(1, 1, 1))), Polygon(listOf(v(-1, -1, 1), v(-1, -1, -1), v(1, -1, -1), v(1, -1, 1))), Polygon(listOf(v(-1, 1, -1), v(-1, -1, -1), v(-1, -1, 1), v(-1, 1, 1))), Polygon(listOf(v(1, 1, -1), v(1, -1, -1), v(-1, -1, -1), v(-1, 1, -1))), ) val cuts = (1 until n).map { -1.0 + 2.0 * it / n } val axes = listOf(Axis("x", v(1, 0, 0), cuts, 4), Axis("y", v(0, 1, 0), cuts, 4), Axis("z", v(0, 0, 1), cuts, 4)) // NxnCube's Notation already speaks "axis 0/1/2, layer 0 = + side, quarter turns clockwise from +axis". return TwistyPuzzle("${n}x$n", faces, axes) { alg -> Notation.parse(alg, n).map { Turn(it.axis, it.layers, it.quarter % 4) } } } // ---- Pyraminx: vertex U up, face F towards you ---- fun pyraminx(): TwistyPuzzle { val r = 2 * sqrt(2.0) / 3 val u = V3(0.0, 1.0, 0.0) val l = V3(-r * sqrt(3.0) / 2, -1.0 / 3, r / 2) val rr = V3(r * sqrt(3.0) / 2, -1.0 / 3, r / 2) val b = V3(0.0, -1.0 / 3, -r) val faces = listOf(oriented(u, l, rr), oriented(u, b, l), oriented(u, rr, b), oriented(l, b, rr)) // Opposite faces sit at -1/3 along each vertex axis; thirds of the height give tips and middles. val cuts = listOf(1.0 / 9, 5.0 / 9) val axes = listOf(Axis("U", u, cuts, 3), Axis("L", l, cuts, 3), Axis("R", rr, cuts, 3), Axis("B", b, cuts, 3)) return TwistyPuzzle("pyraminx", faces, axes) { alg -> tokens(alg).map { t -> val m = Regex("""^([ULRBulrb])(')?$""").matchEntire(t) ?: throw IllegalArgumentException("bad pyraminx move '$t'") val c = m.groupValues[1][0] val axis = "ULRB".indexOf(c.uppercaseChar()) Turn(axis, if (c.isLowerCase()) setOf(0) else setOf(0, 1), if (m.groupValues[2].isEmpty()) 1 else 2) } } } // ---- Skewb (WCA notation: R = DRB corner, U = ULB, L = DLF, B = DLB) ---- fun skewb(): TwistyPuzzle { val cubeFaces = cube(1) val k = 1 / sqrt(3.0) fun corner(x: Int, y: Int, z: Int) = V3(x * k, y * k, z * k) val axes = listOf( Axis("R", corner(1, -1, -1), listOf(0.0), 3), Axis("U", corner(-1, 1, -1), listOf(0.0), 3), Axis("L", corner(-1, -1, 1), listOf(0.0), 3), Axis("B", corner(-1, -1, -1), listOf(0.0), 3), ) return TwistyPuzzle("skewb", cubeFaces.faces(), axes) { alg -> tokens(alg).map { t -> val m = Regex("""^([RULB])(')?$""").matchEntire(t) ?: throw IllegalArgumentException("bad skewb move '$t'") Turn("RULB".indexOf(m.groupValues[1][0]), setOf(0), if (m.groupValues[2].isEmpty()) 1 else 2) } } } /** Re-derive whole (uncut) faces from a 1x1 model. */ private fun TwistyPuzzle.faces(): List = stickers private fun oriented(a: V3, b: V3, c: V3): Polygon { val p = Polygon(listOf(a, b, c)) return if ((p.normal dot p.centroid) > 0) p else Polygon(listOf(a, c, b)) } }