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.ships_owed = 0 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()