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

+43
View File
@@ -0,0 +1,43 @@
extends SceneTree
var starts := 0
var wins := 0
var losses := 0
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var g = load("res://games/frogger/Frogger.tscn").instantiate()
root.add_child(g)
g.set_physics_process(false)
g.music_enabled = false
g.game_started.connect(func(): starts += 1)
g.game_won.connect(func(): wins += 1)
g.game_lost.connect(func(): losses += 1)
g.high_score = 12345
g.score = 1230
for cycle in range(2):
g._physics_process(44)
assert(g.demo_active and not g.is_running)
var highest := 232.0
var arrivals := 0
for frame in range(1921):
g._physics_process(1.0/60)
highest = minf(highest,g.frog.y)
arrivals = maxi(arrivals,g.homes.count(true))
assert(not g.demo_active and g.phase == "scores")
assert(g.score == 1230 and g.high_score == 12345 and g.leaderboard.is_empty())
assert(starts == 0 and wins == 0 and losses == 0)
assert(highest <= 104, "Demo never reached river")
print("Demo cycle ",cycle," highest y=",highest," homes=",arrivals)
g._physics_process(7.1)
assert(g.phase == "title" and g.attract_time == 0)
for state in ["title","scores","demo"]:
g.phase = state
if state == "demo": g.begin_demo()
assert(g.start_game())
assert(not g.demo_active and g.phase == "ready" and g.score == 0)
assert(g.lives == g.starting_lives and not g.homes.has(true))
g.reset_game()
assert(starts == 3 and wins == 0 and losses == 0)
print("Attract isolation, cycling, interruption: PASS")
g.free()
quit()
+1
View File
@@ -0,0 +1 @@
uid://b2xuro2dta84x
+63
View File
@@ -0,0 +1,63 @@
extends SceneTree
var failures := 0
func check(value: bool, message: String) -> void:
if not value:
failures += 1
push_error(message)
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var g = load("res://games/frogger/Frogger.tscn").instantiate()
root.add_child(g)
g.set_physics_process(false)
g.music_enabled = false
check(g.start_game(),"Start rejected")
check(not g.start_game(),"Duplicate start accepted")
g._physics_process(1.6)
check(g.phase == "play","Ready transition")
g.lanes.clear()
check(g.hop(Vector2i.UP),"Hop rejected")
check(not g.hop(Vector2i.RIGHT),"Overlapping hop accepted")
g.step_world(.12)
check(g.frog == Vector2(112,216) and g.score == 10,"Hop distance/score")
g.make_lanes()
g.frog = Vector2(25,216)
g.step_world(0)
check(g.phase == "dead" and g.lives == 2,"Road collision")
g._physics_process(1.2)
check(g.phase == "play" and g.frog == Vector2(112,232),"Respawn")
g.frog = Vector2(50,104)
g.step_world(.01)
check(g.phase == "play" and g.frog.x > 50,"Log carry")
g.frog = Vector2(90,104)
g.step_world(0)
check(g.phase == "dead" and g.death_kind == "plunk","Water collision")
g._physics_process(1.2)
g.clock = 6
g.frog = Vector2(24,72)
g.step_world(0)
check(g.phase == "dead","Submerged turtle collision")
g._physics_process(1.2)
check(g.phase == "lost" and not g.is_running,"Lose event state")
g.start_game()
g.phase = "play"
for i in range(5):
g.frog = Vector2(g.HOME_X[i],40)
g.step_world(0)
check(g.homes[i] and g.phase == "home","Home entry")
g._physics_process(.9)
check(g.phase == "clear","Five homes transition")
g._physics_process(2.1)
check(g.phase == "won" and not g.is_running,"Win state")
g.start_game()
g.boards_to_win = 0
g.phase = "clear"
g.phase_time = 0
g._physics_process(.01)
check(g.board_number == 2 and g.phase == "ready","Endless progression")
g.reset_game()
check(g.phase == "title" and not g.is_running and not g.homes.has(true),"Reset")
print("Frogger checks: ",failures," failures")
g.queue_free()
await process_frame
quit(failures)
+1
View File
@@ -0,0 +1 @@
uid://1hmcrcrsvulb
+1
View File
@@ -0,0 +1 @@
__pycache__/
+25
View File
@@ -0,0 +1,25 @@
extends SceneTree
var activated := false
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var hardware = root.get_node("HardwareManager")
hardware.action_activated.connect(func(_cabinet, _action, _context): activated = true)
var launcher = load("res://games/launcher/ArcadeLauncher.tscn").instantiate()
root.add_child(launcher)
await process_frame
await process_frame
if is_instance_valid(launcher.setup):
launcher.setup.close()
await process_frame
launcher._select(0)
launcher.launch()
launcher.game.start_game()
launcher.game.win_game()
await create_timer(1.0).timeout
if not activated:
push_error("Pac-Man win did not reach GPIO bridge: " + hardware.status)
quit(1)
else:
print("Pac-Man win reached GPIO bridge")
quit()
+1
View File
@@ -0,0 +1 @@
uid://cohmnr1oklto5
+28
View File
@@ -0,0 +1,28 @@
"""Run with the Godot console executable as the first argument. No physical GPIO."""
import subprocess
import sys
import threading
from pathlib import Path
from http.server import HTTPServer
from test_bridge import FakePin, bridge
outputs = bridge.Outputs(FakePin, "cabinet_01_gpio", [17])
server = HTTPServer(("127.0.0.1", 8765), bridge.handler_for(outputs))
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
root = Path(__file__).resolve().parents[2]
result = subprocess.run([
sys.argv[1], "--headless", "--path", str(root),
"--log-file", str(root / ".godot/gpio_integration.log"),
"--script", "tests/gpio/check_gpio.gd", "--", "--gpio=real",
], timeout=15)
assert result.returncode == 0, "Godot integration failed"
assert 17 in outputs.pins, "Bridge never received BCM17"
assert outputs.pins[17]["polarity"] is True, "Expected active-high output"
print("Godot -> HTTP -> GPIO backend integration passed (fake pin)")
finally:
server.shutdown()
thread.join()
outputs.close()
server.server_close()
+105
View File
@@ -0,0 +1,105 @@
import importlib.util
from pathlib import Path
import unittest
import tempfile
source = Path(__file__).resolve().parents[2] / "tools/pi_gpio/pi_gpio_bridge.py"
spec = importlib.util.spec_from_file_location("bridge", source)
bridge = importlib.util.module_from_spec(spec)
spec.loader.exec_module(bridge)
class FakePin:
def __init__(self, pin, active_high, initial_value):
self.pin, self.active_high, self.active = pin, active_high, initial_value
self.closed = False
self.activations = 0
def on(self):
self.active = True
self.activations += 1
def off(self): self.active = False
def close(self): self.closed = True
class BridgeTests(unittest.TestCase):
def setUp(self):
self.now = 0
self.outputs = bridge.Outputs(FakePin, "cabinet_01_gpio", [17], lambda: self.now)
self.request = dict(controller="cabinet_01_gpio", pin=17, mode="pulse", active_high=True, duration_ms=600)
def test_pulse_and_retrigger(self):
self.outputs.command(self.request)
pin = self.outputs.pins[17]["device"]
self.assertTrue(pin.active)
self.now = .4
self.outputs.command(self.request)
self.now = .7
self.outputs.tick()
self.assertTrue(pin.active)
self.now = 1.01
self.outputs.tick()
self.assertFalse(pin.active)
def test_active_low_and_shutdown(self):
self.outputs.command(dict(self.request, active_high=False))
pin = self.outputs.pins[17]["device"]
self.assertFalse(pin.active_high)
self.assertTrue(pin.active)
self.outputs.close()
self.assertFalse(pin.active)
self.assertTrue(pin.closed)
def test_digital_off_and_watchdog(self):
self.outputs.command(dict(self.request, mode="set"))
self.now = 2
self.outputs.heartbeat()
self.now = 4
self.outputs.tick()
self.assertTrue(self.outputs.pins[17]["device"].active)
self.now = 5.1
self.outputs.tick()
self.assertFalse(self.outputs.pins[17]["device"].active)
self.outputs.command(dict(self.request, mode="set"))
self.outputs.command(dict(self.request, mode="set", active=False))
self.assertFalse(self.outputs.pins[17]["device"].active)
def test_reject_wrong_controller_pin_and_duration(self):
for changes in [dict(controller="cabinet_02_gpio"), dict(pin=18), dict(duration_ms=0), dict(duration_ms=10001), dict(active_high="false")]:
with self.assertRaises(ValueError):
self.outputs.command(dict(self.request, **changes))
self.assertFalse(self.outputs.pins)
def test_polarity_cannot_change_while_owned(self):
self.outputs.command(self.request)
with self.assertRaises(ValueError):
self.outputs.command(dict(self.request, active_high=False))
def test_reward_dedup_survives_restart(self):
with tempfile.TemporaryDirectory() as temp:
path = Path(temp) / "requests.sqlite3"
outputs = bridge.Outputs(FakePin, "cabinet_01_gpio", [17], ledger=path)
request = dict(self.request, request_id="reward-1")
self.assertTrue(outputs.command(request)["ok"])
self.assertTrue(outputs.command(request)["ok"])
self.assertEqual(outputs.pins[17]["device"].activations, 1)
outputs.close()
outputs = bridge.Outputs(FakePin, "cabinet_01_gpio", [17], ledger=path)
self.assertTrue(outputs.command(request)["ok"])
self.assertFalse(outputs.pins, "Duplicate must not even reopen output")
with self.assertRaises(ValueError):
outputs.command(dict(request, duration_ms=900))
outputs.close()
def test_incomplete_reward_is_not_replayed(self):
request = dict(self.request, request_id="uncertain")
import hashlib, json
digest = hashlib.sha256(json.dumps(request, sort_keys=True).encode()).hexdigest()
with self.outputs.ledger:
self.outputs.ledger.execute("INSERT INTO requests VALUES (?, ?, NULL)", ("uncertain", digest))
self.assertFalse(self.outputs.command(request)["ok"])
self.assertFalse(self.outputs.pins)
if __name__ == "__main__":
unittest.main()
+81
View File
@@ -0,0 +1,81 @@
extends SceneTree
var failures := 0
func _initialize() -> void:
call_deferred("run")
func check(ok: bool, label: String) -> void:
if not ok:
failures += 1
push_error(label)
func run() -> void:
var controls = root.get_node("ArcadeControls")
controls.path = "res://.godot/test_controls.cfg"
var pads := [{"id": 2, "guid": "one", "name": "Encoder A"}, {"id": 5, "guid": "two", "name": "Encoder B"}]
var press := InputEventJoypadButton.new()
press.device = 2
press.button_index = 3
press.pressed = true
var binding: Dictionary = controls.encode(press, pads)
var decoded: InputEvent = controls.decode(binding, pads)
check(decoded.device == 2, "Binding uses a specific device")
InputMap.action_erase_events("arcade_action_1")
InputMap.action_add_event("arcade_action_1", decoded)
check(InputMap.event_is_action(press, "arcade_action_1"), "Player one matches")
press.device = 5
check(not InputMap.event_is_action(press, "arcade_action_1"), "Player two does not match the same button")
var reordered := [{"id": 0, "guid": "two", "name": "Encoder B"}, {"id": 4, "guid": "one", "name": "Encoder A"}]
check(controls.decode(binding, reordered).device == 4, "Identity survives changed device index")
check(controls.decode(binding, []) == null, "Missing device never falls back to all devices")
var twins := [{"id": 0, "guid": "same", "name": "USB"}, {"id": 1, "guid": "same", "name": "USB"}]
press.device = 1
var twin: Dictionary = controls.encode(press, twins)
check(controls.decode(twin, twins).device == 1, "Identical encoders isolated by slot")
check(controls.decode(twin, [twins[1]]) == null, "Unplugged twin does not reassign inputs")
var draft := {}
for i in range(controls.STEPS.size()):
draft[controls.STEPS[i][0]] = {"kind": "key", "code": KEY_A + i, "logical": 0}
check(controls.save_profile(draft) == OK, "Save profile")
controls.bindings.clear()
check(controls.load_profile(), "Reload profile")
check(controls.bindings == draft, "Bindings survive reload")
check(InputMap.action_get_events("arcade_up")[0].physical_keycode == KEY_C, "Saved override applied")
check(InputMap.action_get_events("robotron_move_up")[0].physical_keycode == KEY_C, "Robotron reuses player one movement")
draft.arcade_start.code = KEY_P
check(controls.save_profile(draft) == OK and controls.load_profile(), "Replace existing profile")
check(controls.bindings.arcade_start.code == KEY_P, "Replacement persists")
var before: Dictionary = controls.bindings.duplicate(true)
controls.path = "res://.godot/missing_directory/controls.cfg"
check(controls.save_profile(draft) != OK and controls.bindings == before, "Failed save leaves active bindings intact")
check(not controls.valid({"arcade_start": {"kind": "axis"}}), "Incomplete profile rejected")
var setup = load("res://games/launcher/control_setup.gd").new()
root.add_child(setup)
setup.begin()
setup.armed_at = 0
var key := InputEventKey.new()
key.physical_keycode = KEY_ENTER
key.pressed = true
setup._input(key)
check(setup.step == 0 and setup.waiting != null, "Capture waits for release")
setup._input(key)
check(setup.step == 0, "Held input cannot fill next step")
key.pressed = false
setup._input(key)
check(setup.step == 1 and setup.waiting == null, "Release advances wizard")
check(not setup.conflict("arcade_menu", setup.draft.arcade_start).is_empty(), "Duplicate Start/Select rejected")
setup.draft.arcade_action_1 = {"kind": "key", "code": KEY_SPACE, "logical": 0}
check(setup.conflict("robotron_fire_up", setup.draft.arcade_action_1).is_empty(), "Game fire button can be reused for Robotron")
setup.begin()
for i in range(controls.STEPS.size()):
setup.armed_at = 0
key.physical_keycode = KEY_A + i
key.pressed = true
setup._input(key)
key.pressed = false
setup._input(key)
check(setup.step == controls.STEPS.size() and setup.draft.size() == 13, "Complete wizard reaches review")
check(controls.bindings == before, "Draft does not change live bindings")
controls.path = "res://.godot/test_controls.cfg"
setup.save()
await process_frame
check(controls.load_profile() and controls.bindings.arcade_start.code == KEY_A, "Wizard save survives restart")
print("Control setup checks: %d failures" % failures)
quit(1 if failures else 0)
+1
View File
@@ -0,0 +1 @@
uid://swmu6kexenhl
+61
View File
@@ -0,0 +1,61 @@
extends SceneTree
var failures := 0
func _initialize() -> void:
call_deferred("run")
func check(ok: bool, label: String) -> void:
if not ok:
failures += 1
push_error(label)
func run() -> void:
var launcher = load("res://games/launcher/ArcadeLauncher.tscn").instantiate()
root.add_child(launcher)
await process_frame
if is_instance_valid(launcher.setup):
launcher.setup.close()
await process_frame
check(launcher.list.item_count == 8, "Seven games and control setup listed")
launcher._select(-1)
check(launcher.selected == 7, "Menu wraps to setup")
for i in range(7):
launcher._select(i)
launcher.launch()
check(launcher.game is ArcadeGame, "Game loads: %s" % i)
check(not launcher.menu.visible, "Menu hides")
check(launcher.game.start_game(), "Game starts: %s" % i)
await process_frame
launcher.return_to_menu()
await process_frame
check(launcher.game == null and launcher.menu.visible, "Game unloads: %s" % i)
# Rebind a battery to a different key, then verify the former key no longer fires.
InputMap.action_erase_events("arcade_action_2")
var key := InputEventKey.new()
key.physical_keycode = KEY_F9
InputMap.action_add_event("arcade_action_2", key)
launcher._select(3)
launcher.launch()
var missile = launcher.game
missile.start_game()
missile.phase = "play"
var old := InputEventKey.new()
old.physical_keycode = KEY_S
old.pressed = true
missile._unhandled_input(old)
check(missile.ammo[1] == 10, "Old hard-coded key does not fire")
key.pressed = true
missile._unhandled_input(key)
check(missile.ammo[1] == 9, "Remapped key fires middle battery")
launcher.return_to_menu()
await process_frame
launcher._select(6)
launcher.launch()
Input.action_press("robotron_fire_up")
Input.action_press("robotron_fire_right")
var direction: Vector2 = launcher.game.control_vector("fire_")
check(direction.x > 0.6 and direction.y < -0.6, "Two fire buttons produce diagonal")
Input.action_release("robotron_fire_up")
Input.action_release("robotron_fire_right")
launcher.return_to_menu()
launcher.queue_free()
await process_frame
print("Launcher/input checks: %d failures" % failures)
quit(1 if failures else 0)
+1
View File
@@ -0,0 +1 @@
uid://bb1cbfxyd5pif
+3
View File
@@ -0,0 +1,3 @@
*.log
*.png
*.png.import
+30
View File
@@ -0,0 +1,30 @@
extends SceneTree
func _initialize() -> void:
call_deferred("run")
func run() -> void:
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
root.size=Vector2i(960,720)
root.content_scale_size=Vector2i(960,720)
var g=load("res://games/robotron/Robotron.tscn").instantiate()
root.add_child(g)
g.set_physics_process(false)
g.sound_enabled=false
g.demo=true
g.wave=20
g.setup_wave()
g.phase="play"
for i in range(30): await process_frame
var started := Time.get_ticks_usec()
var peak := 0
for i in range(360):
g.invulnerable=10
g.step_world(1.0/60,Vector2.ZERO,Vector2.from_angle(i*.04))
g.clock+=1.0/60
g.board.queue_redraw()
peak=maxi(peak,g.entities.size())
await process_frame
await RenderingServer.frame_post_draw
var elapsed := (Time.get_ticks_usec()-started)/1000000.0
print("360 dense-wave frames; peak entities ",peak,"; ",snappedf(elapsed,.001)," seconds; ",snappedf(360/elapsed,1)," FPS desktop uncapped")
g.free()
quit()
+1
View File
@@ -0,0 +1 @@
uid://gptkt4d8d3sy
+141
View File
@@ -0,0 +1,141 @@
extends SceneTree
var failures := 0
var started := 0
var won := 0
var lost := 0
func check(value: bool, message: String) -> void:
if not value:
failures += 1
push_error(message)
func _initialize() -> void:
call_deferred("run")
func clean(g) -> void:
g.entities.clear()
g.shots.clear()
g.enemy_shots.clear()
g.effects.clear()
g.phase="play"
g.fire_delay=0
g.invulnerable=10
g.player=Vector2(146,124)
func run() -> void:
var g = load("res://games/robotron/Robotron.tscn").instantiate()
root.add_child(g)
g.set_physics_process(false)
g.sound_enabled=false
g.game_started.connect(func(): started+=1)
g.game_won.connect(func(): won+=1)
g.game_lost.connect(func(): lost+=1)
check(g.start_game() and not g.start_game(),"Start guard")
g._physics_process(2.1)
check(g.phase=="play" and started==1,"Ready event")
clean(g)
g.spawn("grunt",Vector2(260,210))
g.step_world(.05,Vector2.LEFT,Vector2.UP)
check(g.player.x<146 and g.shots.size()==1 and g.shots[0].velocity.y<0,"Independent movement/fire")
check(is_equal_approx(g.eight_way(Vector2(1,1)).length(),1),"Normalized diagonal")
Input.action_press("robotron_move_left")
Input.action_press("robotron_move_up")
Input.action_press("robotron_fire_right")
check(g.control_vector("move_").x<0 and g.control_vector("move_").y<0 and g.control_vector("fire_").x>0,"Independent mapped sticks")
Input.action_release("robotron_move_left")
Input.action_release("robotron_move_up")
Input.action_release("robotron_fire_right")
check(g.control_vector("fire_")==Vector2.ZERO,"Release stops fire")
clean(g)
var human = g.spawn("human",Vector2(146,124))
for i in range(6):
human.alive=true
g.save_human(human)
check(g.score==20000 and g.rescue_chain==5,"Escalating rescue chain")
var old_lives: int=g.lives
g.add_score(55000)
check(g.lives==old_lives+3,"Multiple bonus thresholds")
clean(g)
var hulk = g.spawn("hulk",Vector2(160,124))
g.fire(Vector2.RIGHT)
g.update_shots(.1)
check(hulk.alive and hulk.position.x>160 and g.shots.is_empty(),"Hulk survives and is pushed")
clean(g)
human=g.spawn("human",Vector2(160,124))
g.fire(Vector2.RIGHT)
g.update_shots(.1)
check(human.alive,"Player bullets spare humans")
clean(g)
var obstacle=g.spawn("electrode",Vector2(160,124))
g.fire(Vector2.RIGHT)
g.update_shots(.1)
check(not obstacle.alive,"Electrode shot destruction")
clean(g)
g.spawn("hulk",Vector2(200,100))
var doomed=g.spawn("human",Vector2(201,100))
g.spawn("grunt",Vector2(270,210))
g.step_world(.01,Vector2.ZERO,Vector2.ZERO)
check(not doomed.alive,"Hulk kills human")
clean(g)
var brain=g.spawn("brain",Vector2(170,124))
var victim=g.spawn("human",Vector2(171,124))
g.step_world(.01,Vector2.ZERO,Vector2.ZERO)
check(not victim.alive and g.entities.any(func(e): return e.kind=="converting"),"Brain captures human")
g.step_world(.8,Vector2.ZERO,Vector2.ZERO)
check(g.entities.any(func(e): return e.kind=="prog"),"Human transforms into Prog")
g.damage(brain)
g.damage(brain)
check(brain.alive,"Brain takes three hits")
g.damage(brain)
check(not brain.alive,"Brain third hit")
clean(g)
var spawner=g.spawn("spheroid",Vector2(230,100))
spawner.timer=0
g.step_world(.01,Vector2.ZERO,Vector2.ZERO)
check(g.entities.any(func(e): return e.kind=="enforcer"),"Spheroid creates enforcers")
clean(g)
spawner=g.spawn("quark",Vector2(230,100))
spawner.timer=0
g.step_world(.01,Vector2.ZERO,Vector2.ZERO)
check(g.entities.any(func(e): return e.kind=="tank"),"Quark creates tanks")
g.projectile("shell",Vector2(282,100),Vector2(300,100))
g.update_enemy_shots(.1)
check(g.enemy_shots[0].velocity.x<0 and g.enemy_shots[0].bounces==1,"Tank shell bounce")
g.wave=5
g.setup_wave()
check(g.entities.any(func(e): return e.kind=="brain"),"Brain wave")
g.wave=7
g.setup_wave()
check(g.entities.any(func(e): return e.kind=="quark"),"Tank wave")
clean(g)
g.wave=1
g.spawn("hulk",Vector2(250,100))
g.spawn("human",Vector2(50,50))
g.spawn("electrode",Vector2(20,200))
g.step_world(.01,Vector2.ZERO,Vector2.ZERO)
check(g.phase=="transition","Only killable enemies gate wave")
g._physics_process(2)
check(won==1 and not g.is_running,"Room win")
g.start_game()
g.waves_to_win=0
g.phase="transition"
g.phase_time=0
g._physics_process(.01)
check(g.wave==2 and g.phase=="ready" and g.is_running,"Endless wave progression")
g.reset_game()
g.start_game()
g.phase="play"
g.lives=1
g.die()
g._physics_process(1.3)
check(lost==1 and not g.is_running,"Room loss")
g.reset_game()
g.high_score=999999
g.last_score=420
var events := Vector3i(started,won,lost)
g.begin_demo()
for i in range(1600): g._physics_process(1.0/60)
check(not g.demo and not g.is_running and g.score==420 and g.high_score==999999,"Demo score isolation")
check(events==Vector3i(started,won,lost),"Demo event isolation")
g.begin_demo()
check(g.start_game() and not g.demo and g.score==0,"Start interrupts demo")
check(load("res://data/project_database.tres").get_game(&"robotron")!=null,"Database")
print("Robotron checks: ",failures," failures")
g.free()
quit(failures)
+1
View File
@@ -0,0 +1 @@
uid://by2ur06o802rn
+29
View File
@@ -0,0 +1,29 @@
extends SceneTree
func _initialize() -> void:
call_deferred("run")
func run() -> void:
root.size=Vector2i(960,720)
root.content_scale_size=Vector2i(960,720)
var g=load("res://games/robotron/Robotron.tscn").instantiate()
root.add_child(g)
g.set_physics_process(false)
g.sound_enabled=false
for state in ["title","play","brain","roster"]:
g.phase_time=3
g.phase=state
if state=="play":
g.wave=3
g.setup_wave()
g.invulnerable=0
if state=="brain":
g.wave=5
g.setup_wave()
g.phase="play"
g.invulnerable=0
g.board.queue_redraw()
await process_frame
await process_frame
await RenderingServer.frame_post_draw
root.get_texture().get_image().save_png("res://tests/robotron/"+state+".png")
g.free()
quit()
+1
View File
@@ -0,0 +1 @@
uid://b8t02quve6wu1
+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()
+3
View File
@@ -0,0 +1,3 @@
*.log
*.png
*.png.import
+22
View File
@@ -0,0 +1,22 @@
extends SceneTree
func _initialize() -> void:
call_deferred("run")
func run() -> void:
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
root.size=Vector2i(672,768)
root.content_scale_size=Vector2i(672,768)
var g = load("res://games/space_invaders/SpaceInvaders.tscn").instantiate()
root.add_child(g)
g.sound_enabled=false
g.set_physics_process(false)
g.begin_demo()
for i in range(30): await process_frame
var begin := Time.get_ticks_usec()
for i in range(240):
g._physics_process(1.0/60)
await process_frame
await RenderingServer.frame_post_draw
var seconds := (Time.get_ticks_usec()-begin)/1000000.0
print("240 rendered frames: ",snappedf(seconds,.001)," seconds; average ",snappedf(240/seconds,1)," FPS (desktop, uncapped)")
g.free()
quit()
+1
View File
@@ -0,0 +1 @@
uid://dtqlkdhpcr15e
@@ -0,0 +1,122 @@
extends SceneTree
var failures := 0
var starts := 0
var wins := 0
var losses := 0
func check(value: bool, message: String) -> void:
if not value:
failures += 1
push_error(message)
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var g = load("res://games/space_invaders/SpaceInvaders.tscn").instantiate()
root.add_child(g)
g.set_physics_process(false)
g.sound_enabled = false
g.game_started.connect(func(): starts += 1)
g.game_won.connect(func(): wins += 1)
g.game_lost.connect(func(): losses += 1)
check(g.aliens.size()==55 and g.shields.size()==4,"Initial fleet/shields")
check(g.start_game() and not g.start_game(),"Start guard")
g._physics_process(2.1)
check(g.phase=="play" and starts==1,"Ready and event")
check(g.fire() and not g.fire(),"One player shot")
g.remove_shot()
check(g.shot_count==1,"Shot sequence")
var a: Vector2 = g.aliens[0].position
var b: Vector2 = g.aliens[1].position
g.march()
check(g.aliens[0].position==a+Vector2(2,0) and g.aliens[1].position==b,"Ripple movement")
for alien in g.aliens: alien.alive=false
g.aliens[0].alive=true
g.aliens[0].position=Vector2(208,100)
g.fleet_cursor=0
g.march()
check(g.fleet_direction==-1 and g.aliens[0].position.y==108,"Edge descent")
var shield: Dictionary = g.shields[0]
var point := Vector2(shield.position)+Vector2(12,4)
check(g.shield_hit(point,true),"Shield collision")
check(shield.image.get_pixel(12,4).a==0,"Destructible shield pixel")
check(not g.shield_hit(point,true),"Hole allows shot through")
g.new_wave()
for alien in g.aliens: alien.alive=false
g.aliens[0].alive=true
g.aliens[0].position=Vector2(100,100)
g.hit_pause=1
g.shot_active=true
g.player_shot=Vector2(100,110)
g.step_world(.05)
check(not g.aliens[0].alive and g.score==10 and g.phase=="clear","Alien hit/clear")
g._physics_process(1.6)
check(wins==1 and not g.is_running,"Win event")
g.start_game()
g.waves_to_win=0
g.phase="clear"
g.phase_time=0
g._physics_process(.01)
check(g.wave==2 and g.aliens[44].position.y==96,"Next wave height")
g.phase="play"
g.lives=1
g.die()
g._physics_process(1.6)
check(losses==1 and g.phase=="lost","Loss event")
g.start_game()
g.add_score(1500)
var lives: int = g.lives
g.add_score(2000)
check(lives==g.starting_lives+1 and g.lives==lives,"One bonus life")
g.reset_game()
g.high_score=9999
g.last_score=420
var original_starts := starts
var original_wins := wins
var original_losses := losses
g.begin_demo()
for i in range(1600): g._physics_process(1.0/60)
check(not g.demo and not g.is_running and g.phase=="attract","Demo exit")
check(g.high_score==9999 and g.score==420,"Demo score isolation")
check(starts==original_starts and wins==original_wins and losses==original_losses,"Demo event isolation")
g.begin_demo()
check(g.start_game() and not g.demo and g.score==0,"Start interrupts demo")
g.reset_game()
check(g.phase=="attract" and not g.is_running,"Reset")
g.start_game()
g.phase="play"
g.hit_pause=1
g.saucer_active=true
g.saucer_x=100
g.shot_count=8
g.shot_active=true
g.player_shot=Vector2(100,50)
g.step_world(.025)
check(g.score==300 and not g.saucer_active and not g.shot_active,"Saucer score sequence and collision")
g.hit_pause=1
g.shot_delay=0
g.fire()
g.player_shot=Vector2(16,180)
g.bombs.clear()
g.bombs.append({"position":Vector2(16,174),"kind":0})
g.step_world(.01)
check(not g.shot_active and g.bombs.is_empty(),"Projectile interception")
g.bombs.append({"position":Vector2(g.player_x,214),"kind":0})
g.step_world(.025)
check(g.phase=="dead","Enemy projectile hits player")
g._physics_process(1.6)
g._physics_process(1.1)
check(g.phase=="play" and g.bombs.is_empty(),"Respawn clears threats")
for alien in g.aliens: alien.alive=false
g.aliens[0].alive=true
g.aliens[0].position=Vector2(100,216)
g.fleet_cursor=0
g.march()
check(g.phase=="lost" and not g.is_running,"Invasion is an immediate loss")
g._physics_process(5.1)
check(g.phase=="attract","Game over returns to attract")
var definition = load("res://data/games/space_invaders.tres")
check(definition.game_id==&"space_invaders" and definition.scene!=null,"Game resource")
var database = load("res://data/project_database.tres")
check(database.get_game(&"space_invaders")!=null,"Database registration")
print("Space Invaders checks: ",failures," failures")
g.free()
quit(failures)
@@ -0,0 +1 @@
uid://chgwbac0t6h7u
+26
View File
@@ -0,0 +1,26 @@
extends SceneTree
func _initialize() -> void:
call_deferred("run")
func run() -> void:
root.size = Vector2i(672,768)
root.content_scale_size = Vector2i(672,768)
var g = load("res://games/space_invaders/SpaceInvaders.tscn").instantiate()
root.add_child(g)
g.set_physics_process(false)
g.sound_enabled = false
for state in ["attract","play","demo"]:
g.phase=state
g.phase_time=8
if state=="play":
g.start_game()
g.phase="play"
if state=="demo":
g.begin_demo()
for i in range(720): g._physics_process(1.0/60)
g.board.queue_redraw()
await process_frame
await process_frame
await RenderingServer.frame_post_draw
root.get_texture().get_image().save_png("res://tests/space_invaders/"+state+".png")
g.free()
quit()
@@ -0,0 +1 @@
uid://0olmyxb3i7gq
+67
View File
@@ -0,0 +1,67 @@
extends SceneTree
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var game = load("res://games/galaga/Galaga.tscn").instantiate()
root.add_child(game)
game.set_physics_process(false)
game.start_game()
assert(game.state=="intro" and not game.player.visible)
game.field._process(0)
assert(not game.player.visible)
game._tick(game.intro_seconds+0.01)
assert(game.state=="stage_card" and not game.player.visible)
game._tick(game.stage_card_seconds+0.01)
assert(game.state=="ready" and game.player.visible)
game._tick(game.player_card_seconds+0.01)
assert(game.state=="playing")
# Different formation slots must share the same pre-docking entrance curve.
game.enemies[0].time=0.4
game.enemies[1].time=0.4
game._update_enemies(0)
assert(game.enemies[0].node.position.is_equal_approx(game.enemies[1].node.position))
for enemy in game.enemies: assert(enemy.node.scale==Vector2.ONE)
var boss: Dictionary = game.enemies[0]
boss.mode="beam"
boss.time=0
boss.node.position=Vector2(112,172)
game.beam=boss
game._update_enemies(0.01)
assert(game.state=="capturing")
game._sound("fire")
for voice in game.voices:
if voice.playing: assert(voice.stream==game.streams["capturing"])
game._tick(1.6)
assert(game.lives==2 and game.captive.visible and not game.player.visible)
for i in range(400):
if game.state=="playing": break
game._tick(1.0/60.0)
assert(game.state=="playing" and game.player.visible)
boss.node.position=Vector2(112,100)
game.captive.position=Vector2(112,84)
boss.hp=1
game._bullet(Vector2(112,100),Vector2.ZERO,true)
game._update_bullets(0)
assert(game.state=="rescue_spin" and game.captor.is_empty())
var frozen: Vector2=game.enemies[0].node.position
game._tick(0.1)
assert(game.enemies[0].node.position==frozen)
for i in range(600):
if game.state=="playing": break
game._tick(1.0/60.0)
assert(game.dual and not game.captive.visible)
game._tick(0.01)
game._clear_bullets()
game.cooldown=0
game._fire()
assert(game.bullets.size()==2)
assert(absf(game.bullets[0].node.position.x-game.bullets[1].node.position.x)==16)
game._die()
assert(not game.dual and game.lives==2 and game.state=="playing")
game.reset_game()
assert(game.captor.is_empty() and not game.captive.visible and not game.dual)
print("CAPTURE AND INTRO TESTS PASS")
game.queue_free()
await process_frame
quit()
+1
View File
@@ -0,0 +1 @@
uid://bly0tpogxcin0
+79
View File
@@ -0,0 +1,79 @@
extends SceneTree
var game
func _initialize() -> void:
call_deferred("run")
func run() -> void:
game = load("res://games/galaga/Galaga.tscn").instantiate()
root.add_child(game)
game.set_physics_process(false)
assert(game.enemies.size()==40)
assert(game.start_game())
game.field.previous_state="idle"
game.field._process(0.0)
for enemy in game.enemies: assert(not enemy.node.visible)
game._update_enemies(0.01)
assert(game.enemies[0].node.visible)
assert(not game.enemies[1].node.visible)
assert(not game.start_game())
game._tick(game.intro_seconds+0.1)
game._tick(game.stage_card_seconds+0.1)
game._tick(game.player_card_seconds+0.1)
assert(game.state=="playing")
game._fire()
game.cooldown=0
game._fire()
game.cooldown=0
game._fire()
assert(game.bullets.size()==2 and game.shots_fired==2)
game._clear_bullets()
var boss: Dictionary = game.enemies[0]
boss.node.position=Vector2(100,100)
game._bullet(Vector2(100,100),Vector2.ZERO,true)
game._update_bullets(0)
assert(boss.hp==1 and game.score==0)
game._bullet(Vector2(100,100),Vector2.ZERO,true)
game._update_bullets(0)
assert(game.enemies.size()==39 and game.score==150)
game._die()
assert(game.lives==2 and game.state=="death")
game._tick(1.6)
assert(game.state=="playing" and game.invulnerable>0)
game.run_config={"score_to_win":200}
game._add_score(50)
assert(game.state=="victory")
game._tick(1.51)
assert(game.state=="won" and not game.is_running)
game.run_config={}
game.start_game()
game._add_score(140000)
assert(game.lives==6 and game.next_extra_life==210000)
game.reset_game()
assert(game.enemies.size()==40 and game.bullets.is_empty())
game.run_config={"stages_to_win":0}
game.start_game()
game._tick(game.intro_seconds+0.1)
game._tick(game.stage_card_seconds+0.1)
game._tick(game.player_card_seconds+0.1)
game.invulnerable=100
for i in range(1200): game._tick(1.0/60.0)
for enemy in game.enemies:
assert(is_finite(enemy.node.position.x))
assert(enemy.node.rotation==0)
game.state="playing"
game._clear_actors()
game._tick(0.01)
assert(game.state=="clear")
game._tick(2.1)
assert(game.stage==2 and game.enemies.size()==40)
game.state="playing"
game.lives=1
game._die()
game._tick(1.6)
assert(game.state=="lost" and not game.is_running)
game.queue_free()
await process_frame
await create_timer(0.1).timeout
print("GALAGA TESTS PASS")
quit()
+1
View File
@@ -0,0 +1 @@
uid://clahhchgclvpb
+104
View File
@@ -0,0 +1,104 @@
extends SceneTree
var failures := 0
var wins := 0
var losses := 0
func check(value: bool,message: String) -> void:
if not value:
push_error(message)
failures += 1
func _initialize() -> void: call_deferred("run")
func run() -> void:
var definition = load("res://data/games/missile_command.tres")
check(definition != null and definition.game_id == &"missile_command","game definition loads")
var game = load("res://games/missile_command/MissileCommand.tscn").instantiate()
root.add_child(game)
game.set_physics_process(false)
game.game_won.connect(func(): wins += 1)
game.game_lost.connect(func(): losses += 1)
game._physics_process(9.1)
check(game.phase == "leaderboard","title destruction leads to leaderboard")
game._physics_process(7.1)
check(game.phase == "title","attract loops")
check(game.start_game() and not game.start_game(),"single start")
check(game.phase == "ready" and not game.fire(1),"DEFEND CITIES before combat")
game._physics_process(2.3)
game.set_aim(Vector2(100,100))
check(game.fire(0) and game.fire(1) and game.fire(2),"three independent batteries")
check(game.ammo == [9,9,9] and not game.fire(0),"ammo and cooldown")
check(game.interceptors[1].speed > game.interceptors[0].speed,"center battery faster")
game.cooldown.fill(0)
var had_action := InputMap.has_action("arcade_action_1")
if not had_action: InputMap.add_action("arcade_action_1")
var space := InputEventKey.new()
space.physical_keycode = KEY_SPACE
InputMap.action_add_event("arcade_action_1",space)
space.pressed = true
var count: int = game.interceptors.size()
game._unhandled_input(space)
check(game.interceptors.size() == count+1,"mapped Space fires only one battery")
InputMap.action_erase_event("arcade_action_1",space)
if not had_action: InputMap.erase_action("arcade_action_1")
game.display.destination = Rect2(50,20,512,462)
check(game.display.to_logical(Vector2(306,251)) == Vector2(128,115.5),"letterboxed mouse coordinates")
game.incoming.clear()
game.remaining = 0
game.interceptors.clear()
game.explosion(Vector2(100,100))
game.spawn_missile(Vector2(100,100))
game.incoming[0].target = Vector2(100,211)
game._step(.1)
check(game.incoming.is_empty() and game.score == 25,"explosion destroys incoming and scores")
game.blasts.clear()
game._impact(Vector2(game.CITY_X[0],211))
check(not game.cities[0],"city impact")
game._impact(Vector2(game.BASE_X[1],204))
check(not game.bases[1] and game.ammo[1] == 0 and not game.fire(1),"battery impact disables firing")
game.add_score(game.bonus_city_interval)
check(game.bonus_bank == 1,"bonus city banked")
game._restore_cities()
check(game.cities[0] and game.bonus_bank == 0,"lost city restored")
game.blasts.clear()
game._step(.1)
check(game.phase == "bonus" and game.wave_bonus > 0,"wave completion awards bonus once")
game._physics_process(3.1)
check(game.wave == 2 and game.ammo == [10,10,10] and game.bases == [true,true,true],"new wave restores batteries")
game.wave = game.waves_to_win
game.phase = "bonus"
game.phase_time = .1
game._physics_process(.2)
game._physics_process(.2)
check(game.phase == "won" and wins == 1 and not game.is_running,"single room win")
game.reset_game()
game.start_game()
game.phase = "play"
game.cities.fill(false)
game._step(.01)
check(game.phase == "lost" and losses == 1 and not game.is_running,"all cities lost")
game._physics_process(5.1)
check(game.phase == "title","loss returns attract")
game.set_aim(Vector2(-10,999))
check(game.aim == Vector2(1,199),"aim clamp")
game.add_trackball_motion(Vector2(10,-10))
check(game.aim == Vector2(11,189),"relative trackball API")
game.reset_game()
game.start_game()
game.phase = "play"
game.wave = 5
game.spawn_time = 999
game.remaining = 1
game.spawn_missile(Vector2(128,91),"splitter")
game._step(.01)
check(game.incoming.size() == 3,"missile splits")
for frame in range(3600):
if game.phase == "play":
game.set_aim(Vector2(20+(frame%210),100))
if frame%9 == 0: game.fire((frame/9)%3)
game._physics_process(1.0/60)
print("Missile Command tests: ",failures," failures")
game.get_node("Audio").stop()
await create_timer(.2).timeout
game.queue_free()
await process_frame
quit(failures)
+1
View File
@@ -0,0 +1 @@
uid://c2ikw4y71sl30
+180
View File
@@ -0,0 +1,180 @@
extends SceneTree
const Model = preload("res://games/pacman/pacman_model.gd")
const Maze = preload("res://games/pacman/pacman_maze.gd")
const Rules = preload("res://games/pacman/pacman_rules.gd")
var checks := 0
var failures := 0
func check(condition: bool, label: String) -> void:
checks += 1
if not condition:
failures += 1
push_error("FAIL: " + label)
func _initialize() -> void:
call_deferred("run")
func run() -> void:
for row in Maze.ROWS: check(row.length() == 28, "28-column maze")
check(Maze.pellets().size() == 244, "244 pellets")
var powers := 0
for value in Maze.pellets().values():
if value: powers += 1
check(powers == 4, "four energizers")
var seen := {Maze.PLAYER_START: true}
var queue := [Maze.PLAYER_START]
while not queue.is_empty():
var tile: Vector2i = queue.pop_front()
for direction in Maze.DIRS:
var next: Vector2i = Maze.neighbor(tile, direction)
if Maze.walkable(next) and not seen.has(next):
seen[next] = true
queue.append(next)
for tile in Maze.pellets(): check(seen.has(tile), "reachable pellet %s" % tile)
check(not Maze.walkable(Vector2i(13, 12)), "player cannot enter ghost door")
check(Maze.neighbor(Vector2i(0,14), Vector2i.LEFT) == Vector2i(27,14), "tunnel wrap")
check(Rules.target(0, Vector2i(5,5), Vector2i.UP, Vector2i.ZERO, Vector2i.ZERO) == Vector2i(5,5), "Blinky target")
check(Rules.target(1, Vector2i(5,5), Vector2i.UP, Vector2i.ZERO, Vector2i.ZERO) == Vector2i(1,1), "Pinky upward quirk")
check(Rules.target(2, Vector2i(5,5), Vector2i.RIGHT, Vector2i(3,5), Vector2i.ZERO) == Vector2i(11,5), "Inky target")
check(Rules.target(3, Vector2i(5,5), Vector2i.RIGHT, Vector2i.ZERO, Vector2i(6,5)) == Rules.SCATTER[3], "Clyde retreats")
var m = Model.new()
m.start()
m.update(5.0)
check(m.state == "playing", "ready becomes playing")
# Movement stops at the corridor wall, despite a held direction.
for i in range(180):
m._move_player(0.1)
m.pellet_pause = 0.0
check(Maze.walkable(m.player.tile), "movement stays in corridor")
# Energizer changes active ghosts; capture returns eyes and adds score.
m.player = Maze.actor(Vector2i(1,3), Vector2i.DOWN)
m._consume()
check(m.fright_time > 0.0, "energizer starts frightened timer")
var before: int = m.score
m.ghosts[0].tile = m.player.tile
m.ghosts[0].next = m.player.tile
m.ghosts[0].progress = 0.0
m._check_collisions()
check(m.ghosts[0].status == "eyes" and m.score == before + 200, "ghost capture score and eyes")
m.reset()
check(m.state == "idle" and m.score == 0 and m.dots.size() == 244, "reset restores board")
m.start()
m.state = "playing"
m.dots = {m.player.tile: false}
m._consume()
check(m.state == "clear", "last pellet clears level")
m.update(3.0)
check(m.state == "won", "default room objective wins")
m.boards_to_win = 0
m.start()
m.state = "clear"
m.phase_time = 0.0
m.update(0.1)
check(m.level == 2 and m.state == "ready" and m.dots.size() == 244, "arcade progression")
m.state = "clear"
m.phase_time = 0.0
m.update(0.1)
check(m.state == "intermission", "second board starts intermission")
m.update(6.0)
check(m.state == "ready" and m.level == 3, "intermission continues to next level")
m.lives = 1
m.state = "dying"
m.phase_time = 0.0
m.update(0.1)
assert(m.state == "death_pop", "death pop before final loss")
m.phase_time = 0.0
m.update(0.1)
check(m.state == "lost", "final life loses")
m.start()
m.state = "playing"
m.score_to_win = 100
m._add_score(100)
check(m.state == "won", "score objective")
# Traverse the entire board through the actual movement and consumption code.
m.score_to_win = 0
m.boards_to_win = 1
m.start()
m.state = "playing"
var moves := 0
while not m.dots.is_empty() and moves < 3000:
var frontier: Array = [m.player.tile]
var parent := {m.player.tile: null}
var destination: Vector2i = m.player.tile
while not frontier.is_empty():
var current: Vector2i = frontier.pop_front()
if m.dots.has(current):
destination = current
break
for direction in Maze.DIRS:
var next: Vector2i = Maze.neighbor(current, direction)
if Maze.walkable(next) and not parent.has(next):
parent[next] = current
frontier.append(next)
var path: Array[Vector2i] = []
while destination != m.player.tile:
path.push_front(destination)
destination = parent[destination]
if path.is_empty():
m._consume()
for tile in path:
var direction: Vector2i = tile - m.player.tile
if abs(direction.x) > 1: direction.x = -signi(direction.x)
m.requested = direction
for step in range(50):
m.pellet_pause = 0.0
m._move_player(0.1)
if m.player.tile == tile: break
check(m.player.tile == tile, "buffered turn reaches next corridor tile")
moves += 1
check(m.dots.is_empty() and m.state == "clear", "full maze cleared through movement")
# Every returning ghost must be able to reach the house and emerge again.
m.start()
m.state = "playing"
for tile in seen:
var g: Dictionary = m.ghosts[0]
g.tile = tile
g.next = tile
g.progress = 0.0
g.dir = Vector2i.LEFT
g.status = "eyes"
g.reverse = false
for step in range(1600):
m._move_ghost(0, 0.5)
if g.status == "active": break
check(g.status == "active", "eyes return from %s" % tile)
# Headless random walk checks ghost movement stays in legal corridors.
m.score_to_win = 0
m.start()
m.state = "playing"
for frame in range(12000):
m.requested = Maze.DIRS[(frame / 30) % 4]
m.update(1.0 / 60.0)
if m.state == "lost": m.start()
for g in m.ghosts:
if g.status == "active":
check(Maze.walkable(g.tile), "ghost stays on map")
var game = load("res://games/pacman/Pacman.tscn").instantiate()
root.add_child(game)
var counts := {"start": 0, "won": 0, "lost": 0, "reset": 0}
game.game_started.connect(func(): counts.start += 1)
game.game_won.connect(func(): counts.won += 1)
game.game_lost.connect(func(): counts.lost += 1)
game.game_reset.connect(func(): counts.reset += 1)
check(game.start_game(), "ArcadeGame starts")
check(not game.start_game(), "duplicate start ignored")
game.win_game()
game.win_game()
check(counts.won == 1 and counts.start == 1, "win event exactly once")
game.reset_game()
game.start_game()
game.lose_game()
game.lose_game()
check(counts.lost == 1 and counts.reset == 1, "loss and reset events")
var loop = game.audio._loop_stream("siren0", game.audio.stream("siren0_firstloop"))
check(loop.loop_begin > 0 and loop.loop_end > loop.loop_begin, "intro plays once before PCM loop")
game.audio.set_background("fright")
game.reset_game()
check(game.audio.background == "" and not game.audio.ambience.playing, "reset stops ambience")
game.queue_free()
print("PACMAN: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0)
+1
View File
@@ -0,0 +1 @@
uid://by4wo7vr2sxx0
+25
View File
@@ -0,0 +1,25 @@
extends SceneTree
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var game = load("res://games/galaga/Galaga.tscn").instantiate()
root.add_child(game)
game.set_physics_process(false)
game.state="playing"
game.player.position=Vector2(112.1,256.1)
await process_frame
await process_frame
await RenderingServer.frame_post_draw
var first := root.get_texture().get_image().get_data()
game.player.position=Vector2(112.4,256.4)
await process_frame
await process_frame
await RenderingServer.frame_post_draw
assert(first==root.get_texture().get_image().get_data(),"Fractional positions within the same native pixel must render identically")
game.player.position=Vector2(112.7,256.7)
await process_frame
await process_frame
await RenderingServer.frame_post_draw
assert(first!=root.get_texture().get_image().get_data(),"Crossing a native pixel boundary must move the rendered sprite")
print("NATIVE PIXEL RENDER TEST PASS")
quit()
+1
View File
@@ -0,0 +1 @@
uid://cbuq7cc4x2jyr
+146
View File
@@ -0,0 +1,146 @@
extends Node
func _ready() -> void:
var room := RoomDefinition.new()
var immediate_rule := RoomLogicRule.new()
immediate_rule.rule_id = (
&"win_turns_on_light"
)
immediate_rule.source_id = (
&"Cabinet01"
)
immediate_rule.event_id = (
&"game_won"
)
immediate_rule.target_id = (
&"StartLight01"
)
immediate_rule.action_id = (
&"turn_on"
)
immediate_rule.delay_seconds = (
0.0
)
var delayed_rule := RoomLogicRule.new()
delayed_rule.rule_id = (
&"win_unlocks_drawer"
)
delayed_rule.source_id = (
&"Cabinet01"
)
delayed_rule.event_id = (
&"game_won"
)
delayed_rule.target_id = (
&"DrawerLock01"
)
delayed_rule.action_id = (
&"unlock"
)
delayed_rule.delay_seconds = (
2.0
)
room.logic_rules = [
immediate_rule,
delayed_rule,
]
var engine := RoomLogicEngine.new()
engine.configure(
room
)
engine.event_received.connect(
func(
source_id: StringName,
event_id: StringName
) -> void:
print(
"[LOGIC TEST] EVENT ",
source_id,
".",
event_id
)
)
engine.rule_triggered.connect(
func(
rule_id: StringName,
_source_id: StringName,
_event_id: StringName,
_target_id: StringName,
_action_id: StringName
) -> void:
print(
"[LOGIC TEST] RULE ",
rule_id
)
)
engine.action_requested.connect(
func(
target_id: StringName,
action_id: StringName,
rule_id: StringName
) -> void:
print(
"[LOGIC TEST] ACTION ",
target_id,
".",
action_id,
" from ",
rule_id,
" at ",
engine.get_simulation_time()
)
)
print(
"--- FIRE GAME WON ---"
)
engine.emit_event(
&"Cabinet01",
&"game_won"
)
print(
"--- ADVANCE 1 SECOND ---"
)
engine.advance(
1.0
)
print(
"--- ADVANCE 1 MORE SECOND ---"
)
engine.advance(
1.0
)
+1
View File
@@ -0,0 +1 @@
uid://i3jtfiim12je
+6
View File
@@ -0,0 +1,6 @@
[gd_scene format=3 uid="uid://dfit3ateu8q3u"]
[ext_resource type="Script" uid="uid://i3jtfiim12je" path="res://tests/test_room_logic_engine.gd" id="1_0g6lg"]
[node name="Node2D" type="Node2D" unique_id=224727458]
script = ExtResource("1_0g6lg")
+50
View File
@@ -0,0 +1,50 @@
extends SceneTree
const Model = preload("res://games/pacman/pacman_model.gd")
const Maze = preload("res://games/pacman/pacman_maze.gd")
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var model = Model.new()
model.start()
model.update(4.3)
assert(model.state == "playing")
model.state = "playing"
model.ghosts[0].tile = model.player.tile
model.ghosts[0].next = model.player.tile
model.ghosts[0].progress = 0.0
model._check_collisions()
assert(model.state == "death_pause")
assert(is_equal_approx(model.phase_time, 1.5))
var paused_clock: float = model.clock
model.update(0.5)
assert(is_equal_approx(model.clock, paused_clock))
assert(model.state == "death_pause")
var sounds := []
model.sound_requested.connect(func(name: String): sounds.append(name))
model.update(1.51)
assert(model.state == "dying")
assert(sounds == ["death"])
assert(is_equal_approx(model.phase_time, 1.55))
model.update(1.56)
assert(model.state == "death_pop")
assert(is_equal_approx(model.phase_time, 0.24))
model.update(0.25)
assert(model.lives == 2)
assert(model.state == "ready")
assert(Maze.position(model.player) == Vector2(13, 23))
assert(Maze.position(model.ghosts[0]) == Vector2(13.5, 11))
model.state = "playing"
model._move_ghost(0, 0.0)
assert(Maze.position(model.ghosts[0]) == Vector2(13, 11))
var scene = load("res://games/pacman/Pacman.tscn").instantiate()
root.add_child(scene)
var preview = scene.get_node("EditorPreview")
await process_frame
if Engine.is_editor_hint():
assert(preview.model.dots.size() == 244)
assert(preview.model.state == "ready")
scene.free()
print("START/DEATH TUNING PASS")
quit()
+1
View File
@@ -0,0 +1 @@
uid://v5anjrmi1rpe
+130
View File
@@ -0,0 +1,130 @@
extends SceneTree
var failures := 0
var won_count := 0
var lost_count := 0
func check(value: bool, message: String) -> void:
if not value:
push_error(message)
failures += 1
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var game = load("res://games/tempest/Tempest.tscn").instantiate()
root.add_child(game)
game.set_physics_process(false)
game.game_won.connect(func(): won_count += 1)
game.game_lost.connect(func(): lost_count += 1)
check(game.start_game() and not game.start_game(),"single start")
check(game.phase == "select","start opens selection")
game.rotate_steps(2)
game.confirm_level()
check(game.stage == 5 and game.score == 0 and game.pending_bonus == 16000,"level five bonus is deferred")
game._physics_process(1.5)
check(game.phase == "play","ready transitions")
game.rotate_steps(-1)
check(game.lane == 15,"lane wrapping")
game.fire_timer = 0.0
game.enemies.assign([game.make_enemy("flipper",15,.9)])
game.fire()
game._step_play(.1)
check(game.score == 150,"swept shot scores flipper")
game.enemies.assign([game.make_enemy("tanker",15,.6)])
game.fire_timer = 0
game.shots.clear()
game.fire()
game._step_play(.2)
check(game.enemies.size() == 2 and game.enemies[0].kind == "flipper","tanker splits on hit")
game.superzap()
check(game.enemies.is_empty() and game.zap_uses == 1,"first zap clears")
game.enemies.assign([game.make_enemy("flipper",0,.1),game.make_enemy("flipper",1,.2)])
game.zap_flash = 0
game.superzap()
check(game.enemies.size() == 1 and game.zap_uses == 2,"second zap kills one")
game.zap_flash = 0
game.superzap()
check(game.enemies.size() == 1,"third zap unavailable")
game.enemies.clear()
game.remaining = 0
var prior_score: int = game.score
game._step_play(.01)
check(game.phase == "dive" and game.score == prior_score+16000 and game.cleared == 1,"first clear earns bonus")
game._physics_process(1.7)
check(game.phase == "space","dive enters starfield")
game._physics_process(1.3)
check(game.phase == "arrive" and game.stage == 6,"next tube arrives")
game._physics_process(1.7)
check(game.phase == "ready" and game.pending_bonus == 0,"bonus not repeated")
game.cleared = game.stages_to_win
game.phase = "space"
game.phase_time = .1
game._physics_process(.2)
game._physics_process(1.1)
game._physics_process(1.1)
check(game.phase == "won" and won_count == 1 and not game.is_running,"single room win counts clears not level number")
game.reset_game()
game.start_game()
game._physics_process(10.1)
check(game.phase == "ready" and game.stage == 1,"selection timeout starts default")
game.phase = "play"
game.lives = 1
game.remaining = 10
game.enemies.assign([game.make_enemy("flipper",game.lane,1.0)])
game._step_play(.01)
game._physics_process(1.3)
check(game.phase == "lost" and lost_count == 1,"last life loses")
game.reset_game()
game.start_game()
game.confirm_level()
game.phase = "play"
var enemy: Dictionary = game.make_enemy("flipper",15,.6)
game._start_roll(enemy,1)
check(game.collision_lane(enemy) == 15,"roll collision starts in source lane")
enemy.roll = .1
check(game.collision_lane(enemy) == 0,"roll collision ends in destination lane")
game.enemies.assign([game.make_enemy("spiker",3,.64)])
game._step_play(.2)
check(game.spikes.has(3) and game.enemies[0].retreat,"spiker builds and retreats")
game.spikes[3] = .4
game.phase = "dive"
game.phase_time = .6
game.lane = 3
game.cleared = 1
var prior_lives: int = game.lives
game._physics_process(.01)
check(game.phase == "death" and game.lives == prior_lives-1 and game.cleared == 0,"warp spike collision")
var board = game.get_node("VectorBoard")
var center: Vector2 = (board.rim(0,0)+board.rim(8,0))*.5
check(is_equal_approx(center.x,320) and center.y > 410,"vanishing point centered and low")
game.stage = 9
game.phase = "play"
game.lane = 0
game.rotate_steps(-1)
check(game.lane == 0,"open tube does not wrap")
game.lane = 15
game.rotate_steps(1)
check(game.lane == 15,"open tube right boundary")
game.stages_to_win = 0
game.phase = "space"
game.phase_time = .1
game._physics_process(.2)
check(game.phase == "arrive","endless mode keeps progressing")
# Simulate a longer random play sequence to exercise rendering data and AI.
game.reset_game()
game.start_game()
game.selection = 4
game.confirm_level()
game.phase = "play"
game.lives = 999
for i in range(3600):
if i%6 == 0: game.rotate_steps(1)
game.fire_timer = maxf(0,game.fire_timer-1.0/60)
game.fire()
game._physics_process(1.0/60)
check(game.score >= 0,"minute of simulation completes")
print("Tempest v2 checks complete: ",failures," failures")
game.get_node("VectorAudio").stop()
await create_timer(.2).timeout
game.queue_free()
await process_frame
quit(failures)
+1
View File
@@ -0,0 +1 @@
uid://c7pqujf6xdhln
+29
View File
@@ -0,0 +1,29 @@
extends SceneTree
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var game=load("res://games/galaga/Galaga.tscn").instantiate()
root.add_child(game)
game.set_physics_process(false)
game.start_game()
var events := []
game.game_won.connect(func(): events.append("won"))
game.state="playing"
game._bullet(Vector2(100,200),Vector2(0,-100),true)
game.win_game()
assert(game.state=="victory" and events.is_empty())
game._tick(0.5)
assert(game.bullets[0].node.position.y==150)
assert(game.state=="victory")
game.win_game()
game._tick(1.01)
assert(game.state=="won" and not game.is_running and events.size()==1)
var before: float=game.clock
game._physics_process(0.5)
assert(game.clock>before)
game.reset_game()
assert(game.state=="idle")
print("VICTORY TRANSITION PASS")
game.queue_free()
await process_frame
quit()
+1
View File
@@ -0,0 +1 @@
uid://dv2jpcurcud0l