File size: 5,246 Bytes
324ecd2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | """Run SUPERBEAM headless with a scripted input sequence.
python scripts/play_chip8.py # replay roms/superbeam_inputs.json
python scripts/play_chip8.py --generate # (re)build the input script with the
# emulator search bot, then replay it
Frame model: one frame = IPF instructions, then tick_timers(). Input events
[frame, key, "down"|"up"] are applied before that frame's instructions.
Writes demo/superbeam_frames.json and (with --generate) roms/superbeam_inputs.json.
"""
import argparse
import json
import os
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, ROOT)
from beamboot.chip8 import Chip8 # noqa: E402
from beamboot.chip8_asm import assemble, check_subset, longest_path # noqa: E402
ASM = os.path.join(ROOT, "roms", "superbeam.asm")
ROM = os.path.join(ROOT, "roms", "superbeam.ch8")
INPUTS = os.path.join(ROOT, "roms", "superbeam_inputs.json")
FRAMES = os.path.join(ROOT, "demo", "superbeam_frames.json")
IPF = 300 # instructions per frame (timer tick period)
KEY_LEFT, KEY_RIGHT, KEY_JUMP = 4, 6, 5
MAX_FRAMES = 600
def build():
"""Assemble roms/superbeam.asm and check that every reachable instruction is one the
in-weights computer models (beamboot/chip8_asm.check_subset)."""
with open(ASM) as f:
rom, symbols = assemble(f.read())
check_subset(rom, (0x200, symbols["halt"]))
return rom, symbols
def run_game(rom, symbols, script, ipf=IPF, max_frames=MAX_FRAMES, capture=False):
"""Replay `script`; stop at the end of the frame in which RAM[WIN] becomes 1."""
m = Chip8()
m.load(rom)
win, deaths = symbols["WIN"], symbols["DEATHS"]
spin = {symbols["wait"], symbols["wait"] + 2, symbols["wait"] + 4}
events = {}
for fr, key, act in script:
events.setdefault(fr, []).append((key, act == "down"))
frames, work = [], []
won = False
f = 0
while f < max_frames:
for key, down in events.get(f, ()):
m.set_key(key, down)
busy = 0
for _ in range(ipf):
if m.step().pc not in spin:
busy += 1
work.append(busy)
if capture:
frames.append(m.display_rows())
m.tick_timers()
f += 1
if m.mem[win]:
won = True
break
return {"won": won, "frames_run": f, "instructions": m.instructions,
"deaths": m.mem[deaths], "work": work, "frames": frames, "machine": m}
def generate(rom, symbols):
"""Winning, zero-death input script from the emulator search bot (beamboot/superbeam_levels.solve,
coins=True: best-first over the 6 env actions at frame_skip=4 that also prefers coins)."""
from beamboot import superbeam_levels as levels
plan = levels.solve(rom, max_nodes=4000, coins=True)
if not plan:
raise RuntimeError("search bot found no winning plan")
per_frame = []
for a in plan:
held, jump = levels._ACTION_KEYS[a]
for i in range(levels.FRAME_SKIP):
per_frame.append(set(held) | ({KEY_JUMP} if jump and i == 0 else set()))
events, held = [], set()
for f, keys in enumerate(per_frame):
for k in sorted(keys - held):
events.append([f, k, "down"])
for k in sorted(held - keys):
events.append([f, k, "up"])
held = set(keys)
return events
def ascii_frame(rows):
return "\n".join("".join("#" if (int(r, 16) >> (63 - c)) & 1 else "." for c in range(64))
for r in rows)
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("--generate", action="store_true")
ap.add_argument("--every", type=int, default=20, help="print an ASCII frame every N frames")
args = ap.parse_args(argv)
rom, symbols = build()
with open(ROM, "wb") as f:
f.write(rom)
if args.generate:
script = generate(rom, symbols)
with open(INPUTS, "w") as f:
json.dump(script, f)
else:
with open(INPUTS) as f:
script = json.load(f)
res = run_game(rom, symbols, script, capture=True)
for i, rows in enumerate(res["frames"]):
if i % args.every == 0 or i == len(res["frames"]) - 1:
print("--- frame %d ---" % i)
print(ascii_frame(rows))
work = res["work"]
bound = longest_path(rom, symbols["wait"] + 6, {symbols["wait"]})
print("won=%s frames=%d instructions=%d deaths=%d coins=%d ipf=%d work/frame max=%d mean=%.1f "
"static worst-case=%d rom=%dB"
% (res["won"], res["frames_run"], res["instructions"], res["deaths"],
res["machine"].mem[symbols["COINS"]], IPF, max(work[:-1]), sum(work) / len(work), bound,
len(rom)))
os.makedirs(os.path.dirname(FRAMES), exist_ok=True)
with open(FRAMES, "w") as f:
json.dump({"ipf": IPF, "rom_size": len(rom), "total_instructions": res["instructions"],
"frames_run": res["frames_run"], "won": res["won"],
"inputs": script, "frames": res["frames"]}, f, separators=(",", ":"))
return 0 if res["won"] else 1
if __name__ == "__main__":
raise SystemExit(main())
|