diff --git a/README.md b/README.md index e747010..ab0fe8e 100644 --- a/README.md +++ b/README.md @@ -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_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 | ## Run in the simulator @@ -17,13 +18,14 @@ The simulator is headless; to watch the screen run `viewer\run_viewer.cmd` ## Build ```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 --solver +python verify_logic.py --v2 ``` `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 @@ -33,3 +35,11 @@ python verify_logic.py --solver 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. - 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). Blank position is + derived from the values; a move latches 0 into the touched cell and its value into the blank. +- Solved = sum of i * value_i over cells 1..15 is 1240 (its unique maximum). diff --git a/app-ORION-aspirinV2.zip b/app-ORION-aspirinV2.zip new file mode 100644 index 0000000..5d4f94e Binary files /dev/null and b/app-ORION-aspirinV2.zip differ diff --git a/aspirinV2.sab b/aspirinV2.sab new file mode 100644 index 0000000..8f22121 Binary files /dev/null and b/aspirinV2.sab differ diff --git a/aspirinV2.sax b/aspirinV2.sax new file mode 100644 index 0000000..d336b75 --- /dev/null +++ b/aspirinV2.sax @@ -0,0 +1,2719 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.py b/build.py index 5bc6194..ce4eee0 100644 --- a/build.py +++ b/build.py @@ -1,4 +1,4 @@ -"""Builds aspirin(.sax/_solver.sax) -> .sab and packages app-ORION-.zip for simulatorLauncher.exe. +"""Builds aspirin(.sax/_solver.sax/V2.sax) -> .sab and packages app-ORION-.zip for simulatorLauncher.exe. Zip layout matches the launcher's ExampleApp backups: 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: scode = src.read("device/kits.scode") -for name, args in (("aspirin", []), ("aspirin_solver", ["--solver"])): - subprocess.check_call([sys.executable, "gen_aspirin.py"] + args) - subprocess.check_call([os.path.join(SEDONA, "bin", "sedonac.exe"), f"{name}.sax"], env=env) +JAVA = r"C:\Niagara\Niagara-4.15.1.16\jre\bin\java.exe" # fallback when sedonac.exe finds no JVM + + +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" with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z: z.write(f"{name}.sax", "app.sax") diff --git a/gen_aspirinV2.py b/gen_aspirinV2.py new file mode 100644 index 0000000..1c40914 --- /dev/null +++ b/gen_aspirinV2.py @@ -0,0 +1,345 @@ +"""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). +- 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. +- Solved: sum_i i*v_i (cells 1..15) == 1240, the unique maximum (rearrangement inequality). +""" +import base64 + +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 +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}'] + 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} ') + for kid in self.kids: + out += kid.emit(ind + 1) + out.append(f"{pad}") + return out + + +def link(a, aslot, b, bslot): + links.append(f' ') + + +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) +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 +# 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. +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} +mid = logic.add("mid", "sys::Folder") +lat = logic.add("latch", "sys::Folder") +post = logic.add("post", "sys::Folder") + +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") + +# 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 -> 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") + +# state latches (created now so glob can link to them; they execute in the latch folder) +L = {i: {} for i in CELLS} +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} + +# 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} +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") + +# ---------------------------------------------------------------- post: display, win, counter +for i in CELLS: + df = post.add(f"D{i:02d}", "sys::Folder") + num = df.add("Num", "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 + link(V[i], "out2", bgf, "in") + bg = df.add("Bg", "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") +not_sh = post.add("NotSh", "control::Not") +link(shuf, "out", not_sh, "in") +solved = post.add("Solved", "control::And2") +link(w_ok, "out", solved, "in1") +link(not_sh, "out", solved, "in2") +link(solved, "out", solved_lbl, "in") + +cnt = post.add("Cnt", "control::Count") +link(anym, "out", cnt, "in") +link(true_c, "out", cnt, "enable") +link(shuf, "out", cnt, "r") +cntf = post.add("CntF", "control::I2F") +link(cnt, "out", cntf, "in") +link(cntf, "out", moves_lbl, "in") + +# ---------------------------------------------------------------- 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 += [f' ' for k, c in kits.items()] +xml += ["", "", + ' ', + f' ', + ' '] +xml += body +xml += ["", ""] + links + ["", "", ""] + +with open(OUT, "w", newline="\n") as fh: + fh.write("\n".join(xml)) +print(f"{OUT}: {_id[0]} components, {len(links)} links") diff --git a/verify_logic.py b/verify_logic.py index 3221525..6028472 100644 --- a/verify_logic.py +++ b/verify_logic.py @@ -345,6 +345,128 @@ def main(): print("re-shuffle OK") +def astar(s0): + """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 + 1.5 * h(ns), gc + 1, ns, p + [n])) + if len(seen) > 2_000_000: + return None + + +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) + + step(3) + assert state() == goal, "boot did not load the solved board: %s" % (state(),) + step(120) + check() + print("after shuffle:\n" + show()) + 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()) + 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(120) + check() + assert state() != goal and not solved() and moves() == 0 + print("re-shuffle OK") + + if __name__ == "__main__": 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() diff --git a/viewer/OxViewer.java b/viewer/OxViewer.java index 0a05f38..660223f 100644 --- a/viewer/OxViewer.java +++ b/viewer/OxViewer.java @@ -15,6 +15,8 @@ public class OxViewer extends JPanel { SoxClient client; SoxComponent page; SoxComponent[] tiles = new SoxComponent[16]; + SoxComponent[] cells = new SoxComponent[17]; // aspirinV2: fixed cells Cell01..Cell16 + boolean v2; SoxComponent[] labels = new SoxComponent[LABELS.length]; String lastBoard = ""; @@ -22,7 +24,9 @@ public class OxViewer extends JPanel { this.client = client; SoxComponent app = client.loadApp(); 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]); setPreferredSize(new Dimension(800, 480)); } @@ -40,6 +44,7 @@ public class OxViewer extends JPanel { void poll() throws Exception { 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); String b = boardText(); if (!b.equals(lastBoard)) { @@ -77,12 +82,19 @@ public class OxViewer extends JPanel { String boardText() { String[] cells = new String[16]; 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; if (col >= 0 && col < 4 && row >= 0 && row < 4) cells[row * 4 + col] = String.format("%2d", t); } 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))); + if (v2) sb.append(" white=").append(whites); return sb.toString(); } @@ -102,7 +114,13 @@ public class OxViewer extends JPanel { g.fillRect(0, 0, 800, 480); g.setColor(c565(0x31A6)); 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]; 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); } } + 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 + ")"); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.add(v);