spaceship/tests/test_manual_supply_route.gd

218 lines
7.7 KiB
GDScript3
Raw Normal View History

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 does NOT bank the hold. It used to, edge
# triggered on arrival (#14); #24 removed that entirely, because an
# automatic one-way move overrides a player who has just loaded the
# hold in order to carry ore somewhere else
# 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 mothership_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
2026-08-12 22:47:46 +02:00
game_state.stored_vessels.clear()
game_state.build_time_scale = 0.02
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)
mothership_id = int(game_state.buildings[-1].id)
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.transfer_to_fleet(mothership_id, 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. parked on the mothership, and arriving again --------
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 banked again by itself)")
quit(1)
return true
# Leaving and coming back is the case the old edge-triggered
# delivery fired on, so it is the one worth re-entering.
_park_fleet_on(CRAFTER_AT)
_park_fleet_on(MOTHERSHIP_AT)
if game_state.fleet_cargo != GameState.SHIP_COST:
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (arriving at the mothership auto-banked the hold - #24 removed that)")
quit(1)
return true
print("hold kept its %d across an arrival (no auto-banking)" % 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.transfer_to_building(crafter_id, game_state.fleet_cargo)
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:
2026-08-12 22:47:46 +02:00
# Long enough for the build itself, which #23 made take time.
if elapsed < game_state.build_time(GameState.PRODUCT_SHIP) * 3.0:
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
2026-08-12 22:47:46 +02:00
# Since #23 a finished vessel waits in the hangar rather than
# crewing itself, so this is where it lands.
if game_state.stored_vessels.size() != 1:
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (no vessel built: %d in the hangar)" % game_state.stored_vessels.size())
quit(1)
return true
2026-08-12 22:47:46 +02:00
print("queued order fulfilled by hand, no transport hub: %d vessel in the hangar" % game_state.stored_vessels.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.transfer_to_fleet(mothership_id, 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()