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:
co-authored by
Claude Opus 5.5
parent
62d1dcecad
commit
a8edd3c3a1
Vendored
+30
@@ -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の待ち費用を含む保存は、原子的に拒否されます。
|
||||
@@ -0,0 +1 @@
|
||||
save_completed 2026-10-20T07:38:59
|
||||
@@ -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 |
BIN
Binary file not shown.
+34
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user