"""Generates aspirin.sax - a 4x4 sliding (15) puzzle for the Ontrol Orion (800x480). Built only from stock kits (control, ontrolControl, ontrolOx, ontrolOrion), so it runs on the stock simulator scode. How it works - Each tile t is a PushButtonLabel whose x/y are linked from logic, so tiles move on screen. - Tile state = offset from its home cell (du, dv), held in ADemux2.out2 (starts at 0 = solved). - Blank cell: cb = 3 - sum(du), rb = 3 - sum(dv) (cells form a permutation). - Tile 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 that latches the blank's cell into the tile; the blank then becomes the tile's old cell automatically. - Shuffle: a logistic-map RNG (free running every scan) picks a direction code 2*dc+dr in {-2,-1,1,2}; the tile whose code matches moves. Runs for a few seconds, at startup and whenever SHUFFLE is pressed. Only legal moves => always solvable. - Solved check: blank home AND sum_t t*(linear offset_t) == 0 (rearrangement inequality). """ import base64 import sys # --solver: demo variant (aspirin_solver.sax). Shuffle makes exactly SHUFFLE_MOVES moves # (no immediate back-steps) and pushes each move's code on a stack; after a pause the # auto solver pops the stack and plays the inverse move every SOLVE_PERIOD seconds. # No stock function block can search for a solution, so the solver undoes the shuffle. SOLVER = "--solver" in sys.argv OUT = "aspirin_solver.sax" if SOLVER else "aspirin.sax" SHUFFLE_MOVES = 30 STACK = 32 SOLVE_DELAY = 2.0 N = 4 PITCH = 105 # tile pitch in px TILE = 100 # tile size in px X0, Y0 = 40, 40 # top-left tile SHUFFLE_SECS = 10.0 # 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 = [] # xml lines 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 sum_tree(parent, prefix, sources, slot): """Sum of up to 16 float sources with Add4 blocks; returns the final Add4.""" groups = [sources[i:i + 4] for i in range(0, len(sources), 4)] partial = [] for gi, g in enumerate(groups): a = parent.add(f"{prefix}{gi}", "control::Add4") for k, src in enumerate(g): link(src, slot, a, f"in{k + 1}") partial.append(a) tot = parent.add(f"{prefix}T", "control::Add4") for k, p in enumerate(partial): link(p, "out", tot, f"in{k + 1}") return tot def or_tree(parent, prefix, sources, slot): groups = [sources[i:i + 4] for i in range(0, len(sources), 4)] partial = [] for gi, g in enumerate(groups): o = parent.add(f"{prefix}{gi}", "control::Or4") for k, src in enumerate(g): link(src, slot, o, f"in{k + 1}") partial.append(o) tot = parent.add(f"{prefix}T", "control::Or4") for k, p in enumerate(partial): link(p, "out", tot, f"in{k + 1}") return tot # ---------------------------------------------------------------- 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) tiles = {} for t in range(1, 16): hc, hr = (t - 1) % N, (t - 1) // N tiles[t] = page.add(f"Tile{t:02d}", "ontrolOx::PushButtonLabel", x=X0 + hc * PITCH, y=Y0 + hr * PITCH, width=TILE, height=TILE, fontSize=48, fontAttributes=1, attributes=24, text=buf(str(t)), textColor=C_TILE_TXT, backgroundColor=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("AUTO SOLVER" if SOLVER else "15 PUZZLE"), 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) if SOLVER: solving_lbl = page.add("Solving", "ontrolOx::BooleanLabel", x=RX, y=350, width=RW, height=36, fontSize=24, attributes=24, trueText=buf("Auto-solving..."), 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 logic = root("logic", "sys::Folder") g = logic.add("glob", "sys::Folder") # executes before tile folders true_c = g.add("TRUE", "control::ConstBool", out=True) # shuffle control: startup pulse OR button -> OneShot(SHUFFLE_SECS) boot = g.add("Boot", "control::OneShot", pulseWidth=0.5) link(true_c, "out", boot, "in") shreq = g.add("ShReq", "control::Or2") link(boot, "out", shreq, "in2") if SOLVER: # SHUFFLE only accepted when the stack is empty (puzzle solved / idle) btn_ok = g.add("BtnOk", "control::And2") link(shuf_btn, "touch", btn_ok, "in1") link(btn_ok, "out", shreq, "in1") shreq_p = g.add("ShReqP", "control::OneShot", pulseWidth=0.05) # one-scan request link(shreq, "out", shreq_p, "in") shuf = g.add("Shuf", "control::SRLatch") # set by request, reset after SHUFFLE_MOVES link(shreq_p, "out", shuf, "s") else: link(shuf_btn, "touch", shreq, "in1") shuf = g.add("Shuf", "control::OneShot", pulseWidth=SHUFFLE_SECS) link(shreq, "out", shuf, "in") link(shuf, "out", busy_lbl, "in") # RNG: x' = 0.003 + 0.9945*4*x*(1-x) (logistic map, never hits 0) inv = reset(g, "RInv", 0, 1, 1, 0) # 1 - x mul = g.add("RMul", "control::Mul2") # x(1-x) rx = reset(g, "RX", 0, 0.25, 0.003, 0.9975) # next x 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") if SOLVER: # sel = code the tiles compare against: random code while shuffling, else inverse of stack top neg_top = reset(g, "NegTop", -2, 2, 2, -2) # -top (linked from stack below) bt = g.add("BT", "control::Cmpr") # random pick would undo last move link(code, "out", bt, "x") link(neg_top, "out", bt, "y") not_bt = g.add("NotBT", "control::Not") link(bt, "xey", not_bt, "in") # no move in the scan right after a move, so every move is a separate edge for counting not_am = g.add("NotAM", "control::Not") # in <- any-move (post, prev scan) sh_en = g.add("ShEn", "control::And4") link(shuf, "out", sh_en, "in1") link(not_bt, "out", sh_en, "in2") link(not_am, "out", sh_en, "in3") not_rq = g.add("NotRq", "control::Not") # no move while the counter resets link(shreq_p, "out", not_rq, "in") link(not_rq, "out", sh_en, "in4") tick = g.add("Tick", "control::TickTock", ticksPerSec=1) step_os = g.add("Step", "control::OneShot", pulseWidth=0.05) # one scan per second link(tick, "out", step_os, "in") sv_en = g.add("SvEn", "control::And2") # in1 <- solving (post, prev scan) link(step_os, "out", sv_en, "in2") en = g.add("En", "control::Or2") link(sh_en, "out", en, "in1") link(sv_en, "out", en, "in2") sel = g.add("Sel", "control::ASW") link(neg_top, "out", sel, "in1") link(code, "out", sel, "in2") link(shuf, "out", sel, "s1") pick, pick_slot, enable = sel, "out", en else: pick, pick_slot, enable = code, "out", shuf # tile folders are created in reverse order (15..1) so the suffix-sum chain settles in one scan tf = {t: logic.add(f"t{t:02d}", "sys::Folder") for t in range(15, 0, -1)} post = logic.add("post", "sys::Folder") # tile components are built first so the global sums (in glob) can link to their holds tile_parts = {} for t in range(15, 0, -1): f = tf[t] hc, hr = (t - 1) % N, (t - 1) // N p = {} p["ru"] = reset(f, "Ru", 0, 3, -hc, 3 - hc) # target du = cb - hc p["rv"] = reset(f, "Rv", 0, 3, -hr, 3 - hr) # target dv = rb - hr p["dc"] = f.add("Dc", "control::Sub2") # col - cb p["dr"] = f.add("Dr", "control::Sub2") # 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") if not SOLVER: 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["du"] = f.add("Du", "control::ADemux2") p["dv"] = f.add("Dv", "control::ADemux2") p["xr"] = reset(f, "Xr", -3, 3, X0 + (hc - 3) * PITCH, X0 + (hc + 3) * PITCH) p["yr"] = reset(f, "Yr", -3, 3, Y0 + (hr - 3) * PITCH, Y0 + (hr + 3) * PITCH) p["xi"] = f.add("Xi", "control::F2I") p["yi"] = f.add("Yi", "control::F2I") p["q4"] = reset(f, "Q4", -3, 3, -12, 12) # 4*dv p["t"] = f.add("T", "control::Add4") # suffix sum of (du + 4dv) tile_parts[t] = p # global blank position (in glob folder, uses previous-scan holds) su = sum_tree(g, "SU", [tile_parts[t]["du"] for t in range(1, 16)], "out2") sv = sum_tree(g, "SV", [tile_parts[t]["dv"] for t in range(1, 16)], "out2") cb = reset(g, "CB", -30, 30, 33, -27) # 3 - sum(du) rb = reset(g, "RB", -30, 30, 33, -27) # 3 - sum(dv) link(su, "out", cb, "in") link(sv, "out", rb, "in") for t in range(15, 0, -1): p = tile_parts[t] tile = tiles[t] link(cb, "out", p["ru"], "in") link(rb, "out", p["rv"], "in") link(p["du"], "out2", p["dc"], "in1") link(p["ru"], "out", p["dc"], "in2") link(p["dv"], "out2", p["dr"], "in1") link(p["rv"], "out", p["dr"], "in2") 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(pick, pick_slot, p["m"], "y") link(p["m"], "xey", p["h"], "in1") link(enable, "out", p["h"], "in2") link(p["val"], "out", p["g"], "in1") if SOLVER: # demo: tiles are driven by the solver only link(p["h"], "out", p["g"], "in2") else: link(tile, "touch", p["p"], "in1") link(p["h"], "out", p["p"], "in2") link(p["p"], "out", p["g"], "in2") link(p["g"], "out", p["os"], "in") link(p["ru"], "out", p["du"], "in") link(p["os"], "out", p["du"], "s1") link(p["rv"], "out", p["dv"], "in") link(p["os"], "out", p["dv"], "s1") link(p["du"], "out2", p["xr"], "in") link(p["dv"], "out2", p["yr"], "in") link(p["xr"], "out", p["xi"], "in") link(p["yr"], "out", p["yi"], "in") link(p["xi"], "out", tile, "x") link(p["yi"], "out", tile, "y") link(p["dv"], "out2", p["q4"], "in") link(p["du"], "out2", p["t"], "in1") link(p["q4"], "out", p["t"], "in2") if t < 15: link(tile_parts[t + 1]["t"], "out", p["t"], "in3") # ---------------------------------------------------------------- post: win check + move counter w = sum_tree(post, "W", [tile_parts[t]["t"] for t in range(1, 16)], "out") w_ok = post.add("WOk", "ontrolControl::InRange", lowLimit=-0.5, highLimit=0.5) link(w, "out", w_ok, "in") bsum = post.add("BSum", "control::Add2") link(cb, "out", bsum, "in1") link(rb, "out", bsum, "in2") b_ok = post.add("BOk", "ontrolControl::InRange", lowLimit=5.5, highLimit=6.5) link(bsum, "out", b_ok, "in") not_sh = post.add("NotSh", "control::Not") link(shuf, "out", not_sh, "in") solved = post.add("Solved", "control::And4") link(w_ok, "out", solved, "in1") link(b_ok, "out", solved, "in2") link(not_sh, "out", solved, "in3") link(true_c, "out", solved, "in4") link(solved, "out", solved_lbl, "in") anym = or_tree(post, "AM", [tile_parts[t]["os"] for t in range(1, 16)], "out") 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") if SOLVER: # stop the shuffle after SHUFFLE_MOVES moves sh_cnt = post.add("ShCnt", "control::Count") link(anym, "out", sh_cnt, "in") link(true_c, "out", sh_cnt, "enable") link(shreq_p, "out", sh_cnt, "r") link(anym, "out", not_am, "in") sh_cntf = post.add("ShCntF", "control::I2F") link(sh_cnt, "out", sh_cntf, "in") sh_done = post.add("ShDone", "ontrolControl::InRange", lowLimit=SHUFFLE_MOVES - 0.5, highLimit=10000.0) link(sh_cntf, "out", sh_done, "in") link(sh_done, "out", shuf, "r") # solving starts SOLVE_DELAY s after the shuffle ends dly = post.add("Dly", "control::DlyOn", delayTime=SOLVE_DELAY) link(not_sh, "out", dly, "in") link(dly, "out", sv_en, "in1") not_sv = post.add("NotSv", "control::Not") link(solved, "out", not_sv, "in") sv_lbl = post.add("SvLbl", "control::And2") link(dly, "out", sv_lbl, "in1") link(not_sv, "out", sv_lbl, "in2") link(sv_lbl, "out", solving_lbl, "in") # move stack: r1 = top. Selectors all run before the registers, so every register # latches its neighbour's previous value -> push shifts down, pop shifts up. stk_s = logic.add("stkSel", "sys::Folder") stk_r = logic.add("stkReg", "sys::Folder") sels = [stk_s.add(f"S{i:02d}", "control::ASW") for i in range(1, STACK + 1)] regs = [stk_r.add(f"R{i:02d}", "control::ADemux2") for i in range(1, STACK + 1)] for i in range(STACK): link(shuf, "out", sels[i], "s1") # true = push, false = pop if i == 0: link(sel, "out", sels[i], "in2") # push the executed code else: link(regs[i - 1], "out2", sels[i], "in2") if i + 1 < STACK: link(regs[i + 1], "out2", sels[i], "in1") # pop (last slot pulls in 0) link(sels[i], "out", regs[i], "in") link(anym, "out", regs[i], "s1") # latch on every executed move empty = stk_r.add("Empty", "ontrolControl::InRange", lowLimit=-0.5, highLimit=0.5) link(regs[0], "out2", empty, "in") link(empty, "out", btn_ok, "in2") link(regs[0], "out2", neg_top, "in") # ---------------------------------------------------------------- write body = [] for r in (service, driver, logic, ox): body += r.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")