"""Offline check of aspirin.sax logic: models the Sedona scan (children first, then propagate links into a component, then execute) for the component types the app uses.""" import random import xml.etree.ElementTree as ET SCAN_MS = 100 class C: def __init__(self, typ, props): self.typ = typ.split("::")[1] self.v = {} self.kids = [] self.links_to = [] self.state = {} for k, val in props.items(): if val in ("true", "false"): self.v[k] = val == "true" else: try: self.v[k] = float(val) except ValueError: self.v[k] = val def g(self, k, d=0.0): return self.v.get(k, d) def start(self): if self.typ == "Count": self.v["out"] = self.g("preset", 0) self.state["last"] = False if self.typ in ("OneShot", "DlyOn"): self.state["last"] = False self.state["end"] = 0 if self.typ == "SRLatch": self.state["ls"] = self.state["lr"] = False def execute(self, now): g, v, t = self.g, self.v, self.typ if t in ("Add2", "Add4"): v["out"] = g("in1") + g("in2") + g("in3") + g("in4") elif t == "Sub2": v["out"] = g("in1") - g("in2") elif t == "Mul2": v["out"] = g("in1") * g("in2") elif t == "Reset": i, a, b, c, d = g("in"), g("inMin"), g("inMax"), g("outMin"), g("outMax") v["out"] = c if i <= a else d if i >= b else (d - c) / (b - a) * (i - a) + c elif t == "Linearize": xs = [g(f"x{i}") for i in range(10)] ys = [g(f"y{i}") for i in range(10)] out = float("nan") i = g("in") for k in range(9): lo_ok = i >= xs[0] if k == 0 else i > xs[k] if lo_ok and i <= xs[k + 1]: m = (ys[k + 1] - ys[k]) / (xs[k + 1] - xs[k]) out = m * i + (ys[k] - m * xs[k]) v["out"] = out elif t == "InRange": v["out"] = g("lowLimit") <= g("in") <= g("highLimit") elif t == "Cmpr": v["xey"] = g("x") == g("y") elif t == "And2": v["out"] = bool(g("in1", False) and g("in2", False)) elif t == "And4": v["out"] = all(g(f"in{k}", False) for k in range(1, 5)) elif t in ("Or2", "Or4"): v["out"] = any(g(f"in{k}", False) for k in range(1, 5)) elif t == "Not": v["out"] = not g("in", False) elif t == "OneShot": i = g("in", False) if i and not self.state["last"]: if not g("out", False): self.state["end"] = now + g("pulseWidth") * 1000 v["out"] = self.state["end"] > now self.state["last"] = i elif t == "ADemux2": if g("s1", False): v["out2"] = g("in") else: v["out1"] = g("in") elif t == "SRLatch": if g("s", False) and not self.state["ls"]: v["out"] = True if g("r", False) and not self.state["lr"]: v["out"] = False self.state["ls"], self.state["lr"] = g("s", False), g("r", False) elif t == "ASW": v["out"] = g("in2") if g("s1", False) else g("in1") elif t == "TickTock": half = 500 / g("ticksPerSec", 1) v["out"] = (now // half) % 2 == 0 elif t == "DlyOn": i = g("in", False) if i and not self.state["last"]: self.state["end"] = now + g("delayTime") * 1000 if not i: self.state["end"] = 0 v["out"] = False else: v["out"] = self.state["end"] < now if v["out"]: self.state["end"] = 0 self.state["last"] = i elif t == "F2I": v["out"] = float(int(g("in"))) elif t == "I2F": v["out"] = g("in") elif t == "Count": if not g("enable", False): return c = g("out") if g("in", False) and not self.state["last"]: c += 1 if g("r", False): c = g("preset", 0) v["out"] = c self.state["last"] = g("in", False) def load(path): root = ET.parse(path).getroot() paths = {} order = [] def walk(el, prefix): comps = [] for ce in el.findall("comp"): props = {p.get("name"): p.get("val") for p in ce.findall("prop")} c = C(ce.get("type"), props) p = f"{prefix}/{ce.get('name')}" paths[p] = c c.kids = walk(ce, p) comps.append(c) return comps tops = walk(root.find("app"), "") for le in root.find("links").findall("link"): fp, fs = le.get("from").rsplit(".", 1) tp, ts = le.get("to").rsplit(".", 1) paths[tp].links_to.append((paths[fp], fs, ts)) return tops, paths def run_scan(tops, now): def ex(c): for k in c.kids: ex(k) for src, fs, ts in c.links_to: c.v[ts] = src.v.get(fs, 0.0) c.execute(now) for t in tops: ex(t) def board(paths): grid = [[" ."] * 4 for _ in range(4)] for t in range(1, 16): w = paths[f"/ox/Game/Tile{t:02d}"] col = int((w.g("x") - 40) // 105) row = int((w.g("y") - 40) // 105) grid[row][col] = f"{t:3d}" return grid def main_solver(): """aspirin_solver.sax: exact-length shuffle, then one inverse move per second until solved.""" tops, paths = load("aspirin_solver.sax") for c in paths.values(): c.start() now = 0 goal = [[" 1", " 2", " 3", " 4"], [" 5", " 6", " 7", " 8"], [" 9", " 10", " 11", " 12"], [" 13", " 14", " 15", " ."]] snapshots, solve_times = [], [] last = board(paths) shuffle_done_at = None for _ in range(1200): # 120 s now += SCAN_MS + random.randint(-3, 3) run_scan(tops, now) b = board(paths) shuffling = paths["/logic/glob/Shuf"].g("out", False) if b != last and not shuffling and shuffle_done_at is not None: solve_times.append(now) if not shuffling and shuffle_done_at is None and now > 1000: shuffle_done_at = now snapshots.append(b) print("after shuffle:") print("\n".join(" ".join(r) for r in b)) assert b != goal, "shuffle left board solved" last = b if shuffle_done_at and paths["/logic/post/Solved"].g("out", False): break assert paths["/logic/post/Solved"].g("out", False), "auto solver did not finish" assert board(paths) == goal gaps = [b - a for a, b in zip(solve_times, solve_times[1:])] print(f"solved with {len(solve_times)} moves, first move {solve_times[0]-shuffle_done_at} ms " f"after shuffle, gaps {min(gaps)}-{max(gaps)} ms, moves label " f"{paths['/logic/post/CntF'].g('out')}") assert len(solve_times) == 30 and min(gaps) >= 900 assert paths["/logic/stkReg/Empty"].g("out", False) # SHUFFLE again paths["/ox/Game/Shuffle"].v["touch"] = True for _ in range(3): now += SCAN_MS run_scan(tops, now) paths["/ox/Game/Shuffle"].v["touch"] = False for _ in range(600): now += SCAN_MS run_scan(tops, now) if paths["/logic/post/Solved"].g("out", False) and not paths["/logic/glob/Shuf"].g("out", False): break assert board(paths) == goal and paths["/logic/post/Solved"].g("out", False) print("second shuffle + auto solve OK") def main(): tops, paths = load("aspirin.sax") for c in paths.values(): c.start() now = 0 def step(n=1): nonlocal now for _ in range(n): now += SCAN_MS + random.randint(-3, 3) run_scan(tops, now) def check_perm(): cells = [tuple(r) for r in board(paths)] flat = [x for r in cells for x in r] nums = sorted(int(x) for x in flat if x.strip() != ".") assert nums == list(range(1, 16)), cells step(120) # boot shuffle (10 s) + settle check_perm() g = board(paths) print("after shuffle:") print("\n".join(" ".join(r) for r in g)) solved = paths["/logic/post/Solved"].g("out", False) print("solved flag:", solved, "moves:", paths["/logic/post/CntF"].g("out")) assert not solved def press(t): paths[f"/ox/Game/Tile{t:02d}"].v["touch"] = True step(2) paths[f"/ox/Game/Tile{t:02d}"].v["touch"] = False step(2) # solve by replaying moves with a simple BFS-free strategy: random legal presses until solved is # impractical, so instead verify mechanics: invalid press does nothing, valid press swaps. def blank(): g = board(paths) for r in range(4): for c in range(4): if g[r][c].strip() == ".": return r, c def tile_at(r, c): return int(board(paths)[r][c]) br, bc = blank() # pick a non-adjacent tile far = next((r, c) for r in range(4) for c in range(4) if abs(r - br) + abs(c - bc) >= 2) before = board(paths) press(tile_at(*far)) assert board(paths) == before, "non-adjacent tile moved" moves0 = paths["/logic/post/CntF"].g("out") # adjacent press nr, nc = (br, bc - 1) if bc > 0 else (br, bc + 1) t = tile_at(nr, nc) press(t) assert blank() == (nr, nc) and tile_at(br, bc) == t, "valid move failed" assert paths["/logic/post/CntF"].g("out") == moves0 + 1 # holding the finger must not oscillate paths[f"/ox/Game/Tile{t:02d}"].v["touch"] = True step(10) paths[f"/ox/Game/Tile{t:02d}"].v["touch"] = False step(2) assert blank() == (br, bc), "hold did not move exactly once" check_perm() # solve: undo using a solver on the current board (IDA* is overkill; use reverse of # a fresh scramble instead): press SHUFFLE-free path -> brute force with BFS on small scramble # Easier: drive tiles to solved using a known-solvable sequence from Python solver. from collections import deque def state(): g = board(paths) return tuple(0 if x.strip() == "." else int(x) for r in g for x in r) goal = tuple(list(range(1, 16)) + [0]) # A* with manhattan heuristic import heapq def h(s): d = 0 for i, v in enumerate(s): if v: d += abs(i // 4 - (v - 1) // 4) + abs(i % 4 - (v - 1) % 4) return d s0 = state() pq = [(h(s0), 0, s0, [])] seen = {s0: 0} path = None while pq: f, gc, s, p = heapq.heappop(pq) if s == goal: path = p break 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 + [s[n]])) if len(seen) > 2_000_000: break assert path is not None, "solver gave up" print("solving in", len(path), "presses") for t in path: press(t) step(25) print("\n".join(" ".join(r) for r in board(paths))) assert state() == goal assert paths["/logic/post/Solved"].g("out", False), "solved flag not set" print("SOLVED flag OK, moves:", paths["/logic/post/CntF"].g("out")) # SHUFFLE button re-scrambles and clears the flag/counter paths["/ox/Game/Shuffle"].v["touch"] = True step(2) paths["/ox/Game/Shuffle"].v["touch"] = False step(120) check_perm() assert state() != goal and not paths["/logic/post/Solved"].g("out", False) assert paths["/logic/post/CntF"].g("out") == 0 print("re-shuffle OK") if __name__ == "__main__": import sys main_solver() if "--solver" in sys.argv else main()