"""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")