Added Glitch Room Source code to Gitty
This commit is contained in:
commit
1ac9eb5644
505 files changed
+28504
No files matched your search
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://b2xuro2dta84x
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://1hmcrcrsvulb
|
||||
@@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cohmnr1oklto5
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://swmu6kexenhl
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bb1cbfxyd5pif
|
||||
@@ -0,0 +1,3 @@
|
||||
*.log
|
||||
*.png
|
||||
*.png.import
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://gptkt4d8d3sy
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://by2ur06o802rn
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://b8t02quve6wu1
|
||||
@@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://da2ysnuinbmh6
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,3 @@
|
||||
*.log
|
||||
*.png
|
||||
*.png.import
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://bly0tpogxcin0
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://clahhchgclvpb
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://c2ikw4y71sl30
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://by4wo7vr2sxx0
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cbuq7cc4x2jyr
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
uid://i3jtfiim12je
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://v5anjrmi1rpe
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://c7pqujf6xdhln
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dv2jpcurcud0l
|
||||
Reference in new issue
Block a user