extends SceneTree ## Headless smoke test: ## /Applications/Godot.app/Contents/MacOS/Godot --headless --path . --script tests/smoke.gd ## Registry finds content; each mode boots and simulates; the car drives; traffic ## gets smashed when driven through. func _initialize() -> void: _run() func _run() -> void: var cars := Registry.cars() var levels := Registry.levels() _check(not cars.is_empty(), "no cars found") _check(not levels.is_empty(), "no levels found") for p in cars.values(): var s: CarStats = load(p) _check(s != null and s.engine_power > 0.0, "bad stats: %s" % p) if s.model_path != "": _check(load(s.model_path) is PackedScene, "bad model: %s" % s.model_path) # datto is procedural (has det_* parts); imported models legitimately have none Game.car_path = cars.get("datto_120y", cars.values()[0]) # the carpark is the controlled arena; city levels have walls that complicate the ram test Game.level_path = levels.get("woolies_carpark", levels.values()[0]) # cruise: throttle 3s, car must move print(" .. cruise") var game: Game = await _boot("cruise") if game.car.stats.model_path != "": _check(game.car._parts.size() >= 8, "model has no detachable det_* parts") var start: Vector3 = game.car.global_position Input.action_press("throttle") for i in 180: await physics_frame var dist := start.distance_to(game.car.global_position) _check(dist > 5.0, "car didn't move (%.1f m)" % dist) Input.action_release("throttle") game.free() # crash mode: traffic spawned, ram the nearest one, score must rise print(" .. crash") game = await _boot("crash") _check(game.traffic.size() == Game.TRAFFIC_COUNT["crash"], "traffic missing") var victim: TrafficCar = game.traffic[0] await physics_frame # traffic takes position on its first physics tick Input.action_press("throttle") for attempt in 8: # re-aim: victim keeps moving along the loop if not victim.live: break var to_victim := (victim.global_position - game.car.global_position) game.car.global_position = victim.global_position - victim.global_basis.z * 8.0 game.car.look_at(victim.global_position) to_victim = (victim.global_position - game.car.global_position).normalized() game.car.linear_velocity = to_victim * 18.0 for i in 30: await physics_frame if not victim.live: break Input.action_release("throttle") _check(not victim.live, "rammed traffic never went dynamic") _check(game.score > 0.0, "crash mode scored nothing") var crash_score := game.score game.free() # junction: convoys spawn, driving straight in causes the crash -> settle -> # crashbreaker -> tally var jcts := Registry.junctions().filter(func(j): return j.level_id == "woolies_carpark") _check(not jcts.is_empty(), "no woolies junction event") Game.junction = jcts[0].data print(" .. junction") game = await _boot("junction") _check(game.traffic.size() >= 8, "junction convoys missing (%d)" % game.traffic.size()) Input.action_press("throttle") for i in 950: await physics_frame if game.jstate == "settle" and not game.breaker_used: Input.action_release("throttle") Input.action_press("boost") elif game.breaker_used: Input.action_release("boost") if game.over: break Input.action_release("throttle") Input.action_release("boost") _check(game.jstate != "run", "never crashed into the convoy") _check(game.breaker_used, "crashbreaker never fired") _check(game.over and game.score > 0.0, "junction never tallied (score %d)" % int(game.score)) var jct_score := game.score Game.junction = {} game.free() # outrun: roaming rivals exist in cruise; pulling alongside at speed arms the # duel, and the win path pays out and clears state game = await _boot("cruise") _check(game.rivals.size() == Game.ROAM_RIVALS, "no roaming rivals in cruise") var rr: Rival = game.rivals[0] await physics_frame game.car.global_position = rr.global_position + Vector3(3, 0.3, 0) game.car.linear_velocity = Vector3(0, 0, -12) # above the 8 m/s arming speed for i in 5: await physics_frame if game.outrun_rival != null: break _check(game.outrun_rival == rr, "outrun didn't arm on proximity") var bm0: float = game.car.boost_max game._outrun_end(true, "test") _check(game.outrun_rival == null and game.outruns_won == 1, "outrun win didn't settle") _check(game.car.boost_max > bm0, "outrun win paid no boost") _check(game.outrun_cd > 0.0, "no re-arm cooldown after outrun") game.free() # pursuit: smashing raises heat, heat summons cops, fleeing loses them print(" .. pursuit") game = await _boot("cruise") for i in 10: game.add_heat(Game.HEAT_PER_SMASH) _check(game.cops.size() >= Game.COPS_MIN, "heat didn't summon cops") game.car.global_position += Vector3(900, 0, 900) for i in 700: await physics_frame if game.cops.is_empty(): break _check(game.escapes == 1 and game.cops.is_empty(), "escape never resolved") _check(game.heat_cd > 0.0, "no cooldown after escape") game.free() # drag: needs a level with DragStart/DragEnd, so not the code-built carpark Game.level_path = levels["southbank"] print(" .. drag") game = await _boot("drag") _check(game.drag_len > 100.0, "no drag strip found (%.0f m)" % game.drag_len) _check(game.rivals.size() == 1, "drag had no opponent") Input.action_press("throttle") for i in 10: await physics_frame _check(game.revs > 0.0, "revs never climbed") var g0: int = game.gear # shift the moment revs enter the window. First gear redlines in ~1.6 s, so # idling here first pins revs at 1.0 and the shift correctly scores as blown. var spin := 0 while game.revs < Game.SHIFT_LO and spin < 600: # bounded: never hang the suite await physics_frame spin += 1 _check(game.revs >= Game.SHIFT_LO and game.revs < Game.REDLINE, "revs %.2f not inside the shift window [%.2f, %.2f)" % [game.revs, Game.SHIFT_LO, Game.REDLINE]) Input.action_press("boost") await physics_frame Input.action_release("boost") await physics_frame _check(game.gear == g0 + 1, "shift didn't change gear") _check(game.clean_shifts == 1 and game.blown_shifts == 0, "in-window shift scored as blown (%d/%d)" % [game.clean_shifts, game.blown_shifts]) Input.action_release("throttle") game.free() Game.level_path = levels.get("woolies_carpark", levels.values()[0]) # derby: opponents spawn in a ring and each hunts something print(" .. derby") game = await _boot("derby") _check(game.derby.size() == Game.DERBY_CARS, "derby field wrong size") await physics_frame for d in game.derby: _check(d.target != null and d.target != d, "derby car has no/self target") game.free() # race: rivals spawn and make progress along the path print(" .. race") game = await _boot("race") _check(game.rivals.size() == Game.RIVAL_COUNT, "rivals missing") var p0: float = game.rivals[0].progress for i in 240: await physics_frame var moved := false for r in game.rivals: if absf(r.progress - p0) > 3.0 or r.linear_velocity.length() > 3.0: moved = true _check(moved, "no rival is driving") game.free() print("smoke ok: %d cars, %d levels | cruise %.1f m | crash $%d | junction $%d | outrun arms+pays | pursuit spawns+escapes | drag shifts | derby %d cars | race AI driving" % [cars.size(), levels.size(), dist, int(crash_score), int(jct_score), Game.DERBY_CARS]) quit(0) func _check(cond: bool, msg: String) -> void: ## NOT assert(): a failed assert in a headless SceneTree script halts the ## script but leaves the tree running, so the process spins forever instead ## of failing. One bad assertion cost 53 minutes of wall clock before this. if cond: return push_error(msg) print("SMOKE FAIL: %s" % msg) quit(1) func _boot(mode: String) -> Game: Game.mode = mode var game: Game = (load("res://src/game.tscn") as PackedScene).instantiate() root.add_child(game) await process_frame # _ready is deferred until the main loop runs return game