Add Pac-Man power-on flow and document Odin integration

- Add a four-second arcade-style boot sequence followed by the
  Character / Nickname introduction.
- Separate cabinet power-on from RFID credit and player Start.
- Add operator Power on control and cabinet power-state reporting.
- Prevent repeated power commands from restarting boot or gameplay.
- Ignore RFID scans received before the cabinet is ready.
- Add power-state, RFID, controller, and GPIO feedback regression tests.

- Expand the master walkthrough with export, packaging, file transfer,
  fresh-Pi setup, RFID compilation, and end-to-end testing instructions.
- Correct tester hardware documentation: BCM23/J20 drives an LED;
  physical buttons connect through a separate I2C1 input board.
- Document operator-triggered power-on as the current test workflow
  and retain the temporary 2.4 configuration compatibility notes.
- Record successful physical testing of power-on, RFID credit,
  and player Start.

- Add an Odin integration README describing the proposed Rust/NATS
  bridge, shared messages, Flutter GM interface, autopilot sequencing,
  hardware ownership, and Starfish SSH-management role.
- Define migration milestones and information needed from the
  framework owner; link the integration guide from the root README.

Validation: 69 controller tests, 10 GPIO tests, Pac-Man power/RFID
checks, managed-cabinet lifecycle checks, and visual screen inspection.
This commit is contained in:
Jeff Luke committed 2026-10-17 18:47:26 -07:00
1 parent c1b2195834
commit 41ef12f954
27 files changed
+577 -48

No files matched your search

+31 -3
View File
@@ -157,6 +157,11 @@ class Room:
self.state.update(session=str(uuid.uuid4()), phase="preparing" if action == "prepare" else "resetting",
reward="not requested", commands=[], seen=[])
self.command(action)
elif action == "power_on":
if self.state["game"] != "pacman" or "pacman_power_v1" not in self.features or self.state["phase"] != "powered_off":
raise ValueError("Power on requires a prepared, powered-off Pac-Man cabinet (2.4+)")
self.state["phase"] = "booting"
self.command("power_on")
elif action == "start":
if self.state.get("game_config", {}).get("rfid_credit_device"):
raise ValueError("Scan a card and press Start on the cabinet for this RFID scenario")
@@ -214,9 +219,13 @@ class Room:
self.state["reward"] = "GPIO accepted (physical delivery unconfirmed)"
else:
self.state["reward"] = "requested"
elif command["type"] == "power_on":
if not success: self.state["phase"] = "needs_prepare"
elif success:
self.state["phase"] = {"prepare": "ready", "reset": "waiting" if self.state.get("gate") else "idle", "start": "playing"}[command["type"]]
if command["type"] == "prepare" and self.state.pop("auto_start", False):
if command["type"] == "prepare" and self.state["game"] == "pacman" and "pacman_power_v1" in self.features:
self.state["phase"] = "powered_off" if self.state.get("game_config", {}).get("power_on_device") else "booting"
if command["type"] == "prepare" and self.state["phase"] == "ready" and self.state.pop("auto_start", False):
self.state["phase"] = "starting"
self.command("start")
if command["type"] == "prepare":
@@ -232,7 +241,14 @@ class Room:
self.state["seen"].append(event_id)
self.state["seen"] = self.state["seen"][-500:]
name = report.get("event")
if name == "started" and self.state["phase"] in ("ready", "starting"):
if name == "power_booting" and self.state["game"] == "pacman" and self.state["phase"] == "powered_off":
self.state["phase"] = "booting"
elif name == "power_ready" and self.state["game"] == "pacman" and self.state["phase"] == "booting":
self.state["phase"] = "ready"
if self.state.pop("auto_start", False):
self.state["phase"] = "starting"
self.command("start")
elif name == "started" and self.state["phase"] in ("ready", "starting"):
self.state["phase"] = "playing"
elif name == "won" and self.state["phase"] == "playing":
self.state["phase"] = "won"
@@ -282,6 +298,18 @@ class Fleet:
limits.update({"galaga": {"score_to_win": (0, 1000000), "stages_to_win": (0, 100)},
"frogger": {"homes_to_win": (0, 5), "boards_to_win": (0, 99)},
"pacman": {"boards_to_win": (0, 100), "score_to_win": (0, 1000000)}}.get(game, {}))
power_device = settings.get("power_on_device")
if power_device is not None:
if game != "pacman" or not isinstance(power_device, str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", power_device):
raise ValueError("Invalid power-on input device")
if not any(d.get("id") == power_device and d.get("kind") == "input" for d in self.catalog["cabinets"][cabinet].get("devices", [])):
raise ValueError("Power-on device is missing from the cabinet PCB input inventory")
if "pacman_power_v1" not in self.rooms[cabinet].features:
raise ValueError("Update the game export: power-on inputs require arcade-cabinet-2.4")
if self.rooms[cabinet].gpio != "real":
raise ValueError("Power-on inputs require real GPIO feedback")
if item.get("activate_after_win"):
raise ValueError("Choose button power-on or automatic dependency activation")
credit_device = settings.get("rfid_credit_device")
if credit_device is not None:
if game != "pacman" or not isinstance(credit_device, str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", credit_device):
@@ -299,7 +327,7 @@ class Fleet:
if credit_device is None or not isinstance(uids, list) or len(uids) > 100 or any(not isinstance(uid, str) or not re.fullmatch(r"[0-9A-F]{8}", uid) for uid in uids):
raise ValueError("RFID allowed UIDs must be a list of uppercase 8-digit hexadecimal IDs")
for key, value in settings.items():
if key in ("rfid_credit_device", "rfid_allowed_uids"): continue
if key in ("rfid_credit_device", "rfid_allowed_uids", "power_on_device"): continue
if key not in limits or type(value) not in (int, float) or not math.isfinite(value):
raise ValueError("Unsupported game objective: " + str(key))
low, high = limits[key]