P14-03: Versioned shaft-wait saves, keep riders aboard, real-actor proof

- ElevatorService snapshot v2 carries the shaft-wide wait; v1 saves (written
  with every elevator dynamic_cost 0) migrate by deriving it on load, and any
  v2 mismatch, including all zeros, is rejected atomically. A genuine save
  from 50ceca0 is kept as tests/fixtures/p14-03/legacy_rush_v1.
- A guest aboard a cabin no longer decides, leaves or despawns until the
  elevator sets it down at a real landing; previously a rider told to leave
  vanished from a moving cabin.
- test_route_wait_choice now proves the rush detour and route loss with a
  worker who actually walks, every shared rider's arrival cell, service
  elevator and stair congestion, legacy migration and a new-PID restart.
- Register completion markers for the seven suites added this session and
  bring the P14 ledger in line with P14-02's completion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Takeshi
2026-10-20 16:51:41 +09:00
co-authored by Claude Opus 5.5
parent 62d1dcecad
commit a8edd3c3a1
15 changed files with 562 additions and 113 deletions
+30
View File
@@ -0,0 +1,30 @@
# P14-03 旧形式セーブの fixture
`legacy_rush_v1/` は、P14-03 より前の本物のセーブパッケージです。待ち時間を shaft 全体で数えるようにする前の版で書き出しています(ElevatorService snapshot v1、Runtime schema 14)。
- 籠Aの1階で12人の客が3階行きを待っています。
- 昇降機の接続はすべて `dynamic_cost` が0です。
## 生成方法(2026-10-20)
1. 作業ツリーの次の2ファイルを、一時的に `50ceca0` 時点の版へ差し替えました。
- `scripts/transport/elevator_service.gd`
- `scripts/world/path_service.gd`
2. `legacy_rush_writer.gd.txt` を `LEGACY_OUT=user://tests/p14_03_legacy_rush` として実行しました(headless)。出力は次のとおりです。
- `LEGACY dynamic costs [0, 0, 0, 0, 0, 0, 0]`
- `SAVE true`
3. 2ファイルを元に戻しました。git の blob が HEAD と一致することを確認済みです。
- elevator: 574ba4f3…
- path: 5cc9516f…
## SHA256
```
978fd98f250ae5a9d3249f22adc3dcf032007dde92ddd10d556e050dd87616d8 journal.log
80c251341bd3f2a6c4a659f30e477dd856c9fec7961d5b2dfd98d7e9eb5006ab manifest.json
0ef55674eca98b6748ae5ccca85a14b042c1545a143810c9c47b487119895c32 thumbnail.png
26171ed8994982ed13fe038d225522044f1049070be53d76a4ed5572a227257d world.bin
```
`tests/transport/test_route_wait_choice.gd` が確認する内容は次のとおりです。
- この保存は読み込みに成功し、待ち時間が再計算されて v2 として保存されます。
- v1 には現れ得ない非0の待ち費用を含む保存は、原子的に拒否されます。
+1
View File
@@ -0,0 +1 @@
save_completed 2026-10-20T07:38:59
+15
View File
@@ -0,0 +1,15 @@
{
"absolute_minute": 1091,
"checksum_world": "baa5cb8d4eb3ed651934cca0bc1f26f22fd345dc9886a72ca383c3722438ffb9",
"content_pack_ids": [],
"created_at_real": "2026-10-20T07:38:59",
"day": 1,
"display_name": "p14_03_legacy_rush",
"enabled_filter_profile": "default",
"format_version": 1,
"game_version": "0.1.0",
"save_id": "p14_03_legacy_rush",
"seed": 1403,
"uncompressed_size": 197716,
"updated_at_real": "2026-10-20T07:38:59"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 B

Binary file not shown.
+34
View File
@@ -0,0 +1,34 @@
extends SceneTree
var n := 0
func c(rt, clock, k, p):
n += 1
var r: CommandResult = rt.submit_command(k, p, &"player", &"", 0, clock.absolute_minute, StringName("p1403.%d" % n))
if not r.is_success(): print("FAIL ", k, " ", r.message)
return r
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var clock: Variant = load("res://autoload/game_clock.gd").new()
var state: Variant = load("res://autoload/game_state.gd").new()
var rt: Variant = load("res://autoload/simulation_runtime.gd").new()
clock.reset(1, 6, 0); state.reset(); rt.initialize(clock, state, 1403); clock.set_paused(true)
c(rt, clock, &"change_resources", {&"deltas": {&"gold": 30000, &"materials": 300}})
c(rt, clock, &"build_facility", {&"definition_id": &"guest_elevator", &"origin": Vector3i(0,0,0)})
c(rt, clock, &"build_facility", {&"definition_id": &"guest_elevator", &"origin": Vector3i(4,0,0)})
clock.advance_minutes(360)
c(rt, clock, &"build_facility", {&"definition_id": &"guest_elevator", &"origin": Vector3i(0,2,0)})
for x in [2, 3]: c(rt, clock, &"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(x,1,0)})
clock.advance_minutes(360)
clock.advance_minutes(10 - int(clock.absolute_minute) % 10 + 1)
for i in 12:
var gid := StringName("rush_%02d" % i)
c(rt, clock, &"spawn_guest", {&"guest_id": gid, &"guest_type_id": &"traveler", &"options": {&"current_cell": Vector3i(0,0,0)}})
var t = rt.elevators.request_trip(StringName("guest:%s" % gid), Vector3i(0,0,0), Vector3i(0,3,0), clock.absolute_minute)
if not t.ok: print("TRIPFAIL ", t)
var dyn := []
for row in rt.path_service.create_snapshot()[&"connectors"]:
if String(row[&"id"]).begins_with("transport."): dyn.append(int(row[&"dynamic_cost"]))
print("LEGACY dynamic costs ", dyn)
var out := OS.get_environment("LEGACY_OUT")
print("SAVE ", SaveService.save_snapshot(out, rt.create_snapshot())[&"ok"], " ", out)
rt.free(); clock.free(); state.free(); quit(0)
+288 -77
View File
@@ -1,17 +1,24 @@
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.
## P14-03 route choice with real waiting. Two guest shafts with real guests at
## real cells; a registered worker physically walks the chosen route. Covers a
## whole-shaft rush detour, shared cabins within capacity with every rider's
## arrival cell, cancellation, route loss on demolition, service-elevator and
## stair congestion pricing, warp-exit congestion, legacy v1 save migration and
## v2 tamper rejection. "-- writer"/"-- reader" add a new-PID rush save check.
const START := Vector3i(1, 0, 0)
const GOAL := Vector3i(2, 1, 0)
const LEGACY_FIXTURE := "res://tests/fixtures/p14-03/legacy_rush_v1"
const LEGACY_COPY := "user://tests/p14_03_legacy_rush_copy"
const PID_SAVE := "user://saves/p14_03_route_new_pid"
const PID_EXPECTED := "user://saves/p14_03_route_new_pid_expected.json"
const PID_CONTINUE := 60
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:
@@ -19,7 +26,31 @@ func _initialize() -> void:
func _run() -> void:
print("Route wait choice (P14-03)")
var args := OS.get_cmdline_user_args()
var mode := "reader" if "reader" in args else ("writer" if "writer" in args else "full")
print("Route wait choice (P14-03) %s pid %d" % [mode, OS.get_process_id()])
_new_runtime()
if mode == "reader":
_read_new_pid()
else:
_layout()
if mode == "writer":
_write_new_pid()
else:
_test_shaft_rush()
_test_worker_walks_detour()
_test_shared_cabin()
_test_route_loss()
_test_service_elevator_wait()
_test_stair_congestion()
_test_warp_exit_congestion()
_test_legacy_v1_migration()
_free_runtime()
print("%s: %d checks" % ["PASS" if _failures == 0 else "FAIL", _checks])
quit(_failures)
func _new_runtime() -> void:
_clock = load("res://autoload/game_clock.gd").new()
_state = load("res://autoload/game_state.gd").new()
_runtime = load("res://autoload/simulation_runtime.gd").new()
@@ -27,14 +58,10 @@ func _run() -> void:
_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()
func _free_runtime() -> void:
_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:
@@ -52,11 +79,14 @@ func _next_evaluation() -> void:
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.
_expect(_cmd(&"change_resources", {&"deltas": {&"gold": 40000, &"materials": 400}}).is_success(), "fund the fixture")
# FRONT: warps x-4, shaft A x0 (floors 0-3), shaft B x4 (floors 0-1), floor-1 link x2-3.
# BACK: service shaft x6, security station x-2 for the worker's shift.
_build(&"guest_elevator", Vector3i(0, 0, 0))
_build(&"guest_elevator", Vector3i(4, 0, 0))
_build(&"mana_warp", Vector3i(-4, 0, 0))
_build(&"service_elevator", Vector3i(6, 0, 1))
_build(&"security_station", Vector3i(-2, 0, 1))
_clock.advance_minutes(360)
_build(&"guest_elevator", Vector3i(0, 2, 0))
_build(&"mana_warp", Vector3i(-4, 2, 0))
@@ -64,7 +94,7 @@ func _layout() -> void:
_build(&"guest_corridor", Vector3i(x, 1, 0))
_clock.advance_minutes(360)
_next_evaluation()
_expect(_runtime.elevators.get_report()[&"shafts"].size() == 2, "two independent shafts")
_expect(_runtime.elevators.get_report()[&"shafts"].size() == 3, "two guest shafts and one service shaft")
func _spawn(guest_id: StringName, cell: Vector3i) -> bool:
@@ -81,96 +111,213 @@ func _uses_shaft_a(path: Array) -> bool:
return path.size() > 1 and path[1] == Vector3i(0, 0, 0)
func _queue_rush(count: int) -> Array[StringName]:
var riders: Array[StringName] = []
for i: int in count:
var guest_id := StringName("rush_%02d" % i)
if _spawn(guest_id, Vector3i(0, 0, 0)):
var actor_id := StringName("guest:%s" % guest_id)
var trip: Dictionary = _runtime.elevators.request_trip(actor_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(actor_id)
return riders
func _shaft_a_link() -> StringName:
return _runtime.path_service.get_step_connector(Vector3i(0, 0, 0), Vector3i(0, 1, 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 riders := _queue_rush(12)
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")
_expect(int(_runtime.path_service.get_connector(_shaft_a_link())[&"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")
_expect(int(saved[&"systems"][&"runtime"][&"elevators"][&"version"]) == ElevatorService.SNAPSHOT_VERSION, "new saves carry elevator snapshot v2")
_expect(_runtime.restore_snapshot(saved.duplicate(true)) and _runtime.create_snapshot() == saved, "rush wait costs and queues survive save restore exactly")
for bad: int in [0, 300]:
var tampered: Dictionary = saved.duplicate(true)
for row: Dictionary in tampered[&"systems"][&"runtime"][&"path"][&"connectors"]:
if row[&"id"] == _shaft_a_link(): row[&"dynamic_cost"] = bad
var live: Dictionary = _runtime.create_snapshot()
_expect(not _runtime.restore_snapshot(tampered) and _runtime.create_snapshot() == live, "v2 save with shaft wait %d (disagreeing with its queue) is rejected atomically" % bad)
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")
_expect(int(_runtime.path_service.get_connector(_shaft_a_link())[&"dynamic_cost"]) == 0, "cancelled rush clears the shaft wait")
_expect(_runtime.path_service.calculate_path_cost(idle) == a_route_during_rush - 12 * PathService.ORTHOGONAL_MOVE_COST, "cleared shaft wait removes exactly the queued riders' cost from shaft A's route")
_expect(_runtime.restore_snapshot(saved), "rewind to the rush state")
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:
func _register_walker(staff_id: StringName, cell: Vector3i) -> bool:
var registered := _cmd(&"register_staff", {&"staff_id": staff_id, &"role_id": &"porter", &"options": {
&"current_cell": cell, &"state": StaffRuntime.State.OFF_DUTY, &"role_tags": [&"porter"],
&"permitted_task_tags": [&"work"], &"skills": {&"operation": 50}}})
var station: FacilityInstance = null
for facility: FacilityInstance in _runtime.facility_service.get_instances():
if facility.definition_id == &"security_station": station = facility
var minute: int = posmod(_clock.absolute_minute, 1440)
var shift := _cmd(&"create_staff_shift", {&"staff_id": staff_id, &"template_id": &"FLEX", &"start_day": _clock.absolute_minute / 1440,
&"start_minute_of_day": minute, &"end_minute_of_day": mini(minute + 600, 1439), &"facility_id": station.instance_id})
_expect(registered.is_success() and shift.is_success(), "register real walker %s at %s %s %s" % [staff_id, cell, registered.message, shift.message])
return registered.is_success() and shift.is_success()
## Sends the walker to GOAL through an ordinary Staff task; returns visited cells.
func _walk(staff_id: StringName, task_id: StringName) -> Array:
var created := _cmd(&"create_staff_task", {&"task_id": task_id, &"task_type": &"inspect", &"priority": 90, &"options": {
&"target_cell": GOAL, &"required_role_tags": [&"porter"], &"task_tags": [&"work"], &"estimated_minutes": 1}})
_expect(created.is_success(), "walk task %s created %s" % [task_id, created.message])
var visited: Array = []
for minute: int in 180:
_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")
var worker: StaffRuntime = _runtime.staff_service.get_staff(staff_id)
if worker == null:
break
if visited.is_empty() or visited.back() != worker.current_cell:
visited.append(worker.current_cell)
if worker.current_cell == GOAL:
break
return visited
func _shaft_b() -> StringName:
func _test_worker_walks_detour() -> void:
# Still in the rush state saved above: shaft A has 12 riders queued.
var before: Dictionary = _runtime.create_snapshot()
if not _register_walker(&"walker_rush", START):
return
var visited := _walk(&"walker_rush", &"walk_rush")
_expect(not visited.is_empty() and visited.back() == GOAL, "real walker reaches the goal during the rush")
_expect(visited.has(Vector3i(4, 0, 0)) and visited.has(Vector3i(4, 1, 0)), "walker physically rides shaft B (%s)" % [visited])
_expect(not visited.has(Vector3i(0, 1, 0)), "walker never rides the congested shaft A")
_expect(_runtime.restore_snapshot(before), "rewind walker drill")
func _shaft_id_at(x: int, layer: int) -> StringName:
for shaft: Dictionary in _runtime.elevators.get_report()[&"shafts"]:
if (shaft[&"stops"] as Array).front().x == 4:
var bottom: Vector3i = (shaft[&"stops"] as Array).front()
if bottom.x == x and bottom.z == layer:
return shaft[&"shaft_id"]
return &""
func _test_shared_cabin() -> void:
var before: Dictionary = _runtime.create_snapshot()
for trip: Dictionary in _runtime.elevators.get_report()[&"trips"]:
_runtime.elevators.cancel_actor(StringName(trip[&"actor_id"]))
var riders: Array[StringName] = []
for i: int in 6:
var guest_id := StringName("share_%02d" % i)
if not _spawn(guest_id, Vector3i(4, 0, 0)):
continue
# Real guest with a floor-1 destination; the Runtime's own path travel
# requests the elevator ride (no test-side trip injection).
var guest: GuestRuntime = _runtime.guest_service.get_guest(guest_id)
guest.path_handle = {&"target_cell": Vector3i(4, 1, 0)}
_expect(bool(_runtime.guest_service.transition_guest(guest_id, GuestRuntime.State.ENTERING, _clock.absolute_minute, &"TEST_ENTRY").get(&"ok", false))
and bool(_runtime.guest_service.transition_guest(guest_id, GuestRuntime.State.MOVING, _clock.absolute_minute, &"TEST_DESTINATION").get(&"ok", false)), "rider %s heads for floor 1" % guest_id)
riders.append(StringName("guest:%s" % guest_id))
var requested := {}
var leaver: StringName = &""
var shaft_b := _shaft_id_at(4, 0)
var max_boarded := 0
var arrival_cell := {}
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 and trip[&"state"] == PathService.ELEVATOR_REQUEST_BOARDED:
boarded += 1
if leaver.is_empty():
# A boarded guest told to leave (patience/closing) must ride on.
leaver = StringName(trip[&"actor_id"])
_expect(bool(_runtime.guest_service.begin_leaving(StringName(String(leaver).trim_prefix("guest:")), _clock.absolute_minute, &"TEST_LEAVE_WHILE_ABOARD").get(&"ok", false)), "boarded rider %s starts leaving mid-ride" % leaver)
max_boarded = maxi(max_boarded, boarded)
for actor_id: StringName in riders:
if not _runtime.elevators.get_actor_trip(actor_id).is_empty():
requested[actor_id] = true
continue
if arrival_cell.has(actor_id) or not requested.has(actor_id):
continue
var guest: GuestRuntime = _runtime.guest_service.get_guest(StringName(String(actor_id).trim_prefix("guest:")))
arrival_cell[actor_id] = guest.current_cell if guest != null else null
_expect(max_boarded > 1 and max_boarded <= 4, "riders share the cabin up to its capacity of 4 (max %d)" % max_boarded)
_expect(requested.size() == riders.size(), "the Runtime requested a ride for every rider")
var arrived_at := -1
var despawned_at := -1
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
if not (leaver in event.entity_ids or StringName(String(leaver).trim_prefix("guest:")) in event.entity_ids):
continue
if event.event_type == &"ELEVATOR_ARRIVED" and event.payload.get(&"cell") == Vector3i(4, 1, 0):
arrived_at = event.absolute_minute
if event.event_type == &"GUEST_DESPAWNED":
despawned_at = event.absolute_minute
_expect(not leaver.is_empty() and arrived_at >= 0 and (despawned_at < 0 or despawned_at >= arrived_at), "a rider leaving mid-ride rides on and alights at the floor-1 landing before it can leave (arrived %d, left %d)" % [arrived_at, despawned_at])
_expect(arrival_cell.size() == riders.size(), "every rider's trip ends")
for actor_id: StringName in riders:
if actor_id == leaver:
continue
_expect(arrival_cell.get(actor_id) == Vector3i(4, 1, 0), "%s exists and stands on the floor-1 landing when the ride ends (%s)" % [actor_id, arrival_cell.get(actor_id)])
_expect(_runtime.restore_snapshot(before), "rewind shared-cabin drill")
func _test_route_loss() -> void:
var before: Dictionary = _runtime.create_snapshot()
var shaft_b_id: StringName = &""
for trip: Dictionary in _runtime.elevators.get_report()[&"trips"]:
_runtime.elevators.cancel_actor(StringName(trip[&"actor_id"]))
var shaft_b_facility: 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")
shaft_b_facility = facility[&"instance_id"]
_expect(_cmd(&"demolish_facility", {&"instance_id": shaft_b_facility}).is_success(), "demolish shaft B")
_expect(_uses_shaft_a(_route()), "route loss falls back to the remaining shaft")
if _register_walker(&"walker_loss", START):
var visited := _walk(&"walker_loss", &"walk_loss")
_expect(not visited.is_empty() and visited.back() == GOAL and visited.has(Vector3i(0, 1, 0)), "real walker reaches the goal through shaft A after the loss (%s)" % [visited])
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "post-loss state restores")
_expect(_runtime.restore_snapshot(before), "rewind route-loss drill")
func _test_service_elevator_wait() -> void:
var ps: PathService = _runtime.path_service
var lower := Vector3i(6, 0, 1)
var upper := Vector3i(6, 1, 1)
var staff_only: Array[StringName] = [&"staff"]
var layers: Array[int] = [WorldService.LAYER_FRONT, WorldService.LAYER_BACK]
_expect(ps.find_path(lower, upper).is_empty() and not ps.find_path(lower, upper, layers, staff_only).is_empty(), "service shaft is staff-only")
var idle_cost: int = ps.last_path_cost
var before: Dictionary = _runtime.create_snapshot()
for i: int in 4:
var staff_id := StringName("svc_rider_%d" % i)
_expect(_cmd(&"register_staff", {&"staff_id": staff_id, &"role_id": &"porter", &"options": {&"current_cell": lower, &"state": StaffRuntime.State.OFF_DUTY, &"role_tags": [&"porter"]}}).is_success(), "register real worker %s on the service landing" % staff_id)
_expect(bool(_runtime.elevators.request_trip(StringName("staff:%s" % staff_id), lower, upper, _clock.absolute_minute, staff_only).get(&"ok", false)), "real worker %s queues on the service shaft" % staff_id)
ps.find_path(lower, upper, layers, staff_only)
_expect(ps.last_path_cost > idle_cost, "queued workers raise the service elevator's A* cost")
_expect(_runtime.restore_snapshot(before), "rewind service drill")
func _test_stair_congestion() -> void:
var ps: PathService = _runtime.path_service
var before: Dictionary = _runtime.create_snapshot()
_expect(_cmd(&"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(-1, 1, 0)}).is_success(), "build a floor-1 corridor beside shaft A")
_expect(_cmd(&"build_facility", {&"definition_id": &"guest_stairs", &"origin": Vector3i(-2, 1, 0)}).is_success(), "build stairs to floor 1")
_clock.advance_minutes(20)
var landing := Vector3i(-2, 1, 0)
var foot := Vector3i(-2, 0, 0)
var quiet: Array = ps.find_path(foot, landing)
var quiet_cost: int = ps.last_path_cost
_expect(quiet == [foot, landing], "stairs connect the two floors")
for i: int in 6:
_spawn(StringName("stair_crowd_%d" % i), landing)
ps.find_path(foot, landing)
_expect(ps.last_path_cost > quiet_cost, "a crowded stair landing raises the stair route's A* cost")
_expect(_runtime.restore_snapshot(before), "rewind stair drill")
func _test_warp_exit_congestion() -> void:
var ps: PathService = _runtime.path_service
var lower := Vector3i(-4, 0, 0)
@@ -186,6 +333,70 @@ func _test_warp_exit_congestion() -> void:
_expect(crowd > 0 and ps.last_path_cost > quiet_cost, "a crowded warp exit raises the warp route's A* cost")
## A genuine save written by 50ceca0 (pre-P14-03, elevator snapshot v1) with 12
## riders queued for floor 3 and every elevator dynamic_cost 0.
func _test_legacy_v1_migration() -> void:
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(LEGACY_COPY))
for member: String in ["world.bin", "manifest.json", "thumbnail.png", "journal.log"]:
var bytes := FileAccess.get_file_as_bytes(LEGACY_FIXTURE.path_join(member))
var file := FileAccess.open(LEGACY_COPY.path_join(member), FileAccess.WRITE)
file.store_buffer(bytes)
file.close()
var loaded := SaveService.load_snapshot(LEGACY_COPY)
_expect(loaded[&"ok"], "legacy v1 package loads from disk")
if not loaded[&"ok"]:
return
var legacy: Dictionary = loaded[&"snapshot"]
_expect(int(legacy[&"systems"][&"runtime"][&"elevators"][&"version"]) == 1, "fixture really is elevator snapshot v1")
var live: Dictionary = _runtime.create_snapshot()
var bad: Dictionary = legacy.duplicate(true)
for row: Dictionary in bad[&"systems"][&"runtime"][&"path"][&"connectors"]:
if String(row[&"id"]).begins_with("transport.") and int(row[&"dynamic_cost"]) == 0:
row[&"dynamic_cost"] = 500
break
_expect(not _runtime.restore_snapshot(bad) and _runtime.create_snapshot() == live, "v1 save with a nonzero wait (never written by v1) is rejected atomically")
_expect(_runtime.restore_snapshot(legacy.duplicate(true)), "legacy v1 rush save migrates")
_expect(int(_runtime.path_service.get_connector(_shaft_a_link())[&"dynamic_cost"]) == 12 * PathService.ORTHOGONAL_MOVE_COST, "migration derives the shaft-wide wait from the legacy queue")
_expect(not _uses_shaft_a(_route()), "migrated rush diverts like a fresh one")
var migrated: Dictionary = _runtime.create_snapshot()
_expect(int(migrated[&"systems"][&"runtime"][&"elevators"][&"version"]) == ElevatorService.SNAPSHOT_VERSION and _runtime.restore_snapshot(migrated) and _runtime.create_snapshot() == migrated, "migrated state saves as v2 and round-trips exactly")
SaveService.delete_save(LEGACY_COPY)
func _write_new_pid() -> void:
_queue_rush(12)
_register_walker(&"walker_pid", START)
var snapshot: Dictionary = _runtime.create_snapshot()
var serial_at_save := _serial
_expect(SaveService.save_snapshot(PID_SAVE, snapshot)[&"ok"], "writer saves the rush state")
var visited := _walk(&"walker_pid", &"walk_pid")
_clock.advance_minutes(maxi(PID_CONTINUE - visited.size(), 0))
var file := FileAccess.open(PID_EXPECTED, FileAccess.WRITE)
file.store_string(JSON.stringify({"writer_pid": OS.get_process_id(), "serial_at_save": serial_at_save, "visited": var_to_str(visited),
"minute": _clock.absolute_minute, "digest": SimulationDigest.compute(_runtime.create_snapshot())}))
file.close()
_expect(not visited.is_empty() and visited.back() == GOAL and visited.has(Vector3i(4, 1, 0)), "writer walker reaches the goal through shaft B")
func _read_new_pid() -> void:
var expected: Variant = JSON.parse_string(FileAccess.get_file_as_string(PID_EXPECTED))
_expect(expected is Dictionary, "reader finds writer record")
if not expected is Dictionary:
return
_expect(int(expected["writer_pid"]) != OS.get_process_id(), "reader runs in a different process")
var loaded := SaveService.load_snapshot(PID_SAVE)
_expect(loaded[&"ok"] and _runtime.restore_snapshot(loaded[&"snapshot"]), "reader restores the rush save")
_expect(int(_runtime.path_service.get_connector(_shaft_a_link())[&"dynamic_cost"]) == 12 * PathService.ORTHOGONAL_MOVE_COST, "restored shaft wait matches the queue")
# Same player command sequence as the writer after its save.
_serial = int(expected["serial_at_save"])
var visited := _walk(&"walker_pid", &"walk_pid")
_clock.advance_minutes(maxi(PID_CONTINUE - visited.size(), 0))
_expect(var_to_str(visited) == String(expected["visited"]), "walker takes the same physical route after restart")
_expect(_clock.absolute_minute == int(expected["minute"]) and SimulationDigest.compute(_runtime.create_snapshot()) == String(expected["digest"]), "whole continued state digest matches the writer")
SaveService.delete_save(PID_SAVE)
DirAccess.remove_absolute(ProjectSettings.globalize_path(PID_EXPECTED))
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition: