spaceship/tests/test_manual_supply_route.gd
Loïc Wisniewski 19ac758da1 Typed owed vessels, ship lab building type, shield vessel gate
Groundwork for #5. vessels_owed carries the VESSEL TYPE rather than a
bare count, so a shield vessel finishing while the player is on a planet
comes back as a shield vessel. Fleet.add_ship takes a type, and vessels
already flying are re-statted when an upgrade is researched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:10:07 +02:00

205 lines
6.9 KiB
GDScript

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_manual_supply_route.gd
# Forgejo #17 (part 4 of #12): the player can supply a building by hand -
# withdraw banked ore into the fleet's hold at the mothership, fly it over,
# and have the building take it.
#
# This is what keeps the game unblockable. #16 lets an order queue waiting
# for ore, and until transporters exist (#19) there is otherwise no way to
# fulfil one - a player with a queued order and no Transport Hub would be
# stuck forever.
#
# Verifies:
# 1. withdrawing moves ore bank -> hold, and conserves the total
# 2. withdrawing is capped by what is actually banked (no overdraft)
# 3. arriving at the mothership banks the hold, but doing so is EDGE
# triggered - ore withdrawn while parked there is not instantly taken
# back, which would make the button look broken
# 4. a building takes the fleet's whole hold when opened
# 5. end to end: a queued order the bank cannot reach is fulfilled using
# only the fleet, with no Transport Hub in existence
# 6. nothing is created or destroyed anywhere along the route
var game_state: Node
var scene: Node
var fleet: Node
var field: Node
var controller: Node
var crafter_id: int = 0
var stage: int = 0
var elapsed: float = 0.0
var ships_before: int = 0
var total_before: int = 0
const MOTHERSHIP_AT: Vector2 = Vector2(0.0, 0.0)
const CRAFTER_AT: Vector2 = Vector2(3000.0, 0.0)
func _total() -> int:
var sum: int = game_state.rocks + game_state.fleet_cargo
for b in game_state.buildings:
sum += int(b.get("stock", 0))
return sum
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.rocks = 40
game_state.fleet_cargo = 0
game_state.vessels_owed.clear()
game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"]
scene = load("res://Main.tscn").instantiate()
root.add_child(scene)
fleet = scene.get_node("Fleet")
field = scene.get_node("BuildingField")
controller = scene.get_node("BuildController")
func _park_fleet_on(at: Vector2) -> void:
for ship in fleet.ships:
ship.position = at
fleet.move_to(at)
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
_clear_hazards()
fleet.spawn_ships(2, MOTHERSHIP_AT)
field.place(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
field.place(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT)
crafter_id = int(game_state.buildings[-1].id)
total_before = _total()
_park_fleet_on(CRAFTER_AT)
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.3:
return false
# --- 5. an order the crafter cannot pay for -----------------
ships_before = fleet.ships.size()
game_state.queue_order(crafter_id)
elapsed = 0.0
stage = 2
return false
2:
if elapsed < 0.3:
return false
if game_state.pending_orders(crafter_id) != 1:
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (order did not queue)")
quit(1)
return true
# --- 1 + 2. withdraw at the mothership ----------------------
_park_fleet_on(MOTHERSHIP_AT)
elapsed = 0.0
stage = 3
return false
3:
if elapsed < 0.3:
return false
var bank_before: int = game_state.rocks
var took: int = game_state.withdraw_to_fleet(GameState.SHIP_COST)
if took != GameState.SHIP_COST or game_state.fleet_cargo != GameState.SHIP_COST:
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (withdrew %d into a hold of %d)" % [took, game_state.fleet_cargo])
quit(1)
return true
if game_state.rocks != bank_before - GameState.SHIP_COST:
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (bank did not pay for the withdrawal)")
quit(1)
return true
print("withdrew %d at the mothership -> bank=%d hold=%d" % [
took, game_state.rocks, game_state.fleet_cargo
])
elapsed = 0.0
stage = 4
return false
4:
# --- 3. still parked on the mothership ----------------------
if elapsed < 0.5:
return false
if game_state.fleet_cargo != GameState.SHIP_COST:
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (ore withdrawn while parked on the mothership was instantly banked again - delivery is not edge triggered)")
quit(1)
return true
print("still parked -> hold kept its %d (delivery is edge triggered)" % game_state.fleet_cargo)
# --- 4. carry it to the crafter and open it -----------------
_park_fleet_on(CRAFTER_AT)
elapsed = 0.0
stage = 5
return false
5:
if elapsed < 0.6:
return false
var moved: int = game_state.unload_into_building(crafter_id)
if moved != GameState.SHIP_COST or game_state.fleet_cargo != 0:
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (unloaded %d, hold left at %d)" % [moved, game_state.fleet_cargo])
quit(1)
return true
print("unloaded %d into the crafter" % moved)
elapsed = 0.0
stage = 6
return false
6:
if elapsed < 0.5:
return false
# --- 5. the queued order completed off the fleet's own ore ---
if game_state.pending_orders(crafter_id) != 0:
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (order still queued after a hand delivery)")
quit(1)
return true
if fleet.ships.size() != ships_before + 1:
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (no vessel: %d -> %d)" % [ships_before, fleet.ships.size()])
quit(1)
return true
print("queued order fulfilled by hand, no transport hub: ships %d->%d" % [
ships_before, fleet.ships.size()
])
# --- 6. conservation ----------------------------------------
var expected: int = total_before - GameState.SHIP_COST
if _total() != expected:
print("bank=%d hold=%d stocks total=%d, expected %d" % [
game_state.rocks, game_state.fleet_cargo, _total(), expected
])
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (ore created or destroyed along the route)")
quit(1)
return true
print("ore conserved: %d -> %d, exactly the %d the vessel cost" % [
total_before, _total(), GameState.SHIP_COST
])
# --- 2. no overdraft ----------------------------------------
game_state.rocks = 3
var over: int = game_state.withdraw_to_fleet(999)
if over != 3 or game_state.rocks != 0:
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (overdrew: took %d from a bank of 3, leaving %d)" % [over, game_state.rocks])
quit(1)
return true
print("withdrawing 999 from a bank of 3 took %d" % over)
print("TEST_MANUAL_SUPPLY_ROUTE: PASS")
quit(0)
return true
return false
func _clear_hazards() -> void:
# set_process(false) on AsteroidField only stops NEW chunks spawning -
# rocks already in the world keep drifting and can destroy a parked ship,
# which silently changes the fleet size this test asserts on. This is a
# logistics test, not a survival one.
for a in get_nodes_in_group("asteroids"):
if is_instance_valid(a):
a.queue_free()
for e in get_nodes_in_group("enemies"):
if is_instance_valid(e):
e.queue_free()