extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_build_uses_fleet_hold.gd # A new game starts with an empty bank. Ore is mined into the FLEET HOLD, and # the only way to move it into the bank is to deliver it to a mothership - so # charging construction to the bank alone made the first mothership cost ore # that could not exist yet. Mine forever, build nothing: the game was # unwinnable from its own opening position. # # Construction now draws on the bank AND the hold. Covers: # A. the deadlock premise still holds - a bank-only test rejects a build # the fleet can plainly pay for, so this is guarding something real # B. the pooled test accepts it, and paying takes from the bank first and # the hold only for the shortfall # C. an unaffordable build takes nothing at all (no partial charge) # D. end to end: a fleet holding ore, with an empty bank, actually gets a # mothership built through BuildController # E. research is deliberately NOT pooled - it still spends from the bank # Deliberately more than one starting interceptor can carry (its hold is 20, # which is exactly MOTHERSHIP_COST). The capacity is widened where this is # used, so "the hold paid for it" is visible as a remainder rather than as # the hold happening to empty to zero. const HOLD_ORE: int = 40 var game_state: Node var scene: Node var build_controller: Node var stage: int = 0 var elapsed: float = 0.0 func _initialize() -> void: game_state = root.get_node("GameState") game_state.reset_progression() game_state.buildings.clear() game_state.extractors.clear() var cost: int = GameState.MOTHERSHIP_COST # --- A. the deadlock, exactly as a fresh game presents it --------------- game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0) game_state.fleet_inventory.total_capacity = HOLD_ORE game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, HOLD_ORE) if game_state.can_afford(cost): _fail("the bank-only test passed with an empty bank, so it can no longer show the deadlock this fixes") return print("bank 0 + hold %d: bank-only affordability says no (the original deadlock)" % HOLD_ORE) # --- B. pooled, and paid bank-first ------------------------------------- if not game_state.can_afford_construction(cost): _fail("a fleet holding %d ore still could not afford a %d build" % [HOLD_ORE, cost]) return if not game_state.spend_construction(cost): _fail("spend_construction refused a build it had just called affordable") return if game_state.bank_inventory.amount(GameState.RESOURCE_ORE) != 0: _fail("paying from an empty bank somehow changed it") return if game_state.fleet_inventory.amount(GameState.RESOURCE_ORE) != HOLD_ORE - cost: _fail("the hold should have paid the whole %d, but holds %d of %d" \ % [cost, game_state.fleet_inventory.amount(GameState.RESOURCE_ORE), HOLD_ORE]) return print("paid %d entirely from the hold: hold %d -> %d" % [cost, HOLD_ORE, HOLD_ORE - cost]) # Bank first when there is a bank to draw on: the hold is also the fleet's # mining capacity, so it is spent last. game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 8) game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 30) if not game_state.spend_construction(20): _fail("a 20 build was refused with 8 banked and 30 held") return if game_state.bank_inventory.amount(GameState.RESOURCE_ORE) != 0 \ or game_state.fleet_inventory.amount(GameState.RESOURCE_ORE) != 18: _fail("a split payment should empty the bank first: bank %d, hold %d (expected 0 and 18)" \ % [game_state.bank_inventory.amount(GameState.RESOURCE_ORE), game_state.fleet_inventory.amount(GameState.RESOURCE_ORE)]) return print("split payment drained the bank first: bank 8 -> 0, hold 30 -> 18") # --- C. no partial charge ----------------------------------------------- game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 5) game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 5) if game_state.spend_construction(50): _fail("a build that cost more than everything owned was allowed") return if game_state.bank_inventory.amount(GameState.RESOURCE_ORE) != 5 \ or game_state.fleet_inventory.amount(GameState.RESOURCE_ORE) != 5: _fail("a rejected build still took ore: bank %d, hold %d (expected 5 and 5)" \ % [game_state.bank_inventory.amount(GameState.RESOURCE_ORE), game_state.fleet_inventory.amount(GameState.RESOURCE_ORE)]) return print("a build nobody can afford takes nothing") # --- E. research stays banked ------------------------------------------- game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0) game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 500) if game_state.can_afford(1): _fail("research affordability started counting the fleet hold - it is meant to stay banked") return print("research still spends from the bank alone") # --- D. end to end, through the real build path -------------------------- game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0) game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, HOLD_ORE) scene = load("res://Main.tscn").instantiate() root.add_child(scene) current_scene = scene func _process(delta: float) -> bool: elapsed += delta match stage: 0: if elapsed < 0.3: return false scene.get_node("AsteroidField").set_process(false) scene.get_node("EnemySpawner").set_process(false) build_controller = scene.get_node("BuildController") # Whatever else the scene did while starting up, the point of the # test is a fleet with ore and an empty bank. game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0) # Widened past what a starting interceptor can carry. Its real # hold is 20 and a mothership costs exactly 20, so at the stock # capacity a successful build empties the hold to zero - which is # indistinguishable from the hold having been emptied by anything # else. With room to spare, the remainder is the evidence. game_state.fleet_inventory.total_capacity = HOLD_ORE game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, HOLD_ORE) var before: int = game_state.buildings.size() build_controller.pending_build_type = GameState.BUILDING_MOTHERSHIP build_controller._confirm_build() if game_state.buildings.size() != before + 1: return _fail("building a mothership from the fleet's own hold did not place one (%d -> %d): %s" \ % [before, game_state.buildings.size(), build_controller.build_status.text]) # Checked as a TOTAL, not as the hold alone: a mothership standing # where the fleet is starts taking delivery of the hold on the # very next tick, so ore legitimately moves from hold to bank # straight after the build. What must be true is that exactly the # cost left the pool - and that it left, since the bank was empty # and could not have paid. var total: int = game_state.construction_ore() if total != HOLD_ORE - GameState.MOTHERSHIP_COST: return _fail("the mothership was placed but %d ore remains, expected %d" \ % [total, HOLD_ORE - GameState.MOTHERSHIP_COST]) print("built a mothership with an empty bank, paid out of the fleet hold (%d ore left, bank %d + hold %d)" \ % [total, game_state.bank_inventory.amount(GameState.RESOURCE_ORE), game_state.fleet_inventory.amount(GameState.RESOURCE_ORE)]) print("TEST_BUILD_USES_FLEET_HOLD: PASS") quit(0) return true return false func _fail(message: String) -> bool: print("TEST_BUILD_USES_FLEET_HOLD: FAIL (%s)" % message) quit(1) return true