114 lines
4.2 KiB
GDScript3
114 lines
4.2 KiB
GDScript3
|
|
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://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
|