spaceship/tests/test_building_stock_queue.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

247 lines
8.9 KiB
GDScript

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_building_stock_queue.gd
# Forgejo #16 (part 3 of #12): a building produces out of its OWN stock, and
# an order it cannot pay for yet is queued rather than refused.
#
# Before this, BuildController deducted from the bank at the moment of
# purchase and refused outright with "Not enough rocks", so nothing ever had
# to be delivered anywhere.
#
# Verifies:
# 1. ordering with an empty stock queues instead of failing, and produces
# nothing yet
# 2. it does NOT quietly take the cost out of the bank - the whole point is
# that the building has to be supplied
# 3. delivering enough stock completes the order on its own
# 4. one delivery covering several queued orders clears them all, rather
# than dribbling out one per frame
# 5. a part-delivery is not enough: stock below the cost leaves the order
# queued and the ore untouched
# 6. stock and queue survive the space scene being torn down (the trip to
# a planet's surface)
# 7. production completes with NO fleet in existence, and the fleet picks
# the vessel up when it comes back - orders must not depend on which
# scene the player is standing in
var game_state: Node
var scene: Node
var fleet: Node
var field: Node
var crafter_id: int = 0
var stage: int = 0
var elapsed: float = 0.0
var ships_at_start: int = 0
var bank_at_start: int = 0
var ticks: int = 0
const CRAFTER_AT: Vector2 = Vector2(900.0, 0.0)
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.rocks = 500
game_state.fleet_cargo = 0
game_state.vessels_owed.clear()
# Room to grow: the default cap is BASE_FLEET_SIZE (5) and the fleet
# starts at exactly 5, so without this every completed order would have
# nowhere to put its vessel and the test would be measuring the cap
# rather than the queue.
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")
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, Vector2.ZERO)
field.place(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT)
crafter_id = int(game_state.buildings[-1].id)
ships_at_start = fleet.ships.size()
bank_at_start = game_state.rocks
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.2:
return false
# --- 1 + 2. an unaffordable order queues, and costs nothing --
if not game_state.queue_order(crafter_id):
print("TEST_BUILDING_STOCK_QUEUE: FAIL (the crafter refused an order outright)")
quit(1)
return true
elapsed = 0.0
stage = 2
return false
2:
if elapsed < 0.3:
return false
if game_state.pending_orders(crafter_id) != 1:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (order did not stay queued: %d pending)" % game_state.pending_orders(crafter_id))
quit(1)
return true
if fleet.ships.size() != ships_at_start:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (a vessel appeared with an empty stock)")
quit(1)
return true
if game_state.rocks != bank_at_start:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (queueing took %d out of the bank - the building is supposed to be supplied)" % (bank_at_start - game_state.rocks))
quit(1)
return true
print("empty stock -> queued=%d ships=%d bank=%d (untouched)" % [
game_state.pending_orders(crafter_id), fleet.ships.size(), game_state.rocks
])
# --- 5. a part-delivery is not enough -----------------------
game_state.deposit_to_building(crafter_id, GameState.SHIP_COST - 1)
elapsed = 0.0
stage = 3
return false
3:
if elapsed < 0.3:
return false
if game_state.pending_orders(crafter_id) != 1 or fleet.ships.size() != ships_at_start:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (produced on a part-delivery of %d against a cost of %d)" % [
GameState.SHIP_COST - 1, GameState.SHIP_COST
])
quit(1)
return true
if game_state.building_stock(crafter_id) != GameState.SHIP_COST - 1:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (part-delivered ore was consumed anyway)")
quit(1)
return true
print("part delivery (%d of %d) -> still queued, ore intact" % [
game_state.building_stock(crafter_id), GameState.SHIP_COST
])
# --- 3. topping it up completes the order -------------------
game_state.deposit_to_building(crafter_id, 1)
elapsed = 0.0
stage = 4
return false
4:
if elapsed < 0.4:
return false
if game_state.pending_orders(crafter_id) != 0:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (order not completed once the stock arrived)")
quit(1)
return true
if fleet.ships.size() != ships_at_start + 1:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (no vessel delivered: %d -> %d)" % [
ships_at_start, fleet.ships.size()
])
quit(1)
return true
if game_state.building_stock(crafter_id) != 0:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (stock not consumed: %d left)" % game_state.building_stock(crafter_id))
quit(1)
return true
print("supplied -> ships %d->%d, stock spent" % [ships_at_start, fleet.ships.size()])
# --- 4. one delivery clears ALL the orders it covers ---------
# Measured in TICKS, not seconds, and with more orders than the
# ticks allowed: production that handled one order per frame
# would still clear them all inside any generous wall-clock wait,
# so a timed check cannot tell "cleared at once" from "dribbled
# out". Three orders, two ticks - only clearing them together
# passes.
ships_at_start = fleet.ships.size()
game_state.queue_order(crafter_id)
game_state.queue_order(crafter_id)
game_state.queue_order(crafter_id)
game_state.deposit_to_building(crafter_id, GameState.SHIP_COST * 3)
ticks = 0
stage = 5
return false
5:
ticks += 1
if ticks < 2:
return false
if game_state.pending_orders(crafter_id) != 0 or fleet.ships.size() != ships_at_start + 3:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (a delivery covering 3 orders left %d queued after %d ticks, ships %d->%d)" % [
game_state.pending_orders(crafter_id), ticks, ships_at_start, fleet.ships.size()
])
quit(1)
return true
print("one delivery cleared 3 orders in %d ticks -> ships %d->%d" % [
ticks, ships_at_start, fleet.ships.size()
])
# --- 6 + 7. no scene, no fleet: production still completes --
# The scene has to be GONE before the order is placed. With a
# fleet still alive and under its cap it claims the vessel the
# same frame production finishes, which would leave vessels_owed
# back at 0 and prove nothing about orders surviving without it.
ships_at_start = fleet.ships.size()
scene.queue_free()
elapsed = 0.0
stage = 6
return false
6:
if elapsed < 0.3:
return false
if is_instance_valid(fleet):
return false
game_state.queue_order(crafter_id)
game_state.deposit_to_building(crafter_id, GameState.SHIP_COST)
elapsed = 0.0
stage = 61
return false
61:
if elapsed < 0.4:
return false
# The queue and stock live in the autoload, so tearing the space
# scene down must not touch them - and the order must complete
# even with no Fleet node anywhere to hand the vessel to.
if game_state.pending_orders(crafter_id) != 0:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (order stalled once the space scene was gone)")
quit(1)
return true
if game_state.vessels_owed.is_empty():
print("TEST_BUILDING_STOCK_QUEUE: FAIL (vessel completed with no fleet present was lost, not owed)")
quit(1)
return true
print("no scene at all -> order completed, %d vessel owed" % game_state.vessels_owed.size())
# Come back to space: the fleet should claim what it is owed.
scene = load("res://Main.tscn").instantiate()
root.add_child(scene)
fleet = scene.get_node("Fleet")
elapsed = 0.0
stage = 8
return false
8:
if elapsed < 0.5:
return false
if not game_state.vessels_owed.is_empty():
print("TEST_BUILDING_STOCK_QUEUE: FAIL (returning fleet never claimed its %d owed vessel(s))" % game_state.vessels_owed.size())
quit(1)
return true
print("fleet returned and claimed the owed vessel (ships=%d)" % fleet.ships.size())
print("TEST_BUILDING_STOCK_QUEUE: 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()