Added Glitch Room Source code to Gitty

This commit is contained in:
Jeff Luke committed 2026-10-04 11:31:17 -07:00
commit 1ac9eb5644
505 files changed
+28504

No files matched your search

+1
View File
@@ -0,0 +1 @@
__pycache__/
+22
View File
@@ -0,0 +1,22 @@
extends SceneTree
func _initialize() -> void:
var cabinet := CabinetProfile.new()
var tempest: GameDefinition = load("res://data/games/tempest.tres")
cabinet.capabilities.assign([&"spinner", &"button_1", &"start_button"])
assert(not tempest.can_run_on(cabinet), "Superzapper requires a second button")
cabinet.capabilities.append(&"button_2")
assert(tempest.can_run_on(cabinet))
cabinet.capabilities.erase(&"spinner")
assert(not tempest.can_run_on(cabinet))
cabinet.capabilities.append(&"trackball")
assert(tempest.can_run_on(cabinet))
var robotron: GameDefinition = load("res://data/games/robotron.tres")
cabinet.capabilities.assign([&"dual_joystick_8way", &"start_button"])
assert(robotron.can_run_on(cabinet))
cabinet.capabilities.assign([&"joystick_8way", &"button_1", &"button_2", &"button_3", &"button_4", &"start_button"])
assert(robotron.can_run_on(cabinet))
cabinet.capabilities.erase(&"button_4")
assert(not robotron.can_run_on(cabinet))
print("PASS: cabinet control alternatives and common requirements")
quit()
@@ -0,0 +1 @@
uid://c28svvvfj8ndh
+83
View File
@@ -0,0 +1,83 @@
"""Start real HTTP services with fake GPIO, then a real headless Godot cabinet."""
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import threading
import time
import json
import urllib.request
import urllib.error
from http.server import HTTPServer
from test_room import ROOT, server
sys.path.insert(0, str(ROOT / "tests/gpio"))
from test_bridge import bridge, FakePin
OP = "test-operator-token-1234"
CAB = "test-cabinet-token-5678"
def post(path, data=None, token=OP):
request = urllib.request.Request("http://127.0.0.1:8766" + path, data=json.dumps(data or {}).encode(),
headers={"Authorization": "Bearer " + token, "Content-Type": "application/json"})
with urllib.request.urlopen(request, timeout=3) as response: return json.load(response)
def wait_for(predicate):
deadline = time.monotonic() + 10
while time.monotonic() < deadline:
state = post("/operator/state")
if predicate(state): return state
time.sleep(.1)
raise AssertionError("Timed out waiting for room state: " + str(state))
with tempfile.TemporaryDirectory() as temp:
room = server.Fleet(Path(temp) / "room.sqlite3", json.loads((ROOT / "tools/room_controller/catalog.json").read_text()))
gpio = bridge.Outputs(FakePin, "cabinet_01_gpio", [17], ledger=Path(temp) / "gpio.sqlite3")
services = [HTTPServer(("127.0.0.1", 8766), server.handler_for(room, OP, {"cabinet_01": CAB, "cabinet_02": CAB + "-two"})),
HTTPServer(("127.0.0.1", 8765), bridge.handler_for(gpio))]
threads = [threading.Thread(target=s.serve_forever, daemon=True) for s in services]
for thread in threads: thread.start()
process = None
second = None
try:
try:
post("/operator/state", token=CAB)
raise AssertionError("Cabinet token accessed operator API")
except urllib.error.HTTPError as exc:
assert exc.code == 401
env = dict(os.environ, ARCADE_ROOM_URL="http://127.0.0.1:8766", ARCADE_CABINET_TOKEN=CAB)
process = subprocess.Popen([sys.argv[1], "--headless", "--path", str(ROOT),
"--log-file", str(ROOT / ".godot/managed_integration.log"),
"--script", "tests/room_network/managed_agent.gd", "--", "--gpio=real"], env=env)
second = subprocess.Popen([sys.argv[1], "--headless", "--path", str(ROOT),
"--log-file", str(ROOT / ".godot/managed_second.log"),
"--script", "tests/room_network/managed_agent.gd", "--", "--cabinet=cabinet_02", "--test-no-reward"],
env=dict(env, ARCADE_CABINET_TOKEN=CAB + "-two"))
wait_for(lambda s: s["online"] and s["cabinets"]["cabinet_02"]["online"])
state = post("/operator/command", {"action": "prepare_scenario", "scenario": "proof_of_concept"})
post("/operator/command", {"action": "prepare", "cabinet": "cabinet_02", "game": "galaga"})
assert state["phase"] == "preparing"
wait_for(lambda s: s["phase"] == "ready" and s["cabinets"]["cabinet_02"]["phase"] == "ready")
post("/operator/command", {"action": "start"})
post("/operator/command", {"action": "start", "cabinet": "cabinet_02"})
final = wait_for(lambda s: s["reward"].startswith("GPIO accepted"))
wait_for(lambda s: s["cabinets"]["cabinet_02"]["phase"] == "won")
assert gpio.pins[17]["device"].activations == 1
assert sum(c["type"] == "reward" for c in final["commands"]) == 1
assert process.wait(timeout=8) == 0
assert second.wait(timeout=8) == 0
assert not any(c["type"] == "reward" for c in post("/operator/state")["cabinets"]["cabinet_02"]["commands"])
print("PASS: two real Godot cabinets -> Pac-Man scenario + Galaga manual -> ready/start/win -> exactly one mapped GPIO acceptance")
finally:
if process and process.poll() is None:
process.terminate()
process.wait(timeout=5)
if second and second.poll() is None:
second.terminate()
second.wait(timeout=5)
for service in services:
service.shutdown()
service.server_close()
for thread in threads: thread.join()
gpio.close()
for cabinet in room.rooms.values(): cabinet.db.close()
+17
View File
@@ -0,0 +1,17 @@
extends SceneTree
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var agent = load("res://core/network/managed_cabinet.gd").new()
root.add_child(agent)
# Only the test harness forces a win; production has no remote force-win endpoint.
for i in range(140):
await create_timer(.1).timeout
if agent.phase == "playing":
agent.game.win_game()
if agent.phase == "won" and ("--test-no-reward" in OS.get_cmdline_user_args() or agent.outcomes.values().any(func(r): return str(r.detail).contains("GPIO bridge accepted"))):
await create_timer(1.0).timeout
quit()
return
push_error("Managed cabinet integration timed out")
quit(1)
+1
View File
@@ -0,0 +1 @@
uid://da2ysnuinbmh6
+145
View File
@@ -0,0 +1,145 @@
import json
import tempfile
import sqlite3
from pathlib import Path
import threading
import urllib.request
import urllib.error
from http.server import HTTPServer
from test_room import ROOT, server
import unittest
class FleetTests(unittest.TestCase):
def setUp(self):
self.now = 100
self.catalog = json.loads((ROOT / 'tools/room_controller/catalog.json').read_text())
self.fleet = server.Fleet(':memory:', self.catalog, lambda: self.now)
for cabinet in self.fleet.rooms: self.poll(cabinet)
def tearDown(self):
for room in self.fleet.rooms.values(): room.db.close()
def poll(self, cabinet, reports=None):
return self.fleet.poll(dict(cabinet=cabinet, protocol=2, boot=cabinet, reports=reports or []))
def ack(self, cabinet):
commands = self.poll(cabinet)['commands']
self.poll(cabinet, [dict(kind='ack', id=c['id'], session=c['session'], ok=True) for c in commands])
def test_scenario_routes_games_and_events_to_correct_cabinets(self):
self.fleet.operator('prepare_scenario', {'scenario': 'two_cabinet_test'})
self.assertEqual(self.poll('cabinet_01')['commands'][0]['game'], 'pacman')
self.assertEqual(self.poll('cabinet_02')['commands'][0]['game'], 'galaga')
self.assertFalse(self.poll('cabinet_03')['commands'])
self.ack('cabinet_01')
self.assertEqual(self.fleet.rooms['cabinet_02'].state['phase'], 'preparing')
self.ack('cabinet_02')
self.assertEqual(self.poll('cabinet_02')['commands'][0]['action_id'], 'glitch_code')
self.assertFalse(self.poll('cabinet_01')['commands'])
def test_offline_assignment_prevents_any_scenario_dispatch(self):
self.now += 5
self.poll('cabinet_01')
with self.assertRaisesRegex(ValueError, 'offline'):
self.fleet.operator('prepare_scenario', {'scenario': 'two_cabinet_test'})
self.assertFalse(self.fleet.rooms['cabinet_01'].state['commands'])
def test_manual_assignment_does_not_inherit_reward(self):
self.fleet.operator('prepare', {'cabinet': 'cabinet_01', 'game': 'galaga'})
self.ack('cabinet_01')
room = self.fleet.rooms['cabinet_01']
self.poll('cabinet_01', [dict(kind='event', id=event, event=event, session=room.state['session']) for event in ['started', 'won']])
self.assertEqual(room.state['phase'], 'won')
self.assertFalse(self.poll('cabinet_01')['commands'])
def test_compatibility_rejects_missing_controls_and_accepts_alternatives(self):
with self.assertRaisesRegex(ValueError, 'hardware'):
self.fleet.operator('prepare', {'cabinet': 'cabinet_01', 'game': 'missile_command'})
cabinet = self.catalog['cabinets']['cabinet_03']
self.assertFalse(self.fleet.compatible('cabinet_03', 'tempest'))
cabinet['capabilities'].append('button_2')
self.assertTrue(self.fleet.compatible('cabinet_03', 'tempest'))
cabinet['capabilities'].remove('spinner')
self.assertFalse(self.fleet.compatible('cabinet_03', 'tempest'))
cabinet['capabilities'].append('trackball')
self.assertTrue(self.fleet.compatible('cabinet_03', 'tempest'))
def test_old_export_cannot_silently_load_wrong_game(self):
with self.assertRaisesRegex(ValueError, 'protocol 2'):
self.fleet.poll(dict(cabinet='cabinet_01', protocol=1, boot='old'))
def test_invalid_mapping_does_not_dispatch(self):
self.catalog['scenarios']['two_cabinet_test']['assignments'][1]['mappings']['unknown_event'] = ['coin_release']
with self.assertRaisesRegex(ValueError, 'not implemented'):
self.fleet.operator('prepare_scenario', {'scenario': 'two_cabinet_test'})
self.assertFalse(self.poll('cabinet_01')['commands'])
def test_output_timeout_does_not_report_a_reward(self):
room = self.fleet.rooms['cabinet_01']
room.command('output')
self.now += 16
room.expire()
self.assertEqual(room.state['reward'], 'not requested')
self.assertIn('unknown', room.state['last_output'])
def test_one_accepted_output_cannot_hide_another_failed_reward(self):
room = self.fleet.rooms['cabinet_01']
first, second = room.command('reward'), room.command('reward')
room.ack(dict(id=first['id'], ok=False))
room.ack(dict(id=second['id'], ok=True))
self.assertIn('failed', room.state['reward'])
def test_migration_and_independent_persistence(self):
with tempfile.TemporaryDirectory() as temp:
path = str(Path(temp) / 'migration.db')
legacy = sqlite3.connect(path)
legacy.execute('CREATE TABLE room (id INTEGER PRIMARY KEY, value TEXT NOT NULL)')
old = dict(self.fleet.rooms['cabinet_01'].state, session='old-session', phase='won')
legacy.execute('INSERT INTO room VALUES (1,?)', (json.dumps(old),))
legacy.commit()
legacy.close()
fleet = server.Fleet(path, self.catalog)
try:
self.assertEqual(fleet.rooms['cabinet_01'].state['session'], 'old-session')
self.assertEqual(fleet.rooms['cabinet_02'].state['session'], '')
fleet.rooms['cabinet_02'].state['game'] = 'galaga'
fleet.rooms['cabinet_02'].save()
finally:
for room in fleet.rooms.values(): room.db.close()
restored = server.Fleet(path, self.catalog)
try:
self.assertEqual(restored.rooms['cabinet_01'].state['phase'], 'won')
self.assertEqual(restored.rooms['cabinet_02'].state['game'], 'galaga')
self.assertFalse(restored.rooms['cabinet_01'].online())
finally:
for room in restored.rooms.values(): room.db.close()
def test_http_browser_and_identity_bound_credentials(self):
service = HTTPServer(('127.0.0.1', 0), server.handler_for(self.fleet, 'operator-secret', {'cabinet_01': 'cabinet-one', 'cabinet_02': 'cabinet-two'}))
thread = threading.Thread(target=service.serve_forever, daemon=True)
thread.start()
base = 'http://127.0.0.1:' + str(service.server_port)
try:
with urllib.request.urlopen(base) as response:
self.assertIn(b'Room control', response.read())
def request(path, token, data, origin=base):
req = urllib.request.Request(base + path, data=json.dumps(data).encode(), headers={'Authorization': 'Bearer '+token, 'Origin': origin})
return urllib.request.urlopen(req)
with request('/operator/state', 'operator-secret', {}) as response:
self.assertEqual(len(json.load(response)['cabinets']), 3)
for path, token, data, code, origin in [
('/operator/state', 'cabinet-one', {}, 401, base),
('/cabinet/poll', 'cabinet-one', {'cabinet':'cabinet_02','protocol':2,'boot':'x'}, 401, base),
('/operator/state', 'operator-secret', {}, 400, 'http://unrelated.example'),
]:
with self.assertRaises(urllib.error.HTTPError) as caught:
request(path, token, data, origin)
self.assertEqual(caught.exception.code, code)
finally:
service.shutdown()
service.server_close()
thread.join()
if __name__ == '__main__': unittest.main()
+80
View File
@@ -0,0 +1,80 @@
import importlib.util
from pathlib import Path
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[2]
spec = importlib.util.spec_from_file_location("room_server", ROOT / "tools/room_controller/server.py")
server = importlib.util.module_from_spec(spec)
spec.loader.exec_module(server)
class RoomTests(unittest.TestCase):
def setUp(self):
self.now = 100
self.room = server.Room(":memory:", lambda: self.now)
self.poll()
def tearDown(self): self.room.db.close()
def poll(self, reports=None, boot="boot1"):
return self.room.poll(dict(cabinet="cabinet_01", protocol=1, boot=boot, reports=reports or []))
def ready(self):
self.room.operator("prepare")
command = self.poll()["commands"][0]
self.assertEqual(self.room.state["phase"], "preparing")
self.poll([dict(kind="ack", id=command["id"], session=command["session"], ok=True)])
self.assertEqual(self.room.state["phase"], "ready")
def event(self, name, event_id):
return dict(kind="event", event=name, id=event_id, session=self.room.state["session"])
def test_ready_then_win_queues_one_reward(self):
self.ready()
self.poll([self.event("started", "1"), self.event("won", "2")])
reward = self.poll()["commands"]
self.assertEqual(len(reward), 1)
self.assertEqual(reward[0]["type"], "reward")
self.poll([self.event("won", "2"), self.event("won", "3")])
self.assertEqual(len(self.poll()["commands"]), 1)
def test_stale_session_cannot_reward_after_reset(self):
self.ready()
self.poll([self.event("started", "1")])
old_win = self.event("won", "2")
self.room.operator("reset")
self.poll([old_win])
self.assertEqual(self.room.state["reward"], "not requested")
def test_offline_and_expiry(self):
self.now += 5
self.assertFalse(self.room.snapshot()["online"])
with self.assertRaises(ValueError): self.room.operator("prepare")
self.poll()
self.room.operator("prepare")
self.now += 16
self.assertEqual(self.poll()["commands"], [])
self.assertEqual(self.room.state["phase"], "needs_prepare")
def test_new_agent_boot_requires_prepare(self):
self.ready()
self.poll(boot="boot2")
self.assertEqual(self.room.state["session"], "")
self.assertEqual(self.room.state["phase"], "needs_prepare")
def test_persistence(self):
with tempfile.TemporaryDirectory() as temp:
path = Path(temp) / "room.db"
room = server.Room(path, lambda: self.now)
room.poll(dict(cabinet="cabinet_01", protocol=1, boot="a"))
room.operator("prepare")
session = room.state["session"]
room.db.close()
restored = server.Room(path, lambda: self.now)
self.assertEqual(restored.state["session"], session)
self.assertFalse(restored.online())
restored.db.close()
if __name__ == "__main__": unittest.main()