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>
This commit is contained in:
@@ -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` | 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
|
## 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,11 @@ 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). 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).
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+2719
File diff suppressed because it is too large
Load Diff
@@ -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")
|
||||||
|
|||||||
@@ -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}<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)
|
||||||
|
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 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")
|
||||||
+123
-1
@@ -345,6 +345,128 @@ def main():
|
|||||||
print("re-shuffle OK")
|
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__":
|
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()
|
||||||
|
|||||||
+24
-3
@@ -15,6 +15,8 @@ public class OxViewer extends JPanel {
|
|||||||
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user