P14-03: Price whole-shaft elevator waits in A* route choice

Riders queued for any floor pair in a shaft now delay every pair: the
elevator service publishes the rest of the shaft queue as each connector's
dynamic cost, refreshed on trip changes and each cabin minute, and restore
rejects saves whose wait disagrees with their trips. A two-shaft fixture with
real guests shows rush diversion, shared cabins within capacity, recovery,
route loss and warp-exit congestion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Takeshi
2026-10-20 15:57:28 +09:00
co-authored by Claude Opus 5.5
parent 50ceca0b7d
commit 0754f412a8
4 changed files with 246 additions and 0 deletions
+195
View File
@@ -0,0 +1,195 @@
extends SceneTree
## P14-03 route choice with real waiting: two guest shafts, real guests at real
## cells, A* switching shafts under a whole-shaft rush, shared cabins within
## capacity, recovery after cancellation, exact save of wait costs, route loss
## on demolition, and congestion priced at a warp exit.
var _checks := 0
var _failures := 0
var _serial := 0
var _clock: Variant
var _state: Variant
var _runtime: Variant
const START := Vector3i(1, 0, 0)
const GOAL := Vector3i(2, 1, 0)
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("Route wait choice (P14-03)")
_clock = load("res://autoload/game_clock.gd").new()
_state = load("res://autoload/game_state.gd").new()
_runtime = load("res://autoload/simulation_runtime.gd").new()
_clock.reset(1, 6, 0)
_state.reset()
_runtime.initialize(_clock, _state, 1403)
_clock.set_paused(true)
_layout()
_test_shaft_rush()
_test_shared_cabin()
_test_route_loss()
_test_warp_exit_congestion()
_runtime.free(); _clock.free(); _state.free()
print("%s: %d checks" % ["PASS" if _failures == 0 else "FAIL", _checks])
quit(_failures)
func _cmd(kind: StringName, payload: Dictionary) -> CommandResult:
_serial += 1
return _runtime.submit_command(kind, payload, &"player", &"", 0, _clock.absolute_minute, StringName("p1403.%d" % _serial))
func _build(id: StringName, origin: Vector3i) -> void:
var result := _cmd(&"build_facility", {&"definition_id": id, &"origin": origin})
_expect(result.is_success(), "build %s at %s %s" % [id, origin, result.message])
func _next_evaluation() -> void:
_clock.advance_minutes(10 - int(_clock.absolute_minute) % 10 + 1)
func _layout() -> void:
_expect(_cmd(&"change_resources", {&"deltas": {&"gold": 30000, &"materials": 300}}).is_success(), "fund the fixture")
# Shaft A at x0 (floors 0-3), shaft B at x4 (floors 0-1), joined on floor 1.
_build(&"guest_elevator", Vector3i(0, 0, 0))
_build(&"guest_elevator", Vector3i(4, 0, 0))
_build(&"mana_warp", Vector3i(-4, 0, 0))
_clock.advance_minutes(360)
_build(&"guest_elevator", Vector3i(0, 2, 0))
_build(&"mana_warp", Vector3i(-4, 2, 0))
for x: int in [2, 3]:
_build(&"guest_corridor", Vector3i(x, 1, 0))
_clock.advance_minutes(360)
_next_evaluation()
_expect(_runtime.elevators.get_report()[&"shafts"].size() == 2, "two independent shafts")
func _spawn(guest_id: StringName, cell: Vector3i) -> bool:
var result := _cmd(&"spawn_guest", {&"guest_id": guest_id, &"guest_type_id": &"traveler", &"options": {&"current_cell": cell}})
_expect(result.is_success(), "spawn real guest %s at %s %s" % [guest_id, cell, result.message])
return result.is_success()
func _route() -> Array:
return _runtime.path_service.find_path(START, GOAL)
func _uses_shaft_a(path: Array) -> bool:
return path.size() > 1 and path[1] == Vector3i(0, 0, 0)
func _test_shaft_rush() -> void:
var idle := _route()
var idle_cost: int = _runtime.path_service.last_path_cost
_expect(_uses_shaft_a(idle), "idle tower routes through the nearer shaft A")
# Twelve real guests on shaft A's ground stop all wait for floor 3, a
# different floor pair from the queried 0->1 trip.
var riders: Array[StringName] = []
for i: int in 12:
var guest_id := StringName("rush_%02d" % i)
if _spawn(guest_id, Vector3i(0, 0, 0)):
var trip: Dictionary = _runtime.elevators.request_trip(StringName("guest:%s" % guest_id), Vector3i(0, 0, 0), Vector3i(0, 3, 0), _clock.absolute_minute)
_expect(bool(trip.get(&"ok", false)), "real guest %s queues for floor 3" % guest_id)
riders.append(StringName("guest:%s" % guest_id))
var rush := _route()
var rush_cost: int = _runtime.path_service.last_path_cost
_expect(not _uses_shaft_a(rush) and rush.has(Vector3i(4, 0, 0)), "whole-shaft rush diverts the 0->1 trip to idle shaft B")
_expect(_runtime.path_service.last_path_cost > idle_cost, "diverted route is dearer than the idle one")
var a_connector: StringName = _runtime.path_service.get_step_connector(Vector3i(0, 0, 0), Vector3i(0, 1, 0), [])
_expect(int(_runtime.path_service.get_connector(a_connector)[&"dynamic_cost"]) == 12 * PathService.ORTHOGONAL_MOVE_COST, "shaft A 0->1 prices all 12 riders waiting elsewhere in the shaft")
var saved: Dictionary = _runtime.create_snapshot()
var restored_ok: bool = _runtime.restore_snapshot(saved.duplicate(true))
var again: Dictionary = _runtime.create_snapshot()
if restored_ok and again != saved:
for system: Variant in saved[&"systems"][&"runtime"]:
if saved[&"systems"][&"runtime"][system] != again[&"systems"][&"runtime"].get(system):
print("DIFF runtime.", system)
for key: Variant in saved:
if key != &"systems" and saved[key] != again.get(key): print("DIFF ", key)
_expect(restored_ok and again == saved, "rush wait costs and queues survive save restore exactly (restored=%s)" % restored_ok)
_expect(not _uses_shaft_a(_route()), "restored rush still diverts")
var tampered: Dictionary = saved.duplicate(true)
for row: Dictionary in tampered[&"systems"][&"runtime"][&"path"][&"connectors"]:
if row[&"id"] == a_connector: row[&"dynamic_cost"] = 0
var live: Dictionary = _runtime.create_snapshot()
_expect(not _runtime.restore_snapshot(tampered) and _runtime.create_snapshot() == live, "a save whose shaft wait disagrees with its queue is rejected atomically")
var a_route_during_rush: int = _runtime.path_service.calculate_path_cost(idle)
for actor_id: StringName in riders:
_runtime.elevators.cancel_actor(actor_id)
_expect(int(_runtime.path_service.get_connector(a_connector)[&"dynamic_cost"]) == 0, "cancelled rush clears the shaft wait")
var a_route_after: int = _runtime.path_service.calculate_path_cost(idle)
_expect(a_route_after == a_route_during_rush - 12 * PathService.ORTHOGONAL_MOVE_COST, "cleared shaft wait removes exactly the queued riders' cost from shaft A's route")
_expect(rush_cost > 0, "rush route cost recorded")
func _test_shared_cabin() -> void:
var before: Dictionary = _runtime.create_snapshot()
var riders: Array[StringName] = []
for i: int in 6:
var guest_id := StringName("share_%02d" % i)
if _spawn(guest_id, Vector3i(4, 0, 0)):
var actor_id := StringName("guest:%s" % guest_id)
_expect(bool(_runtime.elevators.request_trip(actor_id, Vector3i(4, 0, 0), Vector3i(4, 1, 0), _clock.absolute_minute).get(&"ok", false)), "rider %s queues on shaft B" % guest_id)
riders.append(actor_id)
var max_boarded := 0
var arrived := {}
for minute: int in 60:
_clock.advance_minutes(1)
var boarded := 0
for trip: Dictionary in _runtime.elevators.get_report()[&"trips"]:
if trip[&"shaft_id"] != _shaft_b() or trip[&"state"] != PathService.ELEVATOR_REQUEST_BOARDED:
continue
boarded += 1
max_boarded = maxi(max_boarded, boarded)
for actor_id: StringName in riders:
if _runtime.elevators.get_actor_trip(actor_id).is_empty():
arrived[actor_id] = true
_expect(max_boarded > 1 and max_boarded <= 4, "riders share the cabin up to its capacity of 4 (max %d)" % max_boarded)
_expect(arrived.size() == riders.size(), "all six riders are carried (none lost)")
_expect(_runtime.restore_snapshot(before), "rewind shared-cabin drill")
func _shaft_b() -> StringName:
for shaft: Dictionary in _runtime.elevators.get_report()[&"shafts"]:
if (shaft[&"stops"] as Array).front().x == 4:
return shaft[&"shaft_id"]
return &""
func _test_route_loss() -> void:
var before: Dictionary = _runtime.create_snapshot()
var shaft_b_id: StringName = &""
for facility: Dictionary in _runtime.construction_service.get_facilities():
if facility[&"definition_id"] == &"guest_elevator" and facility[&"origin"] == Vector3i(4, 0, 0):
shaft_b_id = facility[&"instance_id"]
_expect(_cmd(&"demolish_facility", {&"instance_id": shaft_b_id}).is_success(), "demolish idle shaft B")
_expect(_uses_shaft_a(_route()), "route loss falls back to the remaining shaft")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "post-loss state restores")
_expect(_runtime.restore_snapshot(before), "rewind route-loss drill")
func _test_warp_exit_congestion() -> void:
var ps: PathService = _runtime.path_service
var lower := Vector3i(-4, 0, 0)
var upper := Vector3i(-4, 2, 0)
var quiet: Array = ps.find_path(lower, upper)
var quiet_cost: int = ps.last_path_cost
_expect(quiet == [lower, upper], "idle warp is the cheapest way up")
var crowd := 0
for i: int in 6:
if _spawn(StringName("crowd_%02d" % i), upper):
crowd += 1
ps.find_path(lower, upper)
_expect(crowd > 0 and ps.last_path_cost > quiet_cost, "a crowded warp exit raises the warp route's A* cost")
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
else:
_failures += 1
push_error(" ✗ %s" % label)