Something went wrong. Try again.
HEXWORK - A Lantern in the Deep: 3D ASCII terminal roguelike RPG in Rust.
Entirely designed and built by Muse Spark 1.3 to evaluate if I like the model.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171#!/usr/bin/env python3"""AC7: real 80x24 Tinker grids; pristine route saves prepared by the binary."""import errnoimport fcntlimport jsonimport osimport reimport selectorsimport structimport subprocessimport sysimport termiosimport timefrom hud_essentials import Grid
def main(): binary, data, content, mode, run_id, ending = sys.argv[1:] confirm_label = "Shift+Tab" if mode == "remapped" else "Enter" quit_label = "Shift+q" if mode == "remapped" else "Esc" confirm = b"\x1b[Z" if mode == "remapped" else b"\r" quit_key = b"Q" if mode == "remapped" else b"\x1b" args = [binary, "--data-dir", data, "--content-root", content, "--load", run_id] env = os.environ.copy() for key in ("HEXWORK_NO_COLOR", "HEXWORK_FORCE_ASCII", "HEXWORK_FORCE_256", "HEXWORK_FORCE_TC", "HEXWORK_CONTENT_ROOT", "NO_COLOR"): env.pop(key, None) env.update(TERM="xterm-256color", COLORTERM="truecolor", LANG="C.UTF-8") if mode != "story": env["HEXWORK_NO_COLOR"] = "1" child = selector = master = slave = None grid = Grid() snapshots = [] capture = bytearray() stage = 0 seen = None evaluated = None settle_at = 0.0 SETTLE_SECONDS = 0.05 deadline = time.monotonic() + 300 try: master, slave = os.openpty() fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack("HHHH", 24, 80, 0, 0)) child = subprocess.Popen(args, stdin=slave, stdout=slave, stderr=slave, close_fds=True, start_new_session=True, env=env) os.close(slave) slave = None os.set_blocking(master, False) selector = selectors.DefaultSelector() selector.register(master, selectors.EVENT_READ) registered = True while child.poll() is None or registered: remaining = deadline - time.monotonic() if remaining <= 0: raise RuntimeError("deadline stage=%d rows=%r" % (stage, grid.rows())) if registered: for _, _ in selector.select(min(remaining, .25)): try: chunk = os.read(master, 65536) except OSError as error: if error.errno == errno.EIO: chunk = b"" elif error.errno in (errno.EAGAIN, errno.EWOULDBLOCK): continue else: raise if not chunk: selector.unregister(master) registered = False break capture.extend(chunk) assert len(capture) <= 8 * 1024 * 1024 grid.feed(chunk) else: try: child.wait(timeout=min(remaining, .25)) except subprocess.TimeoutExpired: pass # The renderer is a cell diff against the previous frame, so a # repaint need not rewrite the bottom-right cell and grid.frames may # not advance. The real frame contract is "the composed screen # changed and the repaint settled", so cue on the decoded grid # content after a short quiet period. current = tuple(grid.rows()) now = time.monotonic() if current != seen: seen = current settle_at = now + SETTLE_SECONDS if child.poll() is not None or not registered: continue if now < settle_at or current == evaluated: continue evaluated = current rows = grid.rows() text = "\n".join(rows) action = None if stage == 0 and "Tinker: identify 15cu" in text: assert quit_label in text and confirm_label in text action = confirm elif stage == 1 and rows[0].startswith("Tinker: identification") and "Scroll:" not in text: assert "25cu" in text and "15cu" in text, rows action = b"4" elif stage == 2 and "43" in text: action = confirm elif stage == 3 and "Scroll:" in text: assert "43" in text and "Unidentified" in text action = confirm + confirm # drained duplicate must not purchase elif stage == 4 and "Identify for 15cu?" in text: assert "43" in text and "25cu" in text assert "Confirm: " + confirm_label in text, rows assert "Cancel: " + quit_label in text, rows action = quit_key elif stage == 5 and "Scroll:" in text and "Identify for 15cu?" not in text: assert "Unidentified" in text and "25cu" in text action = quit_key if ending == "cancel" else confirm elif stage == 6: if ending == "cancel" and "Scroll:" not in text: action = quit_key elif ending == "commit" and "Identify for 15cu?" in text: action = confirm # separate fresh input after completed prompt elif stage == 7: if ending == "cancel" and "Tinker: identify 15cu" in text: action = quit_key elif ending == "commit" and "Scroll:" in text and "Known" in text: assert "43" in text and "10cu" in text assert "identified" in text.lower() and "15cu" in text action = quit_key elif stage == 8 and ending == "commit" and "Scroll:" not in text: action = quit_key elif stage == 9 and ending == "commit" and "Tinker: identify 15cu" in text: action = quit_key if action is not None: if stage in (1, 2, 3, 5, 8) or (stage == 7 and ending == "commit"): ribbon = rows[17] assert ribbon.startswith("Tinker:"), rows assert quit_label + " back" in ribbon, rows assert "drop" not in ribbon.lower() and "wield" not in ribbon.lower(), rows assert "look: move" not in ribbon, rows if stage in (1, 2, 3, 5): assert confirm_label in ribbon, rows assert len(rows) == 24 and all(len(row) == 80 for row in rows) snapshots.append(rows) os.write(master, action) stage += 1 # One evaluation per action, even when a discrete input leaves # the composed screen byte-identical (e.g. re-selecting a slot). evaluated = None status = child.wait(timeout=5) assert status == 0, (status, stage, grid.rows()) assert stage == (8 if ending == "cancel" else 10), (stage, grid.rows()) sgr = re.findall(rb"\x1b\[([0-9;]*)m", capture) color_sgr = any(any(30 <= int(p) <= 49 or 90 <= int(p) <= 107 for p in params.split(b";") if p) for params in sgr) assert color_sgr == (mode == "story"), (mode, sgr[:20]) print(json.dumps({"exit": status, "size": [80, 24], "snapshots": snapshots, "color_sgr": color_sgr})) finally: if child is not None: if child.poll() is None: child.kill() child.wait(timeout=5) if selector is not None: selector.close() for fd in (master, slave): if fd is not None: os.close(fd)
if __name__ == "__main__": main()