"""Generates aspirinV2.sax - the 4x4 sliding (15) puzzle with fixed cells. Same game as aspirin.sax, different screen model: nothing moves on screen. - 16 fixed cells, each a NumericSelectLabel. Its `in` is the number the cell holds (0 = empty) and its text list " ;1;2;...;15;" turns that into the label. - backgroundColor is linked from logic: white for a number, board colour for the empty cell. - Board state = cell values v[1..16] held in ADemux2.out2 (boot loads 1..15, 0 = solved). - One swap mechanism: each scan at most one pair of cells (A, B) exchanges values. Tile move: A = touched cell T = sum(touch_i ? i : 0), B = blank B = sum(v_i == 0 ? i : 0), only on a new touch and when |T-B| == 4 or (|T-B| == 1, same row). - Random deal (tap to start, or SHUFFLE): load the solved board, then a Fisher-Yates pass swaps cell i = 15..2 with cell j = floor(u*i)+1, one swap per scan, u from the logistic-map RNG made ~uniform. The blank stays in cell 16, so the deal is solvable iff it is an even permutation; a parity latch adds one swap of cells 1 and 2 when it is odd. The RNG runs free from boot, so the human's timing picks the board. - Solved: sum_i i*v_i (cells 1..15) == 1240, the unique maximum (rearrangement inequality). """ import base64 import math OUT = "aspirinV2.sax" N = 4 PITCH = 105 # cell pitch in px CELL = 100 # cell size in px X0, Y0 = 40, 40 # top-left cell SOLVED_SUM = sum(i * i for i in range(1, 16)) # 1240 # RGB565 colours C_PAGE = 0x10A2 C_BOARD = 0x31A6 C_TILE = 0xFFFF C_TILE_TXT = 0xC000 # dark red C_WHITE = 0xFFFF C_RED = 0xD800 C_GREEN = 0x07E0 C_GREY = 0xAD55 comps = [] links = [] _id = [0] def nid(): _id[0] += 1 return _id[0] def buf(s): return base64.b64encode(s.encode("ascii") + b"\0").decode() class Node: def __init__(self, name, typ, props=None, path=None): assert len(name) <= 7, name self.name, self.typ, self.props = name, typ, props or {} self.kids = [] self.path = path self.id = None def add(self, name, typ, **props): n = Node(name, typ, props, f"{self.path}/{name}") self.kids.append(n) return n def emit(self, ind): self.id = nid() pad = " " * ind out = [f'{pad}'] 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) hint_lbl = page.add("Hint", "ontrolOx::BooleanLabel", x=RX, y=350, width=RW, height=36, fontSize=24, attributes=24, trueText=buf("Tap to start"), falseText=buf(" "), trueTextColor=C_WHITE, falseTextColor=C_PAGE, trueBackgroundColor=C_PAGE, falseBackgroundColor=C_PAGE) solved_lbl = page.add("Solved", "ontrolOx::BooleanLabel", x=RX, y=395, width=RW, height=60, fontSize=36, fontAttributes=1, attributes=24, trueText=buf("SOLVED!"), falseText=buf(" "), trueTextColor=C_GREEN, falseTextColor=C_PAGE, trueBackgroundColor=C_PAGE, falseBackgroundColor=C_PAGE) link(page, "pageId", oxbase, "homePageId") # ---------------------------------------------------------------- logic # One swap mechanism does everything: each scan at most one pair of cells (A, B) swaps values. # tile move: A = touched cell T, B = blank cell (only when T is next to the blank) # deal step: A = i, B = j (Fisher-Yates), then an optional parity-fix swap of cells 1 and 2 # Cells are indexed 1..16; the blank holds value 0. # Folder order = scan order: glob (blank, touch, RNG, deal control) -> cell (per-cell swap # taps) -> mid (values at A and B) -> latch (cell values) -> post (display, win, counter) # -> state (deal step, parity). Everything before latch reads the previous scan's values. logic = root("logic", "sys::Folder") g = logic.add("glob", "sys::Folder") cl = logic.add("cell", "sys::Folder") mid = logic.add("mid", "sys::Folder") lat = logic.add("latch", "sys::Folder") post = logic.add("post", "sys::Folder") st = logic.add("state", "sys::Folder") def const(parent, name, v): return parent.add(name, "control::ConstFloat", out=float(v)) # state latches first (created early so glob can link to them; they run in their folders) V = {i: lat.add(f"V{i:02d}", "control::ADemux2") for i in CELLS} # out2 = cell value kst = st.add("K", "control::ADemux2") # out2 = deal step (see below) par = st.add("Par", "control::ADemux2") # out2 = deal swap parity true_c = g.add("TRUE", "control::ConstBool", out=True) boot = g.add("Boot", "control::OneShot", pulseWidth=0.5) # loads the solved board link(true_c, "out", boot, "in") # blank index B = sum_i (v_i == 0 ? i : 0) bi = {i: reset(g, f"BI{i:02d}", 0, 1, i, 0) for i in CELLS} for i in CELLS: link(V[i], "out2", bi[i], "in") bb = tree(g, "B", "control::Add4", list(bi.values()), "out") # touched cell index T = sum_i (touch_i ? i : 0); cell i's constant i doubles as its solved value init = {i: const(g, f"N{i:02d}", i) for i in CELLS} tsel = {i: g.add(f"TS{i:02d}", "control::ASW") for i in CELLS} for i in CELLS: link(cells[i], "touch", tsel[i], "s1") link(init[i], "out", tsel[i], "in2") tt = tree(g, "T", "control::Add4", list(tsel.values()), "out") anyt = tree(g, "AT", "control::Or4", list(cells.values()), "touch") # tap to start: the first touch anywhere (while not started) requests a deal anyin = g.add("AnyIn", "control::Or2") link(anyt, "out", anyin, "in1") link(shuf_btn, "touch", anyin, "in2") not_st = g.add("NotSt", "control::Not") # in <- Start (prev scan) link(not_st, "out", hint_lbl, "in") first = g.add("First", "control::And2") link(anyin, "out", first, "in1") link(not_st, "out", first, "in2") start = g.add("Start", "control::SRLatch") # set once, never reset link(anyin, "out", start, "s") link(start, "out", not_st, "in") dreq = g.add("DReq", "control::Or2") link(shuf_btn, "touch", dreq, "in1") link(first, "out", dreq, "in2") req = g.add("Req", "control::OneShot", pulseWidth=0.05) # one-scan deal request link(dreq, "out", req, "in") load = g.add("Load", "control::Or2") # load the solved board link(boot, "out", load, "in1") link(req, "out", load, "in2") # deal step K: 0..13 = swap i = 15-K with j, 14 = parity fix, >= 15 idle dealing = g.add("Deal", "ontrolControl::InRange", lowLimit=-0.5, highLimit=14.5) link(kst, "out2", dealing, "in") link(dealing, "out", busy_lbl, "in") not_deal = g.add("NotDl", "control::Not") link(dealing, "out", not_deal, "in") swap_st = g.add("SwSt", "ontrolControl::InRange", lowLimit=-0.5, highLimit=13.5) link(kst, "out2", swap_st, "in") fix_st = g.add("FixSt", "ontrolControl::InRange", lowLimit=13.5, highLimit=14.5) link(kst, "out2", fix_st, "in") # tile move: T next to B <=> |T-B| == 4, or |T-B| == 1 in the same row dtb = g.add("DTB", "control::Sub2") link(tt, "out", dtb, "in1") link(bb, "out", dtb, "in2") d2 = g.add("D2", "control::Mul2") link(dtb, "out", d2, "in1") link(dtb, "out", d2, "in2") vert = g.add("Vert", "ontrolControl::InRange", lowLimit=15.5, highLimit=16.5) link(d2, "out", vert, "in") horz = g.add("Horz", "ontrolControl::InRange", lowLimit=0.5, highLimit=1.5) link(d2, "out", horz, "in") rows = [] for nm, src in (("RT", tt), ("RB", bb)): # row = floor((x-1)/4) rq = reset(g, nm + "q", 1, 17, 0, 4) link(src, "out", rq, "in") ri = g.add(nm + "i", "control::F2I") link(rq, "out", ri, "in") rows.append(ri) same_row = g.add("SameR", "control::Cmpr") link(rows[0], "out", same_row, "x") link(rows[1], "out", same_row, "y") hz = g.add("HZ", "control::And2") link(horz, "out", hz, "in1") link(same_row, "xey", hz, "in2") adj = g.add("Adj", "control::Or2") link(vert, "out", adj, "in1") link(hz, "out", adj, "in2") tap = g.add("Tap", "control::OneShot", pulseWidth=0.03) # one scan per new touch link(anyt, "out", tap, "in") mv = g.add("Move", "control::And4") link(tap, "out", mv, "in1") link(adj, "out", mv, "in2") link(not_deal, "out", mv, "in3") link(true_c, "out", mv, "in4") # RNG: x' = 0.003 + 0.9945*4*x*(1-x) (logistic map, never hits 0) inv = reset(g, "RInv", 0, 1, 1, 0) mul = g.add("RMul", "control::Mul2") rx = reset(g, "RX", 0, 0.25, 0.003, 0.9975) link(rx, "out", inv, "in") link(rx, "out", mul, "in1") link(inv, "out", mul, "in2") link(mul, "out", rx, "in") # x has an arcsine density; u = (2/pi)*asin(sqrt(x)) is ~uniform. Linearize approximates it # with 10 points, then Reset stretches the reachable range onto [0, 0.9999]. U_PTS = [k / 9 for k in range(10)] X_PTS = [math.sin(math.pi * u / 2) ** 2 for u in U_PTS] uni = g.add("Uni", "control::Linearize", **{f"{a}{k}": round(v, 6) for k in range(10) for a, v in (("x", X_PTS[k]), ("y", U_PTS[k]))}) link(rx, "out", uni, "in") u_lo = 2 / math.pi * math.asin(math.sqrt(0.003)) u_hi = 2 / math.pi * math.asin(math.sqrt(0.9975)) uu = reset(g, "U", u_lo, u_hi, 0, 0.9999) link(uni, "out", uu, "in") # Fisher-Yates over cells 1..15 (blank stays in cell 16): i = 15 - K, j = floor(u*i) + 1 ii = reset(g, "I", 0, 13, 15, 2) link(kst, "out2", ii, "in") ui = g.add("UI", "control::Mul2") link(uu, "out", ui, "in1") link(ii, "out", ui, "in2") jfl = g.add("JFl", "control::F2I") link(ui, "out", jfl, "in") jf = g.add("JF", "control::I2F") link(jfl, "out", jf, "in") jj = reset(g, "J", 0, 16, 1, 17) # +1 link(jf, "out", jj, "in") ieqj = g.add("IeqJ", "control::Cmpr") link(ii, "out", ieqj, "x") link(jj, "out", ieqj, "y") ineq = g.add("INeJ", "control::Not") link(ieqj, "xey", ineq, "in") real_sw = g.add("RealSw", "control::And2") # a deal swap that changes cells link(swap_st, "out", real_sw, "in1") link(ineq, "out", real_sw, "in2") # blank in its home cell => solvable iff the permutation is even; fix an odd one with (1 2) odd = g.add("Odd", "ontrolControl::InRange", lowLimit=0.5, highLimit=1.5) link(par, "out2", odd, "in") fix_sw = g.add("FixSw", "control::And2") link(fix_st, "out", fix_sw, "in1") link(odd, "out", fix_sw, "in2") sw_en = g.add("SwEn", "control::Or4") link(mv, "out", sw_en, "in1") link(real_sw, "out", sw_en, "in2") link(fix_sw, "out", sw_en, "in3") # swap pair: deal step (i, j) / parity fix (1, 2) / tile move (T, B) fa = g.add("FA", "control::ASW") link(ii, "out", fa, "in1") link(init[1], "out", fa, "in2") link(fix_st, "out", fa, "s1") fb = g.add("FB", "control::ASW") link(jj, "out", fb, "in1") link(init[2], "out", fb, "in2") link(fix_st, "out", fb, "s1") sa = g.add("SA", "control::ASW") link(tt, "out", sa, "in1") link(fa, "out", sa, "in2") link(dealing, "out", sa, "s1") sb = g.add("SB", "control::ASW") link(bb, "out", sb, "in1") link(fb, "out", sb, "in2") link(dealing, "out", sb, "s1") # ---------------------------------------------------------------- cell: swap taps IA, IB, HIT = {}, {}, {} va_src, vb_src = [], [] for i in CELLS: ia = cl.add(f"IA{i:02d}", "ontrolControl::InRange", lowLimit=i - 0.5, highLimit=i + 0.5) ib = cl.add(f"IB{i:02d}", "ontrolControl::InRange", lowLimit=i - 0.5, highLimit=i + 0.5) link(sa, "out", ia, "in") link(sb, "out", ib, "in") a = cl.add(f"VA{i:02d}", "control::ASW") # isA ? v : 0 link(V[i], "out2", a, "in2") link(ia, "out", a, "s1") b = cl.add(f"VB{i:02d}", "control::ASW") # isB ? v : 0 link(V[i], "out2", b, "in2") link(ib, "out", b, "s1") ab = cl.add(f"AB{i:02d}", "control::Or2") link(ia, "out", ab, "in1") link(ib, "out", ab, "in2") hit = cl.add(f"H{i:02d}", "control::And2") # this cell swaps this scan link(sw_en, "out", hit, "in1") link(ab, "out", hit, "in2") IA[i], IB[i], HIT[i] = ia, ib, hit va_src.append(a) vb_src.append(b) # ---------------------------------------------------------------- mid: values at A and B va = tree(mid, "SVA", "control::Add4", va_src, "out") vb = tree(mid, "SVB", "control::Add4", vb_src, "out") # ---------------------------------------------------------------- latch: cell values for i in CELLS: sv = lat.add(f"S{i:02d}", "control::ASW") # A takes B's value, B takes A's link(va, "out", sv, "in1") link(vb, "out", sv, "in2") link(IA[i], "out", sv, "s1") li = lat.add(f"L{i:02d}", "control::ASW") # load ? solved value : swap value link(sv, "out", li, "in1") if i < 16: # cell 16 loads 0 (in2 unlinked) link(init[i], "out", li, "in2") link(load, "out", li, "s1") le = lat.add(f"E{i:02d}", "control::Or2") link(load, "out", le, "in1") link(HIT[i], "out", le, "in2") link(li, "out", V[i], "in") link(le, "out", V[i], "s1") lat.kids.remove(V[i]) # latch runs after its inputs lat.kids.append(V[i]) # ---------------------------------------------------------------- post: display, win, counter for i in CELLS: num = post.add(f"Num{i:02d}", "control::F2I") link(V[i], "out2", num, "in") link(num, "out", cells[i], "in") bgf = reset(post, f"BgF{i:02d}", 0, 1, C_BOARD, C_TILE) # 0 -> board, 1..15 -> white link(V[i], "out2", bgf, "in") bg = post.add(f"Bg{i:02d}", "control::F2I") link(bgf, "out", bg, "in") link(bg, "out", cells[i], "backgroundColor") wt = {i: reset(post, f"W{i:02d}", 0, 15, 0, 15 * i) for i in range(1, 16)} # i * v_i for i in wt: link(V[i], "out2", wt[i], "in") w = tree(post, "WS", "control::Add4", list(wt.values()), "out") w_ok = post.add("WOk", "ontrolControl::InRange", lowLimit=SOLVED_SUM - 0.5, highLimit=SOLVED_SUM + 0.5) link(w, "out", w_ok, "in") solved = post.add("Solved", "control::And4") link(w_ok, "out", solved, "in1") link(not_deal, "out", solved, "in2") link(start, "out", solved, "in3") # no SOLVED! before the first game link(true_c, "out", solved, "in4") link(solved, "out", solved_lbl, "in") cnt = post.add("Cnt", "control::Count") link(mv, "out", cnt, "in") link(true_c, "out", cnt, "enable") link(dealing, "out", cnt, "r") cntf = post.add("CntF", "control::I2F") link(cnt, "out", cntf, "in") link(cntf, "out", moves_lbl, "in") # ---------------------------------------------------------------- state: deal step, parity # their inputs are computed here first, then the latches (moved to the end) take them knext = reset(st, "KNext", 0, 16, 1, 17) # K + 1, stops at 17 (idle) link(kst, "out2", knext, "in") k1 = st.add("K1", "control::ASW") # request -> 0 link(knext, "out", k1, "in1") link(req, "out", k1, "s1") c16 = const(st, "C16", 16) k2 = st.add("K2", "control::ASW") # boot -> 16 (idle) link(k1, "out", k2, "in1") link(c16, "out", k2, "in2") link(boot, "out", k2, "s1") link(k2, "out", kst, "in") link(true_c, "out", kst, "s1") pinv = reset(st, "PInv", 0, 1, 1, 0) # 1 - Par link(par, "out2", pinv, "in") p1 = st.add("P1", "control::ASW") # request -> 0 (in2 unlinked) link(pinv, "out", p1, "in1") link(req, "out", p1, "s1") link(p1, "out", par, "in") ps = st.add("PS", "control::Or2") link(req, "out", ps, "in1") link(real_sw, "out", ps, "in2") link(ps, "out", par, "s1") st.kids.remove(kst) st.kids.remove(par) st.kids += [kst, par] # ---------------------------------------------------------------- write body = [] for rt in (service, driver, logic, ox): body += rt.emit(1) # checksums pinned to the kits inside the Orion simulator's kits.scode kits = {"sys": "d3984c51", "control": "808b7db3", "inet": "25648ba7", "ontrolControl": "254170a5", "ontrolOrion": "ebaa6c82", "ontrolOx": "641b8633", "ontrolPlatform": "5c12ff9d", "sox": "397a84dd"} xml = ["", "", ""] xml += [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")