Provision transporters with hubs

This commit is contained in:
Loïc Wisniewski 2026-08-15 00:09:10 +02:00
commit d0a9f5b971
21 changed files with 309 additions and 588 deletions

View file

@ -0,0 +1,23 @@
extends Node
# Visual-only phone preview for the combat-fleet hangar. Logistics
# transporters deliberately have no card here.
func _ready() -> void:
GameState.buildings.clear()
GameState.extractors.clear()
GameState.transporters.clear()
GameState.stored_vessels = [
GameState.VESSEL_INTERCEPTOR,
GameState.VESSEL_SHIELD, GameState.VESSEL_SHIELD,
]
var main: Node = load("res://Main.tscn").instantiate()
add_child(main)
main.get_node("AsteroidField").set_process(false)
main.get_node("EnemySpawner").set_process(false)
main.get_node("Fleet").spawn_ships(3, Vector2.ZERO, PackedStringArray([
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_INTERCEPTOR,
GameState.VESSEL_SHIELD,
]))
main.get_node("BuildController")._on_hangar_pressed()

View file

@ -0,0 +1 @@
uid://grp4nm3xvngn

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/_hangar_menu_preview.gd" id="1"]
[node name="HangarMenuPreview" type="Node"]
script = ExtResource("1")

View file

@ -15,17 +15,20 @@ func _ready() -> void:
GameState.add_building(GameState.BUILDING_MOTHERSHIP, Vector2(-420.0, 0.0))
GameState.add_building(GameState.BUILDING_TRANSPORT_HUB, Vector2(420.0, 0.0))
# One laden, mid-leg, and one running home empty, so both states show.
GameState.transporters.append({
"hub_id": 2, "state": GameState.TRANSPORTER_TO_BUILDING, "target_id": 2,
"carrying": 8, "progress": 0.45,
"from": Vector2(-420.0, 0.0), "to": Vector2(420.0, 0.0),
})
GameState.transporters.append({
"hub_id": 2, "state": GameState.TRANSPORTER_RETURNING, "target_id": 0,
"carrying": 0, "progress": 0.5,
"from": Vector2(-380.0, 150.0), "to": Vector2(420.0, 150.0),
})
# The hub already provisioned these two. Put one laden mid-leg and one on
# its way home empty so both visual states show.
var laden: Dictionary = GameState.transporters[0]
laden.state = GameState.TRANSPORTER_TO_BUILDING
laden.target_id = 2
laden.carrying = 8
laden.progress = 0.45
laden.from = Vector2(-420.0, 0.0)
laden.to = Vector2(420.0, 0.0)
var returning: Dictionary = GameState.transporters[1]
returning.state = GameState.TRANSPORTER_RETURNING
returning.progress = 0.5
returning.from = Vector2(-380.0, 150.0)
returning.to = Vector2(420.0, 150.0)
var main: Node = load("res://Main.tscn").instantiate()
add_child(main)

View file

@ -39,12 +39,10 @@ func _process(delta: float) -> bool:
print("extractor mined locally: stock=%d bank=%d" % [
game_state.extractor_stock(extractor_index), game_state.rocks])
game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
var hub_id: int = game_state.add_building(GameState.BUILDING_TRANSPORT_HUB, HUB_AT)
# A manually seeded idle transporter is enough to exercise the same
# simulation used by a produced one.
game_state.transporters.append({"hub_id": hub_id, "state": GameState.TRANSPORTER_IDLE,
"target_id": 0, "extractor_index": -1, "carrying": 0, "progress": 1.0,
"from": HUB_AT, "to": HUB_AT})
game_state.add_building(GameState.BUILDING_TRANSPORT_HUB, HUB_AT)
if game_state.transporters.size() != game_state.hub_capacity():
_fail("hub did not provision its transporters")
return true
base_load = game_state.transporter_load_limit()
game_state.extractors[extractor_index].stock = base_load - 1
elapsed = 0.0

View file

@ -86,13 +86,19 @@ func _process(delta: float) -> bool:
game_state.add_building(GameState.BUILDING_TRANSPORT_HUB, Vector2(100, 0))
game_state.add_extractor("Aurelia", Vector2(400, 0), "gas")
game_state.extractors[0].stock = game_state.transporter_load_limit()
game_state.transporters.append({"hub_id": 2, "state": GameState.TRANSPORTER_TO_EXTRACTOR,
"target_id": 0, "extractor_index": 0, "carrying": 0, "resource_type": "gas",
"progress": 1.0, "from": Vector2(100, 0), "to": Vector2(400, 0)})
game_state._arrive(game_state.transporters[0])
var transporter: Dictionary = game_state.transporters[0]
transporter.state = GameState.TRANSPORTER_TO_EXTRACTOR
transporter.target_id = 0
transporter.extractor_index = 0
transporter.carrying = 0
transporter.resource_type = "gas"
transporter.progress = 1.0
transporter.from = Vector2(100, 0)
transporter.to = Vector2(400, 0)
game_state._arrive(transporter)
if game_state.transporters[0].state != GameState.TRANSPORTER_TO_MOTHERSHIP_FROM_EXTRACTOR:
return _fail("transporter did not collect gas from the extractor")
game_state._arrive(game_state.transporters[0])
game_state._arrive(transporter)
if game_state.gas != game_state.transporter_load_limit() or game_state.rocks != 0:
return _fail("gas delivery banked gas=%d ore=%d" % [game_state.gas, game_state.rocks])
var mothership_id: int = int(game_state.buildings[0].id)

View file

@ -35,7 +35,8 @@ func _process(delta: float) -> bool:
a.queue_free()
fleet.spawn_ships(1, Vector2.ZERO)
fleet.add_ship(GameState.VESSEL_SHIELD)
game_state.stored_vessels = [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD]
game_state.stored_vessels = [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
GameState.VESSEL_TRANSPORTER]
controller._on_hangar_pressed()
elapsed = 0.0
stage = 1
@ -48,6 +49,10 @@ func _process(delta: float) -> bool:
if "Interceptor" not in available_label.text or "1 available" not in available_label.text:
_fail("available interceptor card says '%s'" % available_label.text)
return true
if controller._hangar_types().has(GameState.VESSEL_TRANSPORTER) \
or fleet.add_ship(GameState.VESSEL_TRANSPORTER):
_fail("a logistics transporter was exposed as a fleet vessel")
return true
controller._on_hangar_crew_type(0)
if fleet.ships.size() != 3 or game_state.stored_vessels.has(GameState.VESSEL_INTERCEPTOR):
_fail("tapping an available interceptor did not crew it")

View file

@ -1,144 +1,56 @@
extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_hub_capacity.gd
# Forgejo #20 (part 7 of #12): a hub stations a limited number of
# transporters. More hubs, or research, raise it.
#
# Verifies:
# 1. capacity is per hub, so a second hub raises the total
# 2. ordering past the cap is REFUSED, not silently swallowed - the ticket
# is explicit that the player has to be told
# 3. a queued-but-unbuilt transporter still holds its berth, or ten could
# be ordered against two and only fail on arrival
# 4. researching Freight Yards raises capacity for hubs ALREADY BUILT, not
# just ones placed afterwards
# 5. the extra berth is immediately usable
var game_state: Node
var scene: Node
var field: Node
var crafter_id: int = 0
var stage: int = 0
var elapsed: float = 0.0
const CRAFTER_AT: Vector2 = Vector2(400.0, 0.0)
const HUB_A: Vector2 = Vector2(-500.0, 0.0)
const HUB_B: Vector2 = Vector2(-500.0, 600.0)
# A transport hub owns its logistics vessels directly. Building a hub
# provisions every researched slot immediately; Freight Yards provisions its
# extra slot on hubs that already exist.
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.transporters.clear()
game_state.rocks = 900
game_state.fleet_cargo = 0
game_state.stored_vessels.clear()
game_state.build_time_scale = 0.02
game_state.unlocked = ["shipyards", "logistics", "freight"]
scene = load("res://Main.tscn").instantiate()
root.add_child(scene)
field = scene.get_node("BuildingField")
var state: Node = root.get_node("GameState")
state.buildings.clear()
state.extractors.clear()
state.transporters.clear()
state.reset_progression()
state.rocks = 900
state.unlocked = ["shipyards", "logistics", "freight"]
var hub_a: int = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(-500, 0))
var per_hub: int = state.hub_capacity()
if state.transporters.size() != per_hub or _at_hub(state, hub_a) != per_hub:
return _fail("first hub did not immediately provision %d transporters" % per_hub)
print("first hub provisioned %d transporters" % per_hub)
var hub_b: int = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(-500, 600))
if state.transporters.size() != per_hub * 2 or _at_hub(state, hub_b) != per_hub:
return _fail("second hub did not bring its own transporters")
print("second hub raised logistics fleet to %d" % state.transporters.size())
var before: int = state.transporters.size()
if not state.unlock("freight_yards"):
return _fail("could not research Freight Yards")
var expected: int = state.hub_capacity() * state.hub_count()
if state.transporters.size() != expected or state.transporters.size() != before + 2:
return _fail("Freight Yards did not provision one transporter per existing hub")
if _at_hub(state, hub_a) != state.hub_capacity() \
or _at_hub(state, hub_b) != state.hub_capacity():
return _fail("researched transporters were assigned to the wrong hubs")
print("Freight Yards provisioned existing hubs: %d -> %d" % [before, expected])
if state.remove_buildings_of_type(state.BUILDING_TRANSPORT_HUB) != 2 \
or not state.transporters.is_empty():
return _fail("removing hubs left orphaned logistics transporters")
print("TEST_HUB_CAPACITY: PASS")
quit(0)
func _fill_berths(n: int) -> void:
# Queue and immediately fund n transporters.
for i in range(n):
if game_state.queue_order(crafter_id, GameState.PRODUCT_TRANSPORTER):
game_state.deposit_to_building(crafter_id, GameState.TRANSPORTER_COST)
func _at_hub(state: Node, hub_id: int) -> int:
var count: int = 0
for transporter in state.transporters:
if int(transporter.hub_id) == hub_id:
count += 1
return count
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
field.place(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
field.place(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT)
crafter_id = int(game_state.buildings[-1].id)
field.place(GameState.BUILDING_TRANSPORT_HUB, HUB_A)
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.3:
return false
var per_hub: int = game_state.hub_capacity()
if game_state.transport_capacity() != per_hub:
print("TEST_HUB_CAPACITY: FAIL (one hub gives %d berths, per-hub capacity is %d)" % [
game_state.transport_capacity(), per_hub
])
quit(1)
return true
print("1 hub -> %d berths (%d per hub)" % [game_state.transport_capacity(), per_hub])
# --- 2 + 3. fill it, then try to exceed it ------------------
_fill_berths(per_hub)
if game_state.transporters_claimed() != per_hub:
print("TEST_HUB_CAPACITY: FAIL (claimed %d berths after ordering %d)" % [
game_state.transporters_claimed(), per_hub
])
quit(1)
return true
# Still only QUEUED at this point - the berth must already count.
if game_state.queue_order(crafter_id, GameState.PRODUCT_TRANSPORTER):
print("TEST_HUB_CAPACITY: FAIL (queued a transporter past the %d-berth cap)" % per_hub)
quit(1)
return true
print("cap enforced at %d, including orders not yet built" % per_hub)
# --- 1. a second hub raises the total -----------------------
field.place(GameState.BUILDING_TRANSPORT_HUB, HUB_B)
elapsed = 0.0
stage = 2
return false
2:
if elapsed < 0.3:
return false
if game_state.transport_capacity() != game_state.hub_capacity() * 2:
print("TEST_HUB_CAPACITY: FAIL (2 hubs give %d berths, expected %d)" % [
game_state.transport_capacity(), game_state.hub_capacity() * 2
])
quit(1)
return true
print("2 hubs -> %d berths" % game_state.transport_capacity())
if not game_state.can_station_transporter():
print("TEST_HUB_CAPACITY: FAIL (second hub did not free a berth)")
quit(1)
return true
# Fill the second hub too, so capacity is exhausted again.
_fill_berths(game_state.transport_capacity() - game_state.transporters_claimed())
if game_state.can_station_transporter():
print("TEST_HUB_CAPACITY: FAIL (still room after filling every berth)")
quit(1)
return true
# --- 4. research raises it for hubs ALREADY built -----------
var before: int = game_state.transport_capacity()
game_state.unlocked.append("freight_yards")
var after: int = game_state.transport_capacity()
print("researched Freight Yards: %d -> %d berths across %d existing hubs" % [
before, after, game_state.hub_count()
])
if after != before + game_state.hub_count():
print("TEST_HUB_CAPACITY: FAIL (research gave %d berths, expected +1 per existing hub)" % (after - before))
quit(1)
return true
# --- 5. and the new berth is usable straight away -----------
if not game_state.can_station_transporter():
print("TEST_HUB_CAPACITY: FAIL (research raised the number but left no usable berth)")
quit(1)
return true
if not game_state.queue_order(crafter_id, GameState.PRODUCT_TRANSPORTER):
print("TEST_HUB_CAPACITY: FAIL (could not order into the berth research just added)")
quit(1)
return true
print("ordered into the newly researched berth")
print("TEST_HUB_CAPACITY: PASS")
quit(0)
return true
return false
func _fail(message: String) -> void:
print("TEST_HUB_CAPACITY: FAIL (%s)" % message)
quit(1)

View file

@ -14,11 +14,8 @@ func _initialize() -> void:
state.rocks = 100
state.build_time_scale = 0.02
state.add_building(state.BUILDING_MOTHERSHIP, Vector2.ZERO)
var hub_id: int = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(50, 0))
state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(50, 0))
lab_id = state.add_building(state.BUILDING_SHIP_LAB, Vector2(100, 0))
state.transporters.append({"hub_id": hub_id, "state": state.TRANSPORTER_IDLE,
"target_id": 0, "extractor_index": -1, "carrying": 0, "progress": 1.0,
"from": Vector2(50, 0), "to": Vector2(50, 0)})
if not state.queue_lab_research(lab_id, "int_damage"):
return _fail("could not queue lab research")
if state.outstanding_cost(lab_id) != state.upgrade_cost("int_damage"):

View file

@ -28,7 +28,7 @@ var returned_vessel_types: PackedStringArray = PackedStringArray()
var game_state: Node
var expected_vessel_types: PackedStringArray = PackedStringArray([
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
GameState.VESSEL_TRANSPORTER, GameState.VESSEL_SHIELD,
GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
GameState.VESSEL_INTERCEPTOR,
])

View file

@ -1,217 +1,89 @@
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
# Hub-owned transporters supply an unfunded combat-vessel order without ever
# being crafted, stored at the mothership, or added to the player fleet.
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 state: Node
var crafter_id: int
var initial_transporters: int
var total_at_start: int
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
var elapsed: float = 0.0
var conservation_broken: bool = false
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)
state = root.get_node("GameState")
state.buildings.clear()
state.extractors.clear()
state.transporters.clear()
state.stored_vessels.clear()
state.vessel_deliveries.clear()
state.reset_progression()
state.rocks = 200
state.build_time_scale = 0.02
state.unlocked = ["shipyards", "logistics", "freight"]
state.add_building(state.BUILDING_MOTHERSHIP, Vector2.ZERO)
crafter_id = state.add_building(state.BUILDING_SHIP_CRAFTER, Vector2(900, -300))
if state.queue_order(crafter_id, state.VESSEL_TRANSPORTER):
return _fail("the crafter accepted a logistics transporter order")
var hub_id: int = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(600, 200))
initial_transporters = state.hub_capacity()
if state.transporters.size() != initial_transporters:
return _fail("hub provisioned %d transporters, expected %d" % [
state.transporters.size(), initial_transporters])
for transporter in state.transporters:
if int(transporter.hub_id) != hub_id or transporter.state != state.TRANSPORTER_IDLE:
return _fail("a provisioned transporter was not idle at its hub")
func _on_order_completed(_building_id: int, product: String) -> void:
consumed += game_state.product_cost(product)
var bank_before: int = state.rocks
if not state.queue_order(crafter_id, state.PRODUCT_SHIP):
return _fail("crafter refused the combat-vessel order")
if state.rocks != bank_before:
return _fail("placing the order changed the bank before a transporter loaded")
state.order_completed.connect(_on_order_completed)
total_at_start = _total()
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:
if _total() + consumed != total_at_start:
conservation_broken = true
if state.pending_orders(crafter_id) > 0:
if elapsed > 25.0:
return _fail("hub transporters never supplied the queued order")
return false
if conservation_broken:
return _fail("ore was created or destroyed on the automatic route")
if state.transporters.size() != initial_transporters:
return _fail("transporters accumulated during delivery")
if state.stored_vessels.has(state.VESSEL_TRANSPORTER):
return _fail("a transporter appeared in the mothership hangar")
print("%d hub-owned transporters supplied the crafter automatically" % initial_transporters)
print("TEST_TRANSPORTER_SUPPLY: PASS")
quit(0)
return 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)
func _on_order_completed(_building_id: int, product: String) -> void:
consumed += state.product_cost(product)
# --- 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")
func _total() -> int:
var sum: int = state.rocks + state.fleet_cargo
for building in state.buildings:
if building.type != state.BUILDING_MOTHERSHIP:
sum += state.building_resource(int(building.id), state.RESOURCE_ORE)
for order in building.get("orders", []):
if bool(order.started):
sum += state.product_cost(String(order.product))
for transporter in state.transporters:
sum += int(transporter.get("carrying", 0))
return sum
# --- 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
func _fail(message: String) -> bool:
print("TEST_TRANSPORTER_SUPPLY: FAIL (%s)" % message)
quit(1)
return true

View file

@ -22,6 +22,10 @@ func _process(delta: float) -> bool:
if elapsed < 0.1:
return false
var definitions: Array = VESSEL_DEFINITIONS.all()
var fleet_definitions: Array = VESSEL_DEFINITIONS.fleet()
if fleet_definitions.size() != 2 \
or fleet_definitions.any(func(definition): return definition.has_capability("transport")):
return _fail("logistics transporters leaked into the combat-fleet catalog")
if ships.size() != definitions.size():
return _fail("catalog and composed fleet differ in size")
for i in range(definitions.size()):