diff --git a/README.md b/README.md
index ab0fe8e..cc1a12a 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
|---|---|
| `aspirin.sax` / `app-ORION-aspirin.zip` | Playable game: auto-shuffle at start, tap a tile next to the gap to slide it, SHUFFLE button, move counter, SOLVED! |
| `aspirin_solver.sax` / `app-ORION-aspirin_solver.zip` | Demo: shuffles 30 moves, then an auto solver plays one move per second until SOLVED! (it records the shuffle and replays the inverse moves) |
-| `aspirinV2.sax` / `app-ORION-aspirinV2.zip` | Same game with fixed cells: nothing moves, each cell shows the number it holds on white, the empty cell takes the board colour |
+| `aspirinV2.sax` / `app-ORION-aspirinV2.zip` | Fixed cells: nothing moves, each cell shows its number on white, the empty cell takes the board colour. Tap to start deals a random solvable board |
## Run in the simulator
@@ -40,6 +40,14 @@ python verify_logic.py --v2
- 16 `NumericSelectLabel` cells; `in` = the number held (0 = empty), text list ` ;1;2;...;15;`.
- `backgroundColor` is linked from logic: white for a number, board colour for the empty cell.
-- State = 16 cell values in `ADemux2` latches (boot loads the solved board). Blank position is
- derived from the values; a move latches 0 into the touched cell and its value into the blank.
+- State = 16 cell values in `ADemux2` latches (boot loads the solved board).
+- One swap mechanism moves values: each scan at most one pair of cells (A, B) exchanges values.
+ A tile move swaps the touched cell with the blank (new touch only, and only when adjacent).
+- No shuffle moves: the first tap (or SHUFFLE) deals. It loads the solved board, then a
+ Fisher-Yates pass swaps cell i = 15..2 with a random cell j <= i, one swap per scan
+ (~1.5 s). The logistic-map value is made ~uniform with a `Linearize` (arcsine CDF). The RNG
+ runs from boot, so tap timing picks the board. The blank starts in cell 16.
+- Solvability: with the blank home, a deal is solvable iff it is an even permutation; a parity
+ latch counts real swaps and an odd deal gets one extra swap of cells 1 and 2.
- Solved = sum of i * value_i over cells 1..15 is 1240 (its unique maximum).
+- Keep RAM in mind: the Orion failed to start a 56 kB build of V2; the current one is ~35 kB.
diff --git a/app-ORION-aspirinV2.zip b/app-ORION-aspirinV2.zip
index 5d4f94e..6942c10 100644
Binary files a/app-ORION-aspirinV2.zip and b/app-ORION-aspirinV2.zip differ
diff --git a/aspirinV2.sab b/aspirinV2.sab
index 8f22121..6160445 100644
Binary files a/aspirinV2.sab and b/aspirinV2.sab differ
diff --git a/aspirinV2.sax b/aspirinV2.sax
index d336b75..a0f5a04 100644
--- a/aspirinV2.sax
+++ b/aspirinV2.sax
@@ -46,1579 +46,1087 @@
-
-
-
-
-
-
-
-
-
-
+
-
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
+
-
+
-
+
-
+
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
+
-
+
-
+
-
+
+
+
-
-
+
+
+
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
-
-
-
-
-
+
-
+
-
+
-
+
-
-
-
+
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
+
+
-
-
-
-
-
+
-
+
-
+
-
+
-
-
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
-
-
-
+
+
+
-
+
+
+
+
+
-
-
-
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
+
+
-
+
+
+
+
+
-
-
-
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
+
+
-
+
+
+
+
+
-
-
-
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
+
+
-
+
+
+
+
+
-
-
-
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
+
+
-
+
+
+
+
+
-
-
-
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
+
+
-
+
+
+
+
+
-
-
-
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
+
+
-
+
+
+
+
+
-
-
-
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
+
+
-
+
+
+
+
+
-
-
-
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
-
+
@@ -1630,7 +1138,7 @@
-
+
@@ -1642,7 +1150,7 @@
-
+
@@ -1654,7 +1162,7 @@
-
+
@@ -1666,7 +1174,7 @@
-
+
@@ -1678,7 +1186,7 @@
-
+
@@ -1690,7 +1198,7 @@
-
+
@@ -1702,7 +1210,7 @@
-
+
@@ -1714,7 +1222,7 @@
-
+
@@ -1726,7 +1234,7 @@
-
+
@@ -1738,7 +1246,7 @@
-
+
@@ -1750,7 +1258,7 @@
-
+
@@ -1762,7 +1270,7 @@
-
+
@@ -1774,7 +1282,7 @@
-
+
@@ -1786,7 +1294,7 @@
-
+
@@ -1798,7 +1306,7 @@
-
+
@@ -1810,7 +1318,7 @@
-
+
@@ -1822,7 +1330,7 @@
-
+
@@ -1833,7 +1341,7 @@
-
+
@@ -1844,7 +1352,7 @@
-
+
@@ -1856,7 +1364,7 @@
-
+
@@ -1870,7 +1378,21 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1891,801 +1413,641 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -2706,14 +2068,30 @@
-
-
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gen_aspirinV2.py b/gen_aspirinV2.py
index 1c40914..b30b7d5 100644
--- a/gen_aspirinV2.py
+++ b/gen_aspirinV2.py
@@ -5,21 +5,24 @@ Same game as aspirin.sax, different screen model: nothing moves on screen.
(0 = empty) and its text list " ;1;2;...;15;" turns that into the label.
- backgroundColor is linked from logic: white for a number, board colour for the empty cell.
- Board state = cell values v[1..16] held in ADemux2.out2 (boot loads 1..15, 0 = solved).
-- Blank cell: cb = sum_j col_j*(v_j==0), rb likewise (Reset maps v 0..1 -> col..0).
-- Cell i is movable when (col-cb)^2 + (row-rb)^2 == 1. A rising edge of (movable AND
- (touched OR shuffle-pick)) fires a one-scan OneShot; in that scan cell i latches 0 and the
- blank cell latches the moved value MV = sum_i (OS_i ? v_i : 0).
-- Shuffle: same logistic-map RNG and direction codes 2*dc+dr as aspirin.sax.
+- One swap mechanism: each scan at most one pair of cells (A, B) exchanges values.
+ Tile move: A = touched cell T = sum(touch_i ? i : 0), B = blank B = sum(v_i == 0 ? i : 0),
+ only on a new touch and when |T-B| == 4 or (|T-B| == 1, same row).
+- Random deal (tap to start, or SHUFFLE): load the solved board, then a Fisher-Yates pass
+ swaps cell i = 15..2 with cell j = floor(u*i)+1, one swap per scan, u from the logistic-map
+ RNG made ~uniform. The blank stays in cell 16, so the deal is solvable iff it is an even
+ permutation; a parity latch adds one swap of cells 1 and 2 when it is odd.
+ The RNG runs free from boot, so the human's timing picks the board.
- Solved: sum_i i*v_i (cells 1..15) == 1240, the unique maximum (rearrangement inequality).
"""
import base64
+import math
OUT = "aspirinV2.sax"
N = 4
PITCH = 105 # cell pitch in px
CELL = 100 # cell size in px
X0, Y0 = 40, 40 # top-left cell
-SHUFFLE_SECS = 10.0
SOLVED_SUM = sum(i * i for i in range(1, 16)) # 1240
# RGB565 colours
@@ -155,6 +158,11 @@ busy_lbl = page.add("Busy", "ontrolOx::BooleanLabel", x=RX, y=310, width=RW, hei
trueText=buf("Shuffling..."), falseText=buf(" "),
trueTextColor=C_GREY, falseTextColor=C_PAGE,
trueBackgroundColor=C_PAGE, falseBackgroundColor=C_PAGE)
+hint_lbl = page.add("Hint", "ontrolOx::BooleanLabel", x=RX, y=350, width=RW, height=36,
+ fontSize=24, attributes=24,
+ trueText=buf("Tap to start"), falseText=buf(" "),
+ trueTextColor=C_WHITE, falseTextColor=C_PAGE,
+ trueBackgroundColor=C_PAGE, falseBackgroundColor=C_PAGE)
solved_lbl = page.add("Solved", "ontrolOx::BooleanLabel", x=RX, y=395, width=RW, height=60,
fontSize=36, fontAttributes=1, attributes=24,
trueText=buf("SOLVED!"), falseText=buf(" "),
@@ -163,29 +171,116 @@ solved_lbl = page.add("Solved", "ontrolOx::BooleanLabel", x=RX, y=395, width=RW,
link(page, "pageId", oxbase, "homePageId")
# ---------------------------------------------------------------- logic
-# folder order = scan order: glob (blank, RNG) -> cells (move detect) -> mid (moved value)
-# -> latch (state) -> post (display, win, counter). glob/cells read the previous scan's state.
+# One swap mechanism does everything: each scan at most one pair of cells (A, B) swaps values.
+# tile move: A = touched cell T, B = blank cell (only when T is next to the blank)
+# deal step: A = i, B = j (Fisher-Yates), then an optional parity-fix swap of cells 1 and 2
+# Cells are indexed 1..16; the blank holds value 0.
+# Folder order = scan order: glob (blank, touch, RNG, deal control) -> cell (per-cell swap
+# taps) -> mid (values at A and B) -> latch (cell values) -> post (display, win, counter)
+# -> state (deal step, parity). Everything before latch reads the previous scan's values.
logic = root("logic", "sys::Folder")
g = logic.add("glob", "sys::Folder")
-cf = {i: logic.add(f"c{i:02d}", "sys::Folder") for i in CELLS}
+cl = logic.add("cell", "sys::Folder")
mid = logic.add("mid", "sys::Folder")
lat = logic.add("latch", "sys::Folder")
post = logic.add("post", "sys::Folder")
+st = logic.add("state", "sys::Folder")
+
+
+def const(parent, name, v):
+ return parent.add(name, "control::ConstFloat", out=float(v))
+
+
+# state latches first (created early so glob can link to them; they run in their folders)
+V = {i: lat.add(f"V{i:02d}", "control::ADemux2") for i in CELLS} # out2 = cell value
+kst = st.add("K", "control::ADemux2") # out2 = deal step (see below)
+par = st.add("Par", "control::ADemux2") # out2 = deal swap parity
true_c = g.add("TRUE", "control::ConstBool", out=True)
boot = g.add("Boot", "control::OneShot", pulseWidth=0.5) # loads the solved board
link(true_c, "out", boot, "in")
-shreq = g.add("ShReq", "control::Or2")
-link(shuf_btn, "touch", shreq, "in1")
-link(boot, "out", shreq, "in2")
-shuf = g.add("Shuf", "control::OneShot", pulseWidth=SHUFFLE_SECS)
-link(shreq, "out", shuf, "in")
-link(shuf, "out", busy_lbl, "in")
-not_boot = g.add("NotBoot", "control::Not")
-link(boot, "out", not_boot, "in")
-enable = g.add("En", "control::And2") # shuffle moves allowed
-link(shuf, "out", enable, "in1")
-link(not_boot, "out", enable, "in2")
+
+# blank index B = sum_i (v_i == 0 ? i : 0)
+bi = {i: reset(g, f"BI{i:02d}", 0, 1, i, 0) for i in CELLS}
+for i in CELLS:
+ link(V[i], "out2", bi[i], "in")
+bb = tree(g, "B", "control::Add4", list(bi.values()), "out")
+
+# touched cell index T = sum_i (touch_i ? i : 0); cell i's constant i doubles as its solved value
+init = {i: const(g, f"N{i:02d}", i) for i in CELLS}
+tsel = {i: g.add(f"TS{i:02d}", "control::ASW") for i in CELLS}
+for i in CELLS:
+ link(cells[i], "touch", tsel[i], "s1")
+ link(init[i], "out", tsel[i], "in2")
+tt = tree(g, "T", "control::Add4", list(tsel.values()), "out")
+anyt = tree(g, "AT", "control::Or4", list(cells.values()), "touch")
+
+# tap to start: the first touch anywhere (while not started) requests a deal
+anyin = g.add("AnyIn", "control::Or2")
+link(anyt, "out", anyin, "in1")
+link(shuf_btn, "touch", anyin, "in2")
+not_st = g.add("NotSt", "control::Not") # in <- Start (prev scan)
+link(not_st, "out", hint_lbl, "in")
+first = g.add("First", "control::And2")
+link(anyin, "out", first, "in1")
+link(not_st, "out", first, "in2")
+start = g.add("Start", "control::SRLatch") # set once, never reset
+link(anyin, "out", start, "s")
+link(start, "out", not_st, "in")
+dreq = g.add("DReq", "control::Or2")
+link(shuf_btn, "touch", dreq, "in1")
+link(first, "out", dreq, "in2")
+req = g.add("Req", "control::OneShot", pulseWidth=0.05) # one-scan deal request
+link(dreq, "out", req, "in")
+load = g.add("Load", "control::Or2") # load the solved board
+link(boot, "out", load, "in1")
+link(req, "out", load, "in2")
+
+# deal step K: 0..13 = swap i = 15-K with j, 14 = parity fix, >= 15 idle
+dealing = g.add("Deal", "ontrolControl::InRange", lowLimit=-0.5, highLimit=14.5)
+link(kst, "out2", dealing, "in")
+link(dealing, "out", busy_lbl, "in")
+not_deal = g.add("NotDl", "control::Not")
+link(dealing, "out", not_deal, "in")
+swap_st = g.add("SwSt", "ontrolControl::InRange", lowLimit=-0.5, highLimit=13.5)
+link(kst, "out2", swap_st, "in")
+fix_st = g.add("FixSt", "ontrolControl::InRange", lowLimit=13.5, highLimit=14.5)
+link(kst, "out2", fix_st, "in")
+
+# tile move: T next to B <=> |T-B| == 4, or |T-B| == 1 in the same row
+dtb = g.add("DTB", "control::Sub2")
+link(tt, "out", dtb, "in1")
+link(bb, "out", dtb, "in2")
+d2 = g.add("D2", "control::Mul2")
+link(dtb, "out", d2, "in1")
+link(dtb, "out", d2, "in2")
+vert = g.add("Vert", "ontrolControl::InRange", lowLimit=15.5, highLimit=16.5)
+link(d2, "out", vert, "in")
+horz = g.add("Horz", "ontrolControl::InRange", lowLimit=0.5, highLimit=1.5)
+link(d2, "out", horz, "in")
+rows = []
+for nm, src in (("RT", tt), ("RB", bb)): # row = floor((x-1)/4)
+ rq = reset(g, nm + "q", 1, 17, 0, 4)
+ link(src, "out", rq, "in")
+ ri = g.add(nm + "i", "control::F2I")
+ link(rq, "out", ri, "in")
+ rows.append(ri)
+same_row = g.add("SameR", "control::Cmpr")
+link(rows[0], "out", same_row, "x")
+link(rows[1], "out", same_row, "y")
+hz = g.add("HZ", "control::And2")
+link(horz, "out", hz, "in1")
+link(same_row, "xey", hz, "in2")
+adj = g.add("Adj", "control::Or2")
+link(vert, "out", adj, "in1")
+link(hz, "out", adj, "in2")
+tap = g.add("Tap", "control::OneShot", pulseWidth=0.03) # one scan per new touch
+link(anyt, "out", tap, "in")
+mv = g.add("Move", "control::And4")
+link(tap, "out", mv, "in1")
+link(adj, "out", mv, "in2")
+link(not_deal, "out", mv, "in3")
+link(true_c, "out", mv, "in4")
# RNG: x' = 0.003 + 0.9945*4*x*(1-x) (logistic map, never hits 0)
inv = reset(g, "RInv", 0, 1, 1, 0)
@@ -195,108 +290,123 @@ link(rx, "out", inv, "in")
link(rx, "out", mul, "in1")
link(inv, "out", mul, "in2")
link(mul, "out", rx, "in")
-# x -> direction code {-2,-1,1,2}; breakpoints at arcsine quartiles for ~uniform picks
-code = g.add("Code", "control::Linearize",
- x0=0.0, y0=-2.0, x1=0.146, y1=-2.0, x2=0.147, y2=-1.0, x3=0.5, y3=-1.0,
- x4=0.501, y4=1.0, x5=0.853, y5=1.0, x6=0.854, y6=2.0, x7=1.0, y7=2.0)
-link(rx, "out", code, "in")
+# x has an arcsine density; u = (2/pi)*asin(sqrt(x)) is ~uniform. Linearize approximates it
+# with 10 points, then Reset stretches the reachable range onto [0, 0.9999].
+U_PTS = [k / 9 for k in range(10)]
+X_PTS = [math.sin(math.pi * u / 2) ** 2 for u in U_PTS]
+uni = g.add("Uni", "control::Linearize",
+ **{f"{a}{k}": round(v, 6) for k in range(10)
+ for a, v in (("x", X_PTS[k]), ("y", U_PTS[k]))})
+link(rx, "out", uni, "in")
+u_lo = 2 / math.pi * math.asin(math.sqrt(0.003))
+u_hi = 2 / math.pi * math.asin(math.sqrt(0.9975))
+uu = reset(g, "U", u_lo, u_hi, 0, 0.9999)
+link(uni, "out", uu, "in")
-# state latches (created now so glob can link to them; they execute in the latch folder)
-L = {i: {} for i in CELLS}
+# Fisher-Yates over cells 1..15 (blank stays in cell 16): i = 15 - K, j = floor(u*i) + 1
+ii = reset(g, "I", 0, 13, 15, 2)
+link(kst, "out2", ii, "in")
+ui = g.add("UI", "control::Mul2")
+link(uu, "out", ui, "in1")
+link(ii, "out", ui, "in2")
+jfl = g.add("JFl", "control::F2I")
+link(ui, "out", jfl, "in")
+jf = g.add("JF", "control::I2F")
+link(jfl, "out", jf, "in")
+jj = reset(g, "J", 0, 16, 1, 17) # +1
+link(jf, "out", jj, "in")
+ieqj = g.add("IeqJ", "control::Cmpr")
+link(ii, "out", ieqj, "x")
+link(jj, "out", ieqj, "y")
+ineq = g.add("INeJ", "control::Not")
+link(ieqj, "xey", ineq, "in")
+real_sw = g.add("RealSw", "control::And2") # a deal swap that changes cells
+link(swap_st, "out", real_sw, "in1")
+link(ineq, "out", real_sw, "in2")
+# blank in its home cell => solvable iff the permutation is even; fix an odd one with (1 2)
+odd = g.add("Odd", "ontrolControl::InRange", lowLimit=0.5, highLimit=1.5)
+link(par, "out2", odd, "in")
+fix_sw = g.add("FixSw", "control::And2")
+link(fix_st, "out", fix_sw, "in1")
+link(odd, "out", fix_sw, "in2")
+sw_en = g.add("SwEn", "control::Or4")
+link(mv, "out", sw_en, "in1")
+link(real_sw, "out", sw_en, "in2")
+link(fix_sw, "out", sw_en, "in3")
+
+# swap pair: deal step (i, j) / parity fix (1, 2) / tile move (T, B)
+fa = g.add("FA", "control::ASW")
+link(ii, "out", fa, "in1")
+link(init[1], "out", fa, "in2")
+link(fix_st, "out", fa, "s1")
+fb = g.add("FB", "control::ASW")
+link(jj, "out", fb, "in1")
+link(init[2], "out", fb, "in2")
+link(fix_st, "out", fb, "s1")
+sa = g.add("SA", "control::ASW")
+link(tt, "out", sa, "in1")
+link(fa, "out", sa, "in2")
+link(dealing, "out", sa, "s1")
+sb = g.add("SB", "control::ASW")
+link(bb, "out", sb, "in1")
+link(fb, "out", sb, "in2")
+link(dealing, "out", sb, "s1")
+
+# ---------------------------------------------------------------- cell: swap taps
+IA, IB, HIT = {}, {}, {}
+va_src, vb_src = [], []
for i in CELLS:
- lf = lat.add(f"L{i:02d}", "sys::Folder")
- L[i]["init"] = reset(lf, "Init", 0, 1, i % 16, i % 16) # constant: solved value
- L[i]["mv"] = lf.add("Mv", "control::ASW") # OS ? 0 : MV
- L[i]["in"] = lf.add("In", "control::ASW") # boot ? init : move value
- L[i]["zm"] = lf.add("ZM", "control::And2") # blank AND any move
- L[i]["s"] = lf.add("S", "control::Or4")
- L[i]["v"] = lf.add("V", "control::ADemux2") # out2 = cell value
-V = {i: L[i]["v"] for i in CELLS}
+ ia = cl.add(f"IA{i:02d}", "ontrolControl::InRange", lowLimit=i - 0.5, highLimit=i + 0.5)
+ ib = cl.add(f"IB{i:02d}", "ontrolControl::InRange", lowLimit=i - 0.5, highLimit=i + 0.5)
+ link(sa, "out", ia, "in")
+ link(sb, "out", ib, "in")
+ a = cl.add(f"VA{i:02d}", "control::ASW") # isA ? v : 0
+ link(V[i], "out2", a, "in2")
+ link(ia, "out", a, "s1")
+ b = cl.add(f"VB{i:02d}", "control::ASW") # isB ? v : 0
+ link(V[i], "out2", b, "in2")
+ link(ib, "out", b, "s1")
+ ab = cl.add(f"AB{i:02d}", "control::Or2")
+ link(ia, "out", ab, "in1")
+ link(ib, "out", ab, "in2")
+ hit = cl.add(f"H{i:02d}", "control::And2") # this cell swaps this scan
+ link(sw_en, "out", hit, "in1")
+ link(ab, "out", hit, "in2")
+ IA[i], IB[i], HIT[i] = ia, ib, hit
+ va_src.append(a)
+ vb_src.append(b)
-# blank position from the cell values
-bc = {i: reset(g, f"BC{i:02d}", 0, 1, colrow(i)[0], 0) for i in CELLS}
-br = {i: reset(g, f"BR{i:02d}", 0, 1, colrow(i)[1], 0) for i in CELLS}
+# ---------------------------------------------------------------- mid: values at A and B
+va = tree(mid, "SVA", "control::Add4", va_src, "out")
+vb = tree(mid, "SVB", "control::Add4", vb_src, "out")
+
+# ---------------------------------------------------------------- latch: cell values
for i in CELLS:
- link(V[i], "out2", bc[i], "in")
- link(V[i], "out2", br[i], "in")
-cb = tree(g, "CB", "control::Add4", [bc[i] for i in CELLS], "out")
-rb = tree(g, "RB", "control::Add4", [br[i] for i in CELLS], "out")
-
-# ---------------------------------------------------------------- per cell: move detect
-P = {}
-for i in CELLS:
- f = cf[i]
- c, r = colrow(i)
- p = {}
- p["dc"] = reset(f, "Dc", 0, 3, c, c - 3) # col - cb
- p["dr"] = reset(f, "Dr", 0, 3, r, r - 3) # row - rb
- p["sc"] = f.add("Sc", "control::Mul2")
- p["sr"] = f.add("Sr", "control::Mul2")
- p["d2"] = f.add("D2", "control::Add2")
- p["val"] = f.add("Val", "ontrolControl::InRange", lowLimit=0.5, highLimit=1.5)
- p["k"] = f.add("K", "control::Add4") # 2*dc + dr
- p["m"] = f.add("M", "control::Cmpr")
- p["h"] = f.add("H", "control::And2")
- p["p"] = f.add("P", "control::Or2")
- p["g"] = f.add("G", "control::And2")
- p["os"] = f.add("OS", "control::OneShot", pulseWidth=0.03)
- p["z"] = f.add("Z", "ontrolControl::InRange", lowLimit=-0.5, highLimit=0.5) # empty
- p["out"] = f.add("Out", "control::ASW") # OS ? v : 0
- P[i] = p
- link(cb, "out", p["dc"], "in")
- link(rb, "out", p["dr"], "in")
- link(p["dc"], "out", p["sc"], "in1")
- link(p["dc"], "out", p["sc"], "in2")
- link(p["dr"], "out", p["sr"], "in1")
- link(p["dr"], "out", p["sr"], "in2")
- link(p["sc"], "out", p["d2"], "in1")
- link(p["sr"], "out", p["d2"], "in2")
- link(p["d2"], "out", p["val"], "in")
- link(p["dc"], "out", p["k"], "in1")
- link(p["dc"], "out", p["k"], "in2")
- link(p["dr"], "out", p["k"], "in3")
- link(p["k"], "out", p["m"], "x")
- link(code, "out", p["m"], "y")
- link(p["m"], "xey", p["h"], "in1")
- link(enable, "out", p["h"], "in2")
- link(cells[i], "touch", p["p"], "in1")
- link(p["h"], "out", p["p"], "in2")
- link(p["val"], "out", p["g"], "in1")
- link(p["p"], "out", p["g"], "in2")
- link(p["g"], "out", p["os"], "in")
- link(V[i], "out2", p["z"], "in")
- link(V[i], "out2", p["out"], "in2")
- link(p["os"], "out", p["out"], "s1")
-
-# ---------------------------------------------------------------- mid: moved value
-mv = tree(mid, "MV", "control::Add4", [P[i]["out"] for i in CELLS], "out")
-anym = tree(mid, "AM", "control::Or4", [P[i]["os"] for i in CELLS], "out")
-
-# ---------------------------------------------------------------- latch wiring
-for i in CELLS:
- l, p = L[i], P[i]
- link(mv, "out", l["mv"], "in1") # in2 unlinked = 0
- link(p["os"], "out", l["mv"], "s1")
- link(l["mv"], "out", l["in"], "in1")
- link(l["init"], "out", l["in"], "in2")
- link(boot, "out", l["in"], "s1")
- link(p["z"], "out", l["zm"], "in1")
- link(anym, "out", l["zm"], "in2")
- link(boot, "out", l["s"], "in1")
- link(p["os"], "out", l["s"], "in2")
- link(l["zm"], "out", l["s"], "in3")
- link(l["in"], "out", l["v"], "in")
- link(l["s"], "out", l["v"], "s1")
+ sv = lat.add(f"S{i:02d}", "control::ASW") # A takes B's value, B takes A's
+ link(va, "out", sv, "in1")
+ link(vb, "out", sv, "in2")
+ link(IA[i], "out", sv, "s1")
+ li = lat.add(f"L{i:02d}", "control::ASW") # load ? solved value : swap value
+ link(sv, "out", li, "in1")
+ if i < 16: # cell 16 loads 0 (in2 unlinked)
+ link(init[i], "out", li, "in2")
+ link(load, "out", li, "s1")
+ le = lat.add(f"E{i:02d}", "control::Or2")
+ link(load, "out", le, "in1")
+ link(HIT[i], "out", le, "in2")
+ link(li, "out", V[i], "in")
+ link(le, "out", V[i], "s1")
+ lat.kids.remove(V[i]) # latch runs after its inputs
+ lat.kids.append(V[i])
# ---------------------------------------------------------------- post: display, win, counter
for i in CELLS:
- df = post.add(f"D{i:02d}", "sys::Folder")
- num = df.add("Num", "control::F2I")
+ num = post.add(f"Num{i:02d}", "control::F2I")
link(V[i], "out2", num, "in")
link(num, "out", cells[i], "in")
- bgf = reset(df, "BgF", 0, 1, C_BOARD, C_TILE) # 0 -> board, 1..15 -> white
+ bgf = reset(post, f"BgF{i:02d}", 0, 1, C_BOARD, C_TILE) # 0 -> board, 1..15 -> white
link(V[i], "out2", bgf, "in")
- bg = df.add("Bg", "control::F2I")
+ bg = post.add(f"Bg{i:02d}", "control::F2I")
link(bgf, "out", bg, "in")
link(bg, "out", cells[i], "backgroundColor")
@@ -307,21 +417,49 @@ w = tree(post, "WS", "control::Add4", list(wt.values()), "out")
w_ok = post.add("WOk", "ontrolControl::InRange",
lowLimit=SOLVED_SUM - 0.5, highLimit=SOLVED_SUM + 0.5)
link(w, "out", w_ok, "in")
-not_sh = post.add("NotSh", "control::Not")
-link(shuf, "out", not_sh, "in")
-solved = post.add("Solved", "control::And2")
+solved = post.add("Solved", "control::And4")
link(w_ok, "out", solved, "in1")
-link(not_sh, "out", solved, "in2")
+link(not_deal, "out", solved, "in2")
+link(start, "out", solved, "in3") # no SOLVED! before the first game
+link(true_c, "out", solved, "in4")
link(solved, "out", solved_lbl, "in")
cnt = post.add("Cnt", "control::Count")
-link(anym, "out", cnt, "in")
+link(mv, "out", cnt, "in")
link(true_c, "out", cnt, "enable")
-link(shuf, "out", cnt, "r")
+link(dealing, "out", cnt, "r")
cntf = post.add("CntF", "control::I2F")
link(cnt, "out", cntf, "in")
link(cntf, "out", moves_lbl, "in")
+# ---------------------------------------------------------------- state: deal step, parity
+# their inputs are computed here first, then the latches (moved to the end) take them
+knext = reset(st, "KNext", 0, 16, 1, 17) # K + 1, stops at 17 (idle)
+link(kst, "out2", knext, "in")
+k1 = st.add("K1", "control::ASW") # request -> 0
+link(knext, "out", k1, "in1")
+link(req, "out", k1, "s1")
+c16 = const(st, "C16", 16)
+k2 = st.add("K2", "control::ASW") # boot -> 16 (idle)
+link(k1, "out", k2, "in1")
+link(c16, "out", k2, "in2")
+link(boot, "out", k2, "s1")
+link(k2, "out", kst, "in")
+link(true_c, "out", kst, "s1")
+pinv = reset(st, "PInv", 0, 1, 1, 0) # 1 - Par
+link(par, "out2", pinv, "in")
+p1 = st.add("P1", "control::ASW") # request -> 0 (in2 unlinked)
+link(pinv, "out", p1, "in1")
+link(req, "out", p1, "s1")
+link(p1, "out", par, "in")
+ps = st.add("PS", "control::Or2")
+link(req, "out", ps, "in1")
+link(real_sw, "out", ps, "in2")
+link(ps, "out", par, "s1")
+st.kids.remove(kst)
+st.kids.remove(par)
+st.kids += [kst, par]
+
# ---------------------------------------------------------------- write
body = []
for rt in (service, driver, logic, ox):
diff --git a/verify_logic.py b/verify_logic.py
index 6028472..9228cf0 100644
--- a/verify_logic.py
+++ b/verify_logic.py
@@ -345,7 +345,7 @@ def main():
print("re-shuffle OK")
-def astar(s0):
+def astar(s0, weight=1.5):
"""Weighted A* on a 16-tuple (0 = blank); returns the list of cell indices to press."""
import heapq
goal = tuple(list(range(1, 16)) + [0])
@@ -370,11 +370,19 @@ def astar(s0):
ns = tuple(l)
if seen.get(ns, 1e9) > gc + 1:
seen[ns] = gc + 1
- heapq.heappush(pq, (gc + 1 + 1.5 * h(ns), gc + 1, ns, p + [n]))
+ heapq.heappush(pq, (gc + 1 + weight * h(ns), gc + 1, ns, p + [n]))
if len(seen) > 2_000_000:
return None
+def solvable(s):
+ """4x4: permutation parity (blank as 16) == parity of the blank's distance from home."""
+ perm = [v if v else 16 for v in s]
+ inv = sum(1 for a in range(16) for b in range(a + 1, 16) if perm[a] > perm[b])
+ z = s.index(0)
+ return inv % 2 == (3 - z // 4 + 3 - z % 4) % 2
+
+
def main_v2():
"""aspirinV2.sax: fixed cells, numbers shown via each cell's `in`, empty cell recoloured."""
tops, paths = load("aspirinV2.sax")
@@ -416,11 +424,18 @@ def main_v2():
cell[k].v["touch"] = False
step(2)
+ hint = paths["/ox/Game/Hint"]
step(3)
assert state() == goal, "boot did not load the solved board: %s" % (state(),)
- step(120)
+ step(random.randint(20, 80)) # idle: no shuffle until the first tap
check()
- print("after shuffle:\n" + show())
+ assert state() == goal and hint.g("in", False) and not solved() and moves() == 0
+ press(0) # first tap (cell 1, not next to the gap) deals
+ assert not hint.g("in", False)
+ step(30)
+ check()
+ print("after deal:\n" + show())
+ assert solvable(state()), "dealt an unsolvable board"
assert state() != goal and not solved() and moves() == 0
z = state().index(0)
@@ -442,7 +457,7 @@ def main_v2():
assert state() == before and moves() == 2, "hold did not move exactly once"
check()
- path = astar(state())
+ path = astar(state(), weight=4) # random deals are deep; any solution will do
assert path is not None, "solver gave up"
print("solving in", len(path), "presses")
for k in path:
@@ -456,10 +471,28 @@ def main_v2():
paths["/ox/Game/Shuffle"].v["touch"] = True
step(2)
paths["/ox/Game/Shuffle"].v["touch"] = False
- step(120)
+ step(30)
check()
- assert state() != goal and not solved() and moves() == 0
- print("re-shuffle OK")
+ assert state() != goal and not solved() and moves() == 0 and solvable(state())
+ print("re-deal OK")
+
+ # many deals: always solvable, all different, every number reaches every cell
+ seen, where = set(), [set() for _ in range(16)]
+ for _ in range(300):
+ step(random.randint(1, 40))
+ paths["/ox/Game/Shuffle"].v["touch"] = True
+ step(1)
+ paths["/ox/Game/Shuffle"].v["touch"] = False
+ step(20)
+ s = state()
+ check()
+ assert solvable(s), s
+ seen.add(s)
+ assert s[15] == 0, "deal moved the blank out of cell 16"
+ for k, v in enumerate(s):
+ where[k].add(v)
+ assert all(len(w) == 15 for w in where[:15]), [len(w) for w in where]
+ print(f"300 deals: {len(seen)} distinct, all solvable, every number seen in cells 1-15")
if __name__ == "__main__":
diff --git a/viewer/OxViewer.java b/viewer/OxViewer.java
index 660223f..3a0bbe2 100644
--- a/viewer/OxViewer.java
+++ b/viewer/OxViewer.java
@@ -11,7 +11,7 @@ import sedona.sox.*;
* Usage: java -cp ".;sedona.jar" OxViewer [host] [port] [user] [password]
*/
public class OxViewer extends JPanel {
- static final String[] LABELS = {"Title", "Sub", "Moves", "Shuffle", "Busy", "Solving", "Solved"};
+ static final String[] LABELS = {"Title", "Sub", "Moves", "Shuffle", "Busy", "Solving", "Hint", "Solved"};
SoxClient client;
SoxComponent page;
SoxComponent[] tiles = new SoxComponent[16];