extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_asteroid_safe_spawn.gd # Asteroid chunks are populated around wherever the fleet is - including the # chunk it is standing in - and positions used to be uniform within a chunk. # So an asteroid could be created directly on top of a ship and destroy it # with no chance to react. Worst on the frame a scene loads: coming back from # a planet's surface could cost a vessel instantly, which is also what made # test_return_position_matches_planet fail intermittently. # # Verifies: # 1. _safe_position never returns a point inside the bubble, for candidates # all around and including the exact centre. This is checked directly # rather than by looking at where asteroids happened to land: with ~54 # asteroids spread over 25 chunks, any single run only rarely produces a # near miss, so an observational check passes just as happily with the # guard removed and proves nothing. # 2. candidates already outside the bubble are left exactly where they are, # so the field is not distorted away from the fleet # 3. the field still spawns a full complement, and a stationary fleet loses # no ships var scene: Node var fleet: Node var field: Node var stage: int = 0 var elapsed: float = 0.0 var ships_at_start: int = 0 func _initialize() -> void: root.get_node("GameState").reset_formation() scene = load("res://world/Main.tscn").instantiate() root.add_child(scene) fleet = scene.get_node("Fleet") field = scene.get_node("AsteroidField") func _process(delta: float) -> bool: elapsed += delta match stage: 0: scene.get_node("EnemySpawner").set_process(false) ships_at_start = fleet.ships.size() elapsed = 0.0 stage = 1 return false 1: # Let the field populate around the fleet a few times over. if elapsed < 1.5: return false var centre: Vector2 = fleet.get_centroid() var safe: float = field.safe_spawn_radius # --- the guard itself, deterministically ---------------------- var worst_inside: float = 1e9 var moved_an_outside_one: bool = false for i in range(72): var a: float = (float(i) / 72.0) * TAU var dir: Vector2 = Vector2(cos(a), sin(a)) # Candidates well inside, just inside, and exactly on centre. for d in [0.0, 1.0, safe * 0.25, safe * 0.99]: var got: Vector2 = field._safe_position(centre + dir * d, centre) worst_inside = minf(worst_inside, got.distance_to(centre)) # Candidates already outside must be returned untouched. var outside: Vector2 = centre + dir * (safe * 1.5) if field._safe_position(outside, centre).distance_to(outside) > 0.001: moved_an_outside_one = true print("closest result for an inside candidate: %.1f (safe radius %.1f)" % [ worst_inside, safe ]) if worst_inside < safe - 0.001: print("TEST_ASTEROID_SAFE_SPAWN: FAIL (guard let a spawn land inside the fleet's bubble)") quit(1) return true if moved_an_outside_one: print("TEST_ASTEROID_SAFE_SPAWN: FAIL (guard displaced an asteroid that was already clear)") quit(1) return true var asteroids: Array = get_nodes_in_group("asteroids") var closest: float = 1e9 for a in asteroids: if is_instance_valid(a): closest = minf(closest, a.position.distance_to(centre)) print("%d asteroids, closest is %.1f units from the fleet (safe radius %.1f)" % [ asteroids.size(), closest, safe ]) if asteroids.is_empty(): print("TEST_ASTEROID_SAFE_SPAWN: FAIL (field spawned nothing - test proves nothing)") quit(1) return true # Asteroids drift, so one may have travelled inward since it was # created. Allow for that, but nothing should be anywhere near # on top of the fleet. if closest < safe * 0.5: print("TEST_ASTEROID_SAFE_SPAWN: FAIL (asteroid created on top of the fleet)") quit(1) return true var kept_ships: bool = fleet.ships.size() == ships_at_start print("ships %d -> %d (kept=%s)" % [ships_at_start, fleet.ships.size(), kept_ships]) if not kept_ships: print("TEST_ASTEROID_SAFE_SPAWN: FAIL (a stationary fleet lost a ship to a spawn)") quit(1) return true print("TEST_ASTEROID_SAFE_SPAWN: PASS") quit(0) return true return false