217 lines
8.2 KiB
GDScript
217 lines
8.2 KiB
GDScript
extends SceneTree
|
|
|
|
# Headless gameplay test: run with
|
|
# godot --headless -s res://tests/test_transporter_supply.gd
|
|
# Forgejo #19 (part 6 of #12): transporters stationed at a hub fetch ore from
|
|
# the mothership and deliver it to whichever building is waiting on it, with
|
|
# no player input at all.
|
|
#
|
|
# Verifies:
|
|
# 1. a transporter is built at the CRAFTER and takes itself to the nearest
|
|
# hub, rather than appearing at the hub out of nowhere
|
|
# 2. a queued order the crafter cannot pay for is fulfilled end to end
|
|
# automatically - the whole point of the ticket
|
|
# 3. ore is conserved across the entire route (bank + hold + every
|
|
# building's stock), at every step, not just at the end
|
|
# 4. ore leaves the bank when it is LOADED, not when the order is placed -
|
|
# the vessel is carrying something real
|
|
# 5. transporters are not ordered when there is no hub to station them at
|
|
# 6. no unbounded node accumulation: the fleet of transporters is exactly
|
|
# what was built, however many runs they make
|
|
|
|
var game_state: Node
|
|
var scene: Node
|
|
var fleet: Node
|
|
var field: Node
|
|
var crafter_id: int = 0
|
|
var hub_id: int = 0
|
|
var stage: int = 0
|
|
var elapsed: float = 0.0
|
|
var total_at_start: int = 0
|
|
var bank_when_ordered: int = 0
|
|
var conservation_broken: bool = false
|
|
# Ore legitimately consumed by completed production. Conservation means
|
|
# "nothing vanishes in TRANSIT", not "the total never falls" - building a
|
|
# vessel spends ore, and that is the one way it is allowed to leave.
|
|
var consumed: int = 0
|
|
|
|
const MOTHERSHIP_AT: Vector2 = Vector2(0.0, 0.0)
|
|
const HUB_AT: Vector2 = Vector2(600.0, 200.0)
|
|
const CRAFTER_AT: Vector2 = Vector2(900.0, -300.0)
|
|
|
|
|
|
func _total() -> int:
|
|
var sum: int = game_state.rocks + game_state.fleet_cargo
|
|
for b in game_state.buildings:
|
|
if b.type != GameState.BUILDING_MOTHERSHIP:
|
|
sum += game_state.building_resource(int(b.id), GameState.RESOURCE_ORE)
|
|
for t in game_state.transporters:
|
|
sum += int(t.get("carrying", 0))
|
|
# Ore inside a build that has STARTED but not finished (#23). It has left
|
|
# the building's stock and has not yet been consumed by a completed
|
|
# vessel, so without counting it the total dips for the length of every
|
|
# build - which is a gap in this sum, not a leak in the game.
|
|
for b in game_state.buildings:
|
|
for o in b.get("orders", []):
|
|
if bool(o.started):
|
|
sum += game_state.product_cost(String(o.product))
|
|
return sum
|
|
|
|
|
|
func _initialize() -> void:
|
|
game_state = root.get_node("GameState")
|
|
game_state.buildings.clear()
|
|
game_state.extractors.clear()
|
|
game_state.transporters.clear()
|
|
game_state.rocks = 200
|
|
game_state.fleet_cargo = 0
|
|
game_state.stored_vessels.clear()
|
|
game_state.build_time_scale = 0.02
|
|
game_state.unlocked = ["shipyards", "logistics", "freight", "coordination", "wing", "squadron", "armada"]
|
|
scene = load("res://Main.tscn").instantiate()
|
|
root.add_child(scene)
|
|
fleet = scene.get_node("Fleet")
|
|
field = scene.get_node("BuildingField")
|
|
game_state.order_completed.connect(_on_order_completed)
|
|
|
|
|
|
func _on_order_completed(_building_id: int, product: String) -> void:
|
|
consumed += game_state.product_cost(product)
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
elapsed += delta
|
|
|
|
# --- 3. conservation, checked EVERY tick ------------------------------
|
|
# Ore in flight is the easy thing to lose: a leg that loads without
|
|
# debiting, or delivers without crediting, would show up here and nowhere
|
|
# else. Checked continuously rather than at the end, so it cannot be
|
|
# masked by a later correction.
|
|
if stage >= 2 and _total() + consumed != total_at_start:
|
|
conservation_broken = true
|
|
|
|
match stage:
|
|
0:
|
|
scene.get_node("AsteroidField").set_process(false)
|
|
scene.get_node("EnemySpawner").set_process(false)
|
|
for a in get_nodes_in_group("asteroids"):
|
|
if is_instance_valid(a):
|
|
a.queue_free()
|
|
|
|
field.place(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
|
field.place(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT)
|
|
crafter_id = int(game_state.buildings[-1].id)
|
|
|
|
# --- 5. no hub yet: a transporter cannot be ordered ----------
|
|
if game_state.queue_order(crafter_id, GameState.PRODUCT_TRANSPORTER):
|
|
print("TEST_TRANSPORTER_SUPPLY: FAIL (ordered a transporter with no hub to station it at)")
|
|
quit(1)
|
|
return true
|
|
print("no hub -> transporter order refused")
|
|
|
|
field.place(GameState.BUILDING_TRANSPORT_HUB, HUB_AT)
|
|
hub_id = int(game_state.buildings[-1].id)
|
|
elapsed = 0.0
|
|
stage = 1
|
|
return false
|
|
1:
|
|
if elapsed < 0.2:
|
|
return false
|
|
# --- 1. build a transporter at the crafter ------------------
|
|
if not game_state.queue_order(crafter_id, GameState.PRODUCT_TRANSPORTER):
|
|
print("TEST_TRANSPORTER_SUPPLY: FAIL (could not order a transporter with a hub built)")
|
|
quit(1)
|
|
return true
|
|
game_state.deposit_to_building(crafter_id, GameState.TRANSPORTER_COST)
|
|
elapsed = 0.0
|
|
stage = 2
|
|
total_at_start = _total()
|
|
return false
|
|
2:
|
|
if elapsed < game_state.build_time(GameState.PRODUCT_TRANSPORTER) * 3.0:
|
|
return false
|
|
if game_state.transporters.size() != 1:
|
|
print("TEST_TRANSPORTER_SUPPLY: FAIL (built %d transporters from one order)" % game_state.transporters.size())
|
|
quit(1)
|
|
return true
|
|
var t: Dictionary = game_state.transporters[0]
|
|
if int(t.hub_id) != hub_id:
|
|
print("TEST_TRANSPORTER_SUPPLY: FAIL (transporter did not take the nearest hub)")
|
|
quit(1)
|
|
return true
|
|
# It must fly FROM the crafter TO the hub, not appear at the hub.
|
|
# Checked on the leg's endpoints rather than its current position,
|
|
# which is already partway there by now - and being partway is
|
|
# the correct behaviour.
|
|
if (t.from as Vector2).distance_to(CRAFTER_AT) > 1.0:
|
|
print("TEST_TRANSPORTER_SUPPLY: FAIL (transporter's first leg starts at %s, not at the crafter that built it)" % t.from)
|
|
quit(1)
|
|
return true
|
|
if (t.to as Vector2).distance_to(HUB_AT) > 1.0:
|
|
print("TEST_TRANSPORTER_SUPPLY: FAIL (transporter's first leg does not end at the hub: %s)" % t.to)
|
|
quit(1)
|
|
return true
|
|
print("transporter built at the crafter, heading for hub %d" % hub_id)
|
|
elapsed = 0.0
|
|
stage = 3
|
|
return false
|
|
3:
|
|
# Give it time to reach the hub and go idle.
|
|
if elapsed < 4.0:
|
|
return false
|
|
if game_state.transporters[0].state != GameState.TRANSPORTER_IDLE:
|
|
print("TEST_TRANSPORTER_SUPPLY: FAIL (never settled at its hub: state=%s)" % game_state.transporters[0].state)
|
|
quit(1)
|
|
return true
|
|
print("stationed at the hub, idle")
|
|
|
|
# --- 2 + 4. an order it cannot pay for ----------------------
|
|
game_state.queue_order(crafter_id, GameState.PRODUCT_SHIP)
|
|
bank_when_ordered = game_state.rocks
|
|
elapsed = 0.0
|
|
stage = 4
|
|
return false
|
|
4:
|
|
# One tick: placing the order must not touch the bank.
|
|
if elapsed < 0.1:
|
|
return false
|
|
if game_state.rocks != bank_when_ordered:
|
|
print("TEST_TRANSPORTER_SUPPLY: FAIL (bank moved on placing the order, before anything was loaded)")
|
|
quit(1)
|
|
return true
|
|
print("order placed, bank untouched at %d" % game_state.rocks)
|
|
elapsed = 0.0
|
|
stage = 5
|
|
return false
|
|
5:
|
|
# --- 2. the whole run, with no player input -----------------
|
|
if game_state.pending_orders(crafter_id) > 0:
|
|
if elapsed > 25.0:
|
|
print("TEST_TRANSPORTER_SUPPLY: FAIL (order never fulfilled: transporter state=%s carrying=%d crafter stock=%d)" % [
|
|
game_state.transporters[0].state,
|
|
int(game_state.transporters[0].carrying),
|
|
game_state.building_stock(crafter_id),
|
|
])
|
|
quit(1)
|
|
return true
|
|
return false
|
|
if conservation_broken:
|
|
print("TEST_TRANSPORTER_SUPPLY: FAIL (ore created or destroyed somewhere along the route)")
|
|
quit(1)
|
|
return true
|
|
print("order fulfilled automatically in %.1fs, no player input" % elapsed)
|
|
|
|
# --- 6. no vessel accumulation ------------------------------
|
|
if game_state.transporters.size() != 1:
|
|
print("TEST_TRANSPORTER_SUPPLY: FAIL (%d transporters exist after the run, one was built)" % game_state.transporters.size())
|
|
quit(1)
|
|
return true
|
|
print("still exactly 1 transporter after a full round trip")
|
|
print("bank=%d crafter stock=%d total=%d + %d consumed = %d (started %d)" % [
|
|
game_state.rocks, game_state.building_stock(crafter_id), _total(),
|
|
consumed, _total() + consumed, total_at_start
|
|
])
|
|
print("TEST_TRANSPORTER_SUPPLY: PASS")
|
|
quit(0)
|
|
return true
|
|
return false
|