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
+13 -2
View File
@@ -4,7 +4,10 @@ extends RefCounted
## One physical cabin per contiguous shaft. Path connectors expose reachability;
## boarding and arrival here remain authoritative for actual movement.
## Dispatch is oldest request first, batching one origin/destination at a time.
const SNAPSHOT_VERSION := 1
## 2: connectors publish the shaft-wide wait as dynamic_cost (P14-03).
## 1: legacy saves, where every elevator connector's dynamic_cost was 0.
const SNAPSHOT_VERSION := 2
const LEGACY_SNAPSHOT_VERSION := 1
const PREFIX := "transport."
const REQUEST_PREFIX := "transport.trip."
const DOOR_MINUTES := 2
@@ -371,7 +374,8 @@ func create_snapshot() -> Dictionary:
func restore_snapshot(snapshot: Dictionary) -> bool:
if _path == null or not _exact_keys(snapshot, [&"version", &"current_minute", &"request_sequence", &"shafts", &"trips"]):
return _invalid("Elevator snapshot shape is invalid.")
if snapshot[&"version"] != SNAPSHOT_VERSION or not snapshot[&"current_minute"] is int or snapshot[&"current_minute"] < 0 \
if not snapshot[&"version"] is int or snapshot[&"version"] not in [LEGACY_SNAPSHOT_VERSION, SNAPSHOT_VERSION] \
or not snapshot[&"current_minute"] is int or snapshot[&"current_minute"] < 0 \
or not snapshot[&"request_sequence"] is int or snapshot[&"request_sequence"] < 0 \
or not snapshot[&"shafts"] is Array or not snapshot[&"trips"] is Array:
return _invalid("Elevator snapshot header is invalid.")
@@ -466,6 +470,9 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
queued += 1
for connector: Dictionary in _connector_configs(next_shafts[shaft_id][&"config"]):
var expected_wait := maxi(queued - _path.elevator_queue_size(connector[&"id"]), 0) * PathService.ORTHOGONAL_MOVE_COST
# Legacy saves never priced the shaft queue; v2 must match exactly.
if snapshot[&"version"] == LEGACY_SNAPSHOT_VERSION:
expected_wait = 0
if int(_path.get_connector(connector[&"id"]).get(&"dynamic_cost", -1)) != expected_wait:
return _invalid("Path shaft wait disagrees with queued trips.")
var path_snapshot := _path.create_snapshot()
@@ -488,6 +495,10 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
_trips = next_trips
_minute = snapshot[&"current_minute"]
_sequence = snapshot[&"request_sequence"]
if snapshot[&"version"] == LEGACY_SNAPSHOT_VERSION:
# Validated legacy queue: derive the shaft-wide wait once on load.
for shaft_id: StringName in _names(_shafts):
_refresh_shaft_wait(shaft_id)
last_error = ""
return true