Provision transporters with hubs
This commit is contained in:
parent
663648d949
commit
d0a9f5b971
21 changed files with 309 additions and 588 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue