Compare commits

..
2 Commits
Author SHA1 Message Date
ArdaandClaude Opus 5.5 f078dc99f3 aspirinV2: tap to start with a random solvable deal
The first tap (or SHUFFLE) loads the solved board and deals it with a
Fisher-Yates pass driven by the free-running logistic-map RNG, so tap
timing picks the board. A parity latch adds one swap of cells 1 and 2
when needed, so every deal is solvable.

Tile moves and deal swaps share one cell-swap mechanism, cutting RAM
from 56.4 kB (failed to start on the Orion) to 34.6 kB (runs). Adds
deal checks to verify_logic.py --v2 and the hint label to the viewer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:01:59 +03:00
ArdaandClaude Opus 5.5 d1f4e87526 Add aspirinV2: fixed-cell 15-puzzle
Cells never move; each NumericSelectLabel shows the number it holds
on white and the empty cell takes the board colour via a linked
backgroundColor. Adds the V2 generator, build step (with a direct-Java
sedonac fallback), offline verifier mode (--v2) and viewer support.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:14:16 +03:00
8 changed files with 2800 additions and 11 deletions
+20 -2
View File
@@ -7,6 +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.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) | | `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` | 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 ## Run in the simulator
@@ -17,13 +18,14 @@ The simulator is headless; to watch the screen run `viewer\run_viewer.cmd`
## Build ## Build
```bash ```bash
python build.py # gen .sax -> sedonac .sab -> launcher backup zips (both apps) python build.py # gen .sax -> sedonac .sab -> launcher backup zips (all apps)
python verify_logic.py # offline scan-engine model test of the game python verify_logic.py # offline scan-engine model test of the game
python verify_logic.py --solver python verify_logic.py --solver
python verify_logic.py --v2
``` ```
`gen_aspirin.py` holds layout, colours, shuffle length (`SHUFFLE_MOVES`) and solver delay `gen_aspirin.py` holds layout, colours, shuffle length (`SHUFFLE_MOVES`) and solver delay
(`SOLVE_DELAY`). Paths to the Sedona install are at the top of `build.py`. (`SOLVE_DELAY`); `gen_aspirinV2.py` generates V2. Paths to the Sedona install are at the top of `build.py`.
## How it works ## How it works
@@ -33,3 +35,19 @@ python verify_logic.py --solver
by the shuffle); it latches the blank's cell and the blank follows automatically. by the shuffle); it latches the blank's cell and the blank follows automatically.
- Shuffle uses a logistic-map RNG made of `Reset`/`Mul2` blocks; only legal moves, so always solvable. - Shuffle uses a logistic-map RNG made of `Reset`/`Mul2` blocks; only legal moves, so always solvable.
- Solved = blank home and the weighted sum of tile offsets is 0 (rearrangement inequality). - Solved = blank home and the weighted sum of tile offsets is 0 (rearrangement inequality).
### V2 (fixed cells)
- 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).
- 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.
Binary file not shown.
BIN
View File
Binary file not shown.
+2097
View File
File diff suppressed because it is too large Load Diff
+19 -4
View File
@@ -1,4 +1,4 @@
"""Builds aspirin(.sax/_solver.sax) -> .sab and packages app-ORION-<name>.zip for simulatorLauncher.exe. """Builds aspirin(.sax/_solver.sax/V2.sax) -> .sab and packages app-ORION-<name>.zip for simulatorLauncher.exe.
Zip layout matches the launcher's ExampleApp backups: Zip layout matches the launcher's ExampleApp backups:
app.sax, device/app.sab, device/kits.scode, device/fonts/*.oxfl, fontBankXL/*.oxfl app.sax, device/app.sab, device/kits.scode, device/fonts/*.oxfl, fontBankXL/*.oxfl
@@ -22,9 +22,24 @@ env = dict(os.environ, sedona_home=SEDONA)
with zipfile.ZipFile(EXAMPLE) as src: with zipfile.ZipFile(EXAMPLE) as src:
scode = src.read("device/kits.scode") scode = src.read("device/kits.scode")
for name, args in (("aspirin", []), ("aspirin_solver", ["--solver"])): JAVA = r"C:\Niagara\Niagara-4.15.1.16\jre\bin\java.exe" # fallback when sedonac.exe finds no JVM
subprocess.check_call([sys.executable, "gen_aspirin.py"] + args)
subprocess.check_call([os.path.join(SEDONA, "bin", "sedonac.exe"), f"{name}.sax"], env=env)
def sedonac(sax):
if subprocess.call([os.path.join(SEDONA, "bin", "sedonac.exe"), sax], env=env) == 0:
return
lib = os.path.join(SEDONA, "lib")
subprocess.check_call([JAVA, f"-Dsedona.home={SEDONA}", "-cp",
os.pathsep.join([os.path.join(lib, "sedonac.jar"),
os.path.join(lib, "sedona.jar")]),
"sedonac.Main", sax])
for name, gen in (("aspirin", ["gen_aspirin.py"]),
("aspirin_solver", ["gen_aspirin.py", "--solver"]),
("aspirinV2", ["gen_aspirinV2.py"])):
subprocess.check_call([sys.executable] + gen)
sedonac(f"{name}.sax")
out = f"app-ORION-{name}.zip" out = f"app-ORION-{name}.zip"
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z: with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
z.write(f"{name}.sax", "app.sax") z.write(f"{name}.sax", "app.sax")
+483
View File
@@ -0,0 +1,483 @@
"""Generates aspirinV2.sax - the 4x4 sliding (15) puzzle with fixed cells.
Same game as aspirin.sax, different screen model: nothing moves on screen.
- 16 fixed cells, each a NumericSelectLabel. Its `in` is the number the cell holds
(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).
- 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
SOLVED_SUM = sum(i * i for i in range(1, 16)) # 1240
# RGB565 colours
C_PAGE = 0x10A2
C_BOARD = 0x31A6
C_TILE = 0xFFFF
C_TILE_TXT = 0xC000 # dark red
C_WHITE = 0xFFFF
C_RED = 0xD800
C_GREEN = 0x07E0
C_GREY = 0xAD55
comps = []
links = []
_id = [0]
def nid():
_id[0] += 1
return _id[0]
def buf(s):
return base64.b64encode(s.encode("ascii") + b"\0").decode()
class Node:
def __init__(self, name, typ, props=None, path=None):
assert len(name) <= 7, name
self.name, self.typ, self.props = name, typ, props or {}
self.kids = []
self.path = path
self.id = None
def add(self, name, typ, **props):
n = Node(name, typ, props, f"{self.path}/{name}")
self.kids.append(n)
return n
def emit(self, ind):
self.id = nid()
pad = " " * ind
out = [f'{pad}<comp name="{self.name}" id="{self.id}" type="{self.typ}">']
for k, v in self.props.items():
if isinstance(v, bool):
v = "true" if v else "false"
elif isinstance(v, float):
v = repr(round(v, 6))
out.append(f'{pad} <prop name="{k}" val="{v}"/>')
for kid in self.kids:
out += kid.emit(ind + 1)
out.append(f"{pad}</comp>")
return out
def link(a, aslot, b, bslot):
links.append(f' <link from="{a.path}.{aslot}" to="{b.path}.{bslot}"/>')
def root(name, typ, **props):
return Node(name, typ, props, f"/{name}")
def reset(parent, name, in_min, in_max, out_min, out_max):
"""Affine map (clamped) using control::Reset - lets constants live in config props."""
return parent.add(name, "control::Reset", inMin=float(in_min), inMax=float(in_max),
outMin=float(out_min), outMax=float(out_max))
def tree(parent, prefix, typ, sources, slot):
"""Add4/Or4 tree over up to 16 sources; returns the final block."""
groups = [sources[i:i + 4] for i in range(0, len(sources), 4)]
partial = []
for gi, grp in enumerate(groups):
a = parent.add(f"{prefix}{gi}", typ)
for k, src in enumerate(grp):
link(src, slot, a, f"in{k + 1}")
partial.append(a)
tot = parent.add(f"{prefix}T", typ)
for k, p in enumerate(partial):
link(p, "out", tot, f"in{k + 1}")
return tot
CELLS = range(1, 17)
def colrow(i):
return (i - 1) % N, (i - 1) // N
# ---------------------------------------------------------------- service
service = root("service", "sys::Folder")
plat = service.add("OrionPl", "ontrolOrion::OrionPlatformService")
oxbase = plat.add("OxBase", "ontrolOx::OxBase", userTimeout=255, lcdSleepTimeout=60,
lcdSleepBrightness=30)
users = service.add("users", "sys::UserService")
users.add("admin", "sys::User", cred="wHy/iCHUdXWkccFgala3nl9uamg=", perm=2147483647, prov=255)
users.add("guest", "sys::User", cred="jVShQjDWIkrr+o3lYLcAPPvNVio=", perm=63)
service.add("sox", "sox::SoxService")
driver = root("driver", "sys::Folder")
# ---------------------------------------------------------------- ox (screen)
ox = root("ox", "sys::Folder")
page = ox.add("Game", "ontrolOx::OxPage", backgroundColor=C_PAGE, touchTimeout=0)
page.add("Board", "ontrolOx::Geometry", x=X0 - 5, y=Y0 - 5,
width=N * PITCH + 5, height=N * PITCH + 5, backgroundColor=C_BOARD)
NUMBERS = " ;" + "".join(f"{n};" for n in range(1, 16)) # index 0 = empty
assert len(NUMBERS) <= 64
cells = {}
for i in CELLS:
c, r = colrow(i)
cells[i] = page.add(f"Cell{i:02d}", "ontrolOx::NumericSelectLabel",
x=X0 + c * PITCH, y=Y0 + r * PITCH, width=CELL, height=CELL,
fontSize=48, fontAttributes=1, attributes=24, text=NUMBERS,
textColor=C_TILE_TXT,
backgroundColor=C_BOARD if i == 16 else C_TILE)
RX, RW = 490, 290
page.add("Title", "ontrolOx::PushButtonLabel", x=RX, y=40, width=RW, height=70,
fontSize=52, fontAttributes=1, attributes=24, text=buf("ASPIRIN"),
textColor=C_RED, backgroundColor=C_PAGE)
page.add("Sub", "ontrolOx::PushButtonLabel", x=RX, y=110, width=RW, height=34,
fontSize=18, attributes=24, text=buf("15 PUZZLE V2"),
textColor=C_GREY, backgroundColor=C_PAGE)
moves_lbl = page.add("Moves", "ontrolOx::NumericLabel", x=RX, y=160, width=RW, height=50,
fontSize=32, attributes=24, text=buf("Moves: %.0f"),
textColor=C_WHITE, backgroundColor=C_PAGE)
shuf_btn = page.add("Shuffle", "ontrolOx::PushButtonLabel", x=RX + 30, y=225, width=RW - 60,
height=70, fontSize=32, fontAttributes=1, attributes=24,
text=buf("SHUFFLE"), textColor=C_WHITE, backgroundColor=C_RED)
busy_lbl = page.add("Busy", "ontrolOx::BooleanLabel", x=RX, y=310, width=RW, height=36,
fontSize=24, attributes=24,
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(" "),
trueTextColor=C_GREEN, falseTextColor=C_PAGE,
trueBackgroundColor=C_PAGE, falseBackgroundColor=C_PAGE)
link(page, "pageId", oxbase, "homePageId")
# ---------------------------------------------------------------- logic
# 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")
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")
# 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)
mul = g.add("RMul", "control::Mul2")
rx = reset(g, "RX", 0, 0.25, 0.003, 0.9975)
link(rx, "out", inv, "in")
link(rx, "out", mul, "in1")
link(inv, "out", mul, "in2")
link(mul, "out", rx, "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")
# 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:
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)
# ---------------------------------------------------------------- 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:
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:
num = post.add(f"Num{i:02d}", "control::F2I")
link(V[i], "out2", num, "in")
link(num, "out", cells[i], "in")
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 = post.add(f"Bg{i:02d}", "control::F2I")
link(bgf, "out", bg, "in")
link(bg, "out", cells[i], "backgroundColor")
wt = {i: reset(post, f"W{i:02d}", 0, 15, 0, 15 * i) for i in range(1, 16)} # i * v_i
for i in wt:
link(V[i], "out2", wt[i], "in")
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")
solved = post.add("Solved", "control::And4")
link(w_ok, "out", solved, "in1")
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(mv, "out", cnt, "in")
link(true_c, "out", cnt, "enable")
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):
body += rt.emit(1)
# checksums pinned to the kits inside the Orion simulator's kits.scode
kits = {"sys": "d3984c51", "control": "808b7db3", "inet": "25648ba7",
"ontrolControl": "254170a5", "ontrolOrion": "ebaa6c82", "ontrolOx": "641b8633",
"ontrolPlatform": "5c12ff9d", "sox": "397a84dd"}
xml = ["<?xml version='1.0'?>", "<sedonaApp>", "<schema>"]
xml += [f' <kit name="{k}" checksum="{c}" />' for k, c in kits.items()]
xml += ["</schema>", "<app>",
' <prop name="deviceName" val="ORION"/>',
f' <prop name="appName" val="{OUT[:-4]}"/>',
' <prop name="scanPeriod" val="100"/>']
xml += body
xml += ["</app>", "<links>"] + links + ["</links>", "</sedonaApp>", ""]
with open(OUT, "w", newline="\n") as fh:
fh.write("\n".join(xml))
print(f"{OUT}: {_id[0]} components, {len(links)} links")
+156 -1
View File
@@ -345,6 +345,161 @@ def main():
print("re-shuffle OK") print("re-shuffle OK")
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])
def h(s):
return sum(abs(i // 4 - (v - 1) // 4) + abs(i % 4 - (v - 1) % 4)
for i, v in enumerate(s) if v)
pq = [(h(s0), 0, s0, [])]
seen = {s0: 0}
while pq:
_, gc, s, p = heapq.heappop(pq)
if s == goal:
return p
z = s.index(0)
for dz in (-1, 1, -4, 4):
n = z + dz
if n < 0 or n > 15 or (dz in (-1, 1) and n // 4 != z // 4):
continue
l = list(s)
l[z], l[n] = l[n], l[z]
ns = tuple(l)
if seen.get(ns, 1e9) > gc + 1:
seen[ns] = gc + 1
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")
for c in paths.values():
c.start()
now = 0
goal = tuple(list(range(1, 16)) + [0])
cell = [paths[f"/ox/Game/Cell{i:02d}"] for i in range(1, 17)]
def step(n=1):
nonlocal now
for _ in range(n):
now += SCAN_MS + random.randint(-3, 3)
run_scan(tops, now)
def state():
return tuple(int(c.g("in")) for c in cell)
def check():
s = state()
assert sorted(s) == list(range(16)), s
for v, c in zip(s, cell): # empty cell = board colour, numbers on white
assert int(c.g("backgroundColor")) == (0x31A6 if v == 0 else 0xFFFF), (s, v)
def show():
s = state()
return "\n".join(" ".join(f"{v:3d}" if v else " ." for v in s[r * 4:r * 4 + 4])
for r in range(4))
def solved():
return paths["/logic/post/Solved"].g("out", False)
def moves():
return paths["/logic/post/CntF"].g("out")
def press(k):
cell[k].v["touch"] = True
step(2)
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(random.randint(20, 80)) # idle: no shuffle until the first tap
check()
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)
far = next(k for k in range(16) if abs(k // 4 - z // 4) + abs(k % 4 - z % 4) >= 2)
before = state()
press(far)
assert state() == before, "non-adjacent cell moved"
press(z)
assert state() == before, "pressing the empty cell changed the board"
n = z - 1 if z % 4 else z + 1
v = before[n]
press(n)
s = state()
assert s[n] == 0 and s[z] == v and moves() == 1, "valid move failed"
cell[z].v["touch"] = True # hold: exactly one move back
step(10)
cell[z].v["touch"] = False
step(2)
assert state() == before and moves() == 2, "hold did not move exactly once"
check()
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:
press(k)
step(5)
print(show())
check()
assert state() == goal and solved(), "solved flag not set"
print("SOLVED flag OK, moves:", moves())
paths["/ox/Game/Shuffle"].v["touch"] = True
step(2)
paths["/ox/Game/Shuffle"].v["touch"] = False
step(30)
check()
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__": if __name__ == "__main__":
import sys import sys
main_solver() if "--solver" in sys.argv else main() if "--solver" in sys.argv:
main_solver()
elif "--v2" in sys.argv:
main_v2()
else:
main()
+25 -4
View File
@@ -11,10 +11,12 @@ import sedona.sox.*;
* Usage: java -cp ".;sedona.jar" OxViewer [host] [port] [user] [password] * Usage: java -cp ".;sedona.jar" OxViewer [host] [port] [user] [password]
*/ */
public class OxViewer extends JPanel { 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; SoxClient client;
SoxComponent page; SoxComponent page;
SoxComponent[] tiles = new SoxComponent[16]; SoxComponent[] tiles = new SoxComponent[16];
SoxComponent[] cells = new SoxComponent[17]; // aspirinV2: fixed cells Cell01..Cell16
boolean v2;
SoxComponent[] labels = new SoxComponent[LABELS.length]; SoxComponent[] labels = new SoxComponent[LABELS.length];
String lastBoard = ""; String lastBoard = "";
@@ -22,7 +24,9 @@ public class OxViewer extends JPanel {
this.client = client; this.client = client;
SoxComponent app = client.loadApp(); SoxComponent app = client.loadApp();
page = load(app, "ox", "Game"); page = load(app, "ox", "Game");
for (int t = 1; t <= 15; t++) tiles[t] = load(page, String.format("Tile%02d", t)); v2 = load(page, "Cell01") != null;
if (v2) for (int k = 1; k <= 16; k++) cells[k] = load(page, String.format("Cell%02d", k));
else for (int t = 1; t <= 15; t++) tiles[t] = load(page, String.format("Tile%02d", t));
for (int i = 0; i < LABELS.length; i++) labels[i] = load(page, LABELS[i]); for (int i = 0; i < LABELS.length; i++) labels[i] = load(page, LABELS[i]);
setPreferredSize(new Dimension(800, 480)); setPreferredSize(new Dimension(800, 480));
} }
@@ -40,6 +44,7 @@ public class OxViewer extends JPanel {
void poll() throws Exception { void poll() throws Exception {
for (SoxComponent c : tiles) if (c != null) client.update(c, 0x06); for (SoxComponent c : tiles) if (c != null) client.update(c, 0x06);
for (SoxComponent c : cells) if (c != null) client.update(c, 0x06);
for (SoxComponent c : labels) if (c != null) client.update(c, 0x06); for (SoxComponent c : labels) if (c != null) client.update(c, 0x06);
String b = boardText(); String b = boardText();
if (!b.equals(lastBoard)) { if (!b.equals(lastBoard)) {
@@ -77,12 +82,19 @@ public class OxViewer extends JPanel {
String boardText() { String boardText() {
String[] cells = new String[16]; String[] cells = new String[16];
java.util.Arrays.fill(cells, " ."); java.util.Arrays.fill(cells, " .");
for (int t = 1; t <= 15; t++) { int whites = 0;
if (v2) for (int k = 1; k <= 16; k++) {
int n = i(this.cells[k], "in");
if (n != 0) cells[k - 1] = String.format("%2d", n);
if ((i(this.cells[k], "backgroundColor") & 0xFFFF) == 0xFFFF) whites++;
}
else for (int t = 1; t <= 15; t++) {
int col = (i(tiles[t], "x") - 40) / 105, row = (i(tiles[t], "y") - 40) / 105; int col = (i(tiles[t], "x") - 40) / 105, row = (i(tiles[t], "y") - 40) / 105;
if (col >= 0 && col < 4 && row >= 0 && row < 4) cells[row * 4 + col] = String.format("%2d", t); if (col >= 0 && col < 4 && row >= 0 && row < 4) cells[row * 4 + col] = String.format("%2d", t);
} }
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (int r = 0; r < 4; r++) sb.append(r == 0 ? "" : " | ").append(String.join(" ", java.util.Arrays.copyOfRange(cells, r * 4, r * 4 + 4))); for (int r = 0; r < 4; r++) sb.append(r == 0 ? "" : " | ").append(String.join(" ", java.util.Arrays.copyOfRange(cells, r * 4, r * 4 + 4)));
if (v2) sb.append(" white=").append(whites);
return sb.toString(); return sb.toString();
} }
@@ -102,7 +114,13 @@ public class OxViewer extends JPanel {
g.fillRect(0, 0, 800, 480); g.fillRect(0, 0, 800, 480);
g.setColor(c565(0x31A6)); g.setColor(c565(0x31A6));
g.fillRect(35, 35, 425, 425); g.fillRect(35, 35, 425, 425);
for (int t = 1; t <= 15; t++) { if (v2) for (int k = 1; k <= 16; k++) {
SoxComponent w = cells[k];
String[] opts = str(w, "text").split(";");
int n = i(w, "in");
box(g, w, n < opts.length ? opts[n].trim() : "", i(w, "textColor"), i(w, "backgroundColor"), true, true);
}
else for (int t = 1; t <= 15; t++) {
SoxComponent w = tiles[t]; SoxComponent w = tiles[t];
box(g, w, str(w, "text"), i(w, "textColor"), i(w, "backgroundColor"), true, true); box(g, w, str(w, "text"), i(w, "textColor"), i(w, "backgroundColor"), true, true);
} }
@@ -140,6 +158,9 @@ public class OxViewer extends JPanel {
Thread.sleep(500); Thread.sleep(500);
} }
} }
if (GraphicsEnvironment.isHeadless()) { // -Djava.awt.headless=true: console log only
while (true) { v.poll(); Thread.sleep(200); }
}
JFrame f = new JFrame("Orion Screen - aspirin (live via SOX " + host + ":" + port + ")"); JFrame f = new JFrame("Orion Screen - aspirin (live via SOX " + host + ":" + port + ")");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(v); f.add(v);