1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
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<Puzzle, TwistyPuzzle?>()
/** 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<Long> = 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<Polygon> = 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))
}
}
|