summaryrefslogtreecommitdiff
path: root/core/src/jvmTest/kotlin/coffee/liz
diff options
context:
space:
mode:
authorElizabeth Alexander Hunt <me@liz.coffee>2026-11-13 15:28:48 -0800
committerElizabeth Alexander Hunt <me@liz.coffee>2026-11-13 15:37:09 -0800
commit009525bd46edfc77068da212e36f38d410503f2c (patch)
treef042565bb01d070f626213b72865800ec0dbf576 /core/src/jvmTest/kotlin/coffee/liz
parent60426ce45763d0a4a5572db6926e8e677cbc0dc0 (diff)
downloadrookery-009525bd46edfc77068da212e36f38d410503f2c.tar.gz
rookery-009525bd46edfc77068da212e36f38d410503f2c.zip
Smart-cube timing, macOS Bluetooth, Android app, stackmat, pairing UI
- Timing with a smart cube: the scramble stays put and is marked off move by move (ScrambleTracker); a stray turn gets the shortest path to the same scrambled state from the two-phase solver, kept until the next stray turn. Wide moves in BLD scrambles become face turns. Once scrambled, inspection (or blind memo) starts by itself after a configurable delay; the first turn starts the clock and the solved cube stops it. The cube's own state reports are authoritative, so a move lost over Bluetooth can't keep the timer running. Blind solves show a MEMO/EXEC annunciator and store the memo split. A live view of the cube sits beside the timer. - Bluetooth through Kable on macOS and Windows (Linux keeps BlueZ) and on Android; the cube's MAC is read from its advertisement where the platform hides it. Scan/pair/connect/forget UI in settings. - Android app (:android): core and ui gain Android targets, with a jvmShared source set for code desktop and Android share. Android host for storage, sound, file import, images, Bluetooth and audio. Phone layouts: stacked panes and a full-screen timer with touch start. Scramble drawings (SVG) are desktop-only for now. - Stackmat: audio decoder and session; the mat can drive the timer. - Timer readout always shows the tens-of-seconds digit. - UI copy reduced to plain labels; marquee, snow, pointer-following and penguin speech removed. Brave key can be entered in settings. - Phone timer: one-line header, session picker and averages on one line, solves/stats in the footer; sessions can be renamed and deleted and are named <event>-<date>. Display mode fills the screen (cut-out included), fits the scramble on one line, shows the live cube, and the screen stays awake. - Readout: digits keep one size (inspection included), whole seconds while running unless set otherwise; previous scramble shown; the scramble no longer blanks while the next is generated. - Fixes: stackmat and smart cube no longer both drive the timer; a solved cube or a sub-quarter-second tap can't record a solve. - Blind memo indicator is an LCD penguin pacing the glass. - Android: system-bar insets, folder picker (all files access), Brave key synced with the data. - Algorithm trainer: PLL and OLL from J Perm's sheets, each algorithm verified by simulation; case plans, your choice of algorithm (or your own, checked against the case), new/learning/known, and a drill weighted towards weak cases. - Pair cards show the pair on a 3D see-through cube with an arrow; the memo cube can be dragged between targets. - CI image installs the Android SDK and runs core tests + desktop packaging (untested). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Diffstat (limited to 'core/src/jvmTest/kotlin/coffee/liz')
-rw-r--r--core/src/jvmTest/kotlin/coffee/liz/rookery/ScrambleTrackerTest.kt78
1 files changed, 78 insertions, 0 deletions
diff --git a/core/src/jvmTest/kotlin/coffee/liz/rookery/ScrambleTrackerTest.kt b/core/src/jvmTest/kotlin/coffee/liz/rookery/ScrambleTrackerTest.kt
new file mode 100644
index 0000000..896aaa4
--- /dev/null
+++ b/core/src/jvmTest/kotlin/coffee/liz/rookery/ScrambleTrackerTest.kt
@@ -0,0 +1,78 @@
+package coffee.liz.rookery
+
+import coffee.liz.rookery.puzzle.Cube3x3
+import coffee.liz.rookery.puzzle.NxnCube
+import coffee.liz.rookery.puzzle.ScrambleTracker
+import coffee.liz.rookery.scramble.TnoodleScrambler
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class ScrambleTrackerTest {
+ private val solver = TnoodleScrambler()
+ private fun tracker() = ScrambleTracker(solver::solve3x3)
+ private val scramble = "R U2 F' L D B2 R' U L2 F"
+
+ @Test fun followingTheScrambleWalksThePlanDown() {
+ val t = tracker()
+ t.reset(scramble, Cube3x3.SOLVED)
+ assertEquals(scramble.split(" "), t.plan)
+ t.move("R"); assertEquals("U2", t.plan[0])
+ assertEquals(scramble.split(" "), t.sequence); assertEquals(setOf(0), t.done); assertEquals(1, t.current)
+ t.move("U"); assertEquals("U", t.plan[0]) // half of a double turn
+ assertEquals(1, t.current) // still on the same move of the sequence
+ t.move("U"); assertEquals("F'", t.plan[0])
+ t.move("F"); assertEquals("F2", t.plan[0]) // wrong way round: two more fixes it, no solver needed
+ assertFalse(t.corrected)
+ t.move("F2")
+ for (m in listOf("L", "D", "B2", "R'", "U", "L2", "F")) t.move(m)
+ assertTrue(t.scrambled)
+ assertEquals(scramble.split(" "), t.sequence); assertEquals(t.sequence.indices.toSet(), t.done)
+ }
+
+ @Test fun oppositeFacesCanBeDoneInEitherOrder() {
+ val t = tracker()
+ t.reset("R L U", Cube3x3.SOLVED)
+ t.move("L"); assertEquals(listOf("R", "U"), t.plan)
+ assertFalse(t.corrected)
+ }
+
+ @Test fun strayMoveGetsAShortFixThatStaysPutAndReachesTheScramble() {
+ val t = tracker()
+ t.reset(scramble, Cube3x3.SOLVED)
+ t.move("R"); t.move("U2"); t.move("D") // D is not next
+ assertTrue(t.corrected)
+ val fix = t.plan
+ assertTrue(fix.size <= 21, "fix is ${fix.size} moves")
+ // following the fix: the rest of the plan is exactly its tail each time (cached, not re-solved)
+ val cube = NxnCube(3).apply("R U2 D")
+ for ((i, m) in fix.withIndex()) { t.move(m); cube.apply(m); assertEquals(fix.drop(i + 1), t.plan) }
+ assertTrue(t.scrambled)
+ assertEquals(Cube3x3.facelets(NxnCube(3).apply(scramble)), Cube3x3.facelets(cube))
+ }
+
+ @Test fun startsFromWhateverStateTheCubeIsIn() {
+ val t = tracker()
+ t.reset(scramble, Cube3x3.facelets(NxnCube(3).apply("F R U")))
+ assertTrue(t.known && t.corrected)
+ val cube = NxnCube(3).apply("F R U")
+ for (m in t.plan.toList()) { t.move(m); cube.apply(m) }
+ assertTrue(t.scrambled)
+ assertEquals(Cube3x3.facelets(NxnCube(3).apply(scramble)), Cube3x3.facelets(cube))
+ // then solving it is noticed
+ for (m in solver.solve3x3(Cube3x3.facelets(cube))!!.split(" ")) t.move(m)
+ assertTrue(t.solved)
+ }
+
+ @Test fun wideMovesBecomeFaceTurnsReachingTheSameCubeUpToHowItIsHeld() {
+ val bld = "R U F2 Rw Uw'"
+ val faces = Cube3x3.faceTurns(bld)
+ assertEquals(listOf("R", "U", "F2", "L"), faces.take(4))
+ // same cube: rotating the wide-move result back onto its centres gives the face-turn result
+ val wide = NxnCube(3).apply(bld)
+ val plain = Cube3x3.facelets(NxnCube(3).apply(faces.joinToString(" ")))
+ val rotations = listOf("", "x", "x2", "x'", "z", "z'").flatMap { a -> listOf("", "y", "y2", "y'").map { "$a $it".trim() } }
+ assertTrue(rotations.any { Cube3x3.facelets(wide.copy().apply(it)) == plain })
+ }
+}