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.ships_owed = 0 # 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 ships_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.ships_owed < 1: 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.ships_owed) # 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 game_state.ships_owed != 0: print("TEST_BUILDING_STOCK_QUEUE: FAIL (returning fleet never claimed its %d owed vessel(s))" % game_state.ships_owed) 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()