Add explicit stairs and measure elevator congestion with real actors
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
extends SceneTree
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
var _runtime: Variant
|
||||
var _clock: Variant
|
||||
var _state: Variant
|
||||
var _serial := 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
func _run() -> void:
|
||||
_runtime = load("res://autoload/simulation_runtime.gd").new()
|
||||
_clock = load("res://autoload/game_clock.gd").new()
|
||||
_state = load("res://autoload/game_state.gd").new()
|
||||
_reset()
|
||||
_expect(_runtime.path_service.get_vertical_walk_requires_connector(), "new Runtime requires explicit vertical transport")
|
||||
_expect(_runtime.definition_registry.facility_count() == 18, "Runtime registers baseline16 and two staircase definitions")
|
||||
var before: Dictionary = _runtime.create_snapshot()
|
||||
var invalid := _command(&"build_facility", {&"definition_id": &"guest_stairs", &"origin": Vector3i(4, 3, 0)})
|
||||
_expect(not invalid.is_success() and invalid.details.get(&"reason") == &"STAIR_LANDING_MISSING", "stairs without a lower landing reject before any construction")
|
||||
_expect(_runtime.construction_service.get_jobs().is_empty() and _state.get_resource(&"gold") == 5000, "missing landing spends no money and starts no job")
|
||||
_build(&"guest_stairs", Vector3i(4, 1, 0))
|
||||
_build(&"guest_stairs", Vector3i(4, 2, 0))
|
||||
var route: Array = _runtime.path_service.find_path(Vector3i(4, 0, 0), Vector3i(4, 2, 0))
|
||||
_expect(route.size() == 3 and _runtime.path_service.calculate_path_cost(route) == 300, "two staircase floors cost three walking minutes")
|
||||
_expect(_runtime._estimated_actor_path_minutes(route, Array([], TYPE_STRING_NAME, &"", null)) == 3, "actor arrival estimate includes stair time")
|
||||
var spawn := _command(&"spawn_guest", {&"guest_id": &"stair.guest", &"guest_type_id": &"traveler", &"gold": 100,
|
||||
&"options": {&"current_cell": Vector3i(4, 0, 0), &"state": GuestRuntime.State.MOVING,
|
||||
&"state_entered_at": _clock.absolute_minute - 1, &"planned_exit_at": _clock.absolute_minute + 2000,
|
||||
&"path_handle": {&"target_cell": Vector3i(4, 2, 0)}}})
|
||||
_expect(spawn.is_success(), "public guest spawn creates a real stair traveller")
|
||||
_clock.advance_minutes(1)
|
||||
var guest: GuestRuntime = _runtime.guest_service.get_guest(&"stair.guest")
|
||||
_expect(guest.current_cell == Vector3i(4, 0, 0), "after one minute guest is still completing the first staircase")
|
||||
var mid: Dictionary = _runtime.create_snapshot()
|
||||
_expect(mid[&"systems"][&"runtime"][&"movement_credits"][0][&"credit"] == 1000, "partial staircase persists one minute of movement credit")
|
||||
_expect(_runtime.restore_snapshot(mid) and _runtime.create_snapshot() == mid, "half-finished staircase restores exactly")
|
||||
_clock.advance_minutes(2)
|
||||
var arrived: Dictionary = _runtime.create_snapshot()
|
||||
_expect(_runtime.guest_service.get_guest(&"stair.guest").current_cell == Vector3i(4, 2, 0), "two floors complete in three actual simulation minutes")
|
||||
_expect(_runtime.restore_snapshot(mid), "restore partial staircase for deterministic replay")
|
||||
_clock.advance_minutes(2)
|
||||
_expect(_runtime.create_snapshot() == arrived, "fractional stair travel replays every aggregate exactly")
|
||||
var bad := mid.duplicate(true)
|
||||
bad[&"systems"][&"runtime"][&"movement_credits"][0][&"credit"] = 1500
|
||||
_expect(not _runtime.restore_snapshot(bad) and _runtime.create_snapshot() == arrived, "unspent complete stair step is rejected atomically")
|
||||
bad = mid.duplicate(true)
|
||||
bad[&"systems"][&"runtime"][&"path"][&"vertical_walk_requires_connector"] = false
|
||||
_expect(not _runtime.restore_snapshot(bad), "current Runtime cannot save unrestricted vertical walking")
|
||||
bad = mid.duplicate(true)
|
||||
bad[&"systems"][&"runtime"].erase(&"legacy_stairs")
|
||||
_expect(not _runtime.restore_snapshot(bad), "current Runtime requires stair ownership metadata")
|
||||
bad = mid.duplicate(true)
|
||||
bad[&"systems"][&"runtime"][&"runtime_schema_version"] = 8
|
||||
bad[&"systems"][&"runtime"].erase(&"legacy_stairs")
|
||||
bad[&"systems"][&"runtime"][&"path"][&"vertical_walk_requires_connector"] = false
|
||||
_expect(not _runtime.restore_snapshot(bad), "older Runtime label cannot smuggle current Path policy fields")
|
||||
var blockade := mid.duplicate(true)
|
||||
for traffic: Dictionary in blockade[&"systems"][&"runtime"][&"path"][&"cell_traffic"]:
|
||||
if traffic[&"cell"] == Vector3i(4, 1, 0):
|
||||
traffic[&"temporary_block"] = true
|
||||
_expect(_runtime.restore_snapshot(blockade), "temporary blockade preserves valid fractional staircase credit")
|
||||
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "blocked partial staircase re-saves canonically")
|
||||
_reset()
|
||||
_build(&"guest_corridor", Vector3i(4, 1, 0))
|
||||
_build(&"guest_stairs", Vector3i(4, 2, 0))
|
||||
var lower_id := _facility_at(Vector3i(4, 1, 0))
|
||||
var upper_id := _facility_at(Vector3i(4, 2, 0))
|
||||
var blocked := _command(&"demolish_facility", {&"instance_id": lower_id})
|
||||
_expect(not blocked.is_success() and blocked.details.get(&"reason") == &"STAIR_LANDING_IN_USE", "occupied lower landing rejects demolition before a broken stair is created")
|
||||
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "rejected stair demolition remains saveable")
|
||||
_expect(_command(&"demolish_facility", {&"instance_id": upper_id}).is_success(), "upper staircase can be removed first")
|
||||
_expect(_command(&"demolish_facility", {&"instance_id": lower_id}).is_success(), "unused lower corridor can then be removed")
|
||||
_reset()
|
||||
_build(&"service_stairs", Vector3i(4, 1, 1))
|
||||
_expect(_runtime.path_service.find_path(Vector3i(4, 0, 1), Vector3i(4, 1, 1)).is_empty(), "guests cannot use service stairs")
|
||||
_expect(_runtime.path_service.find_path(Vector3i(4, 0, 1), Vector3i(4, 1, 1), [0, 1], [&"staff"]).size() == 2, "staff can use service stairs")
|
||||
_test_legacy()
|
||||
_test_occupied_stair_demolition()
|
||||
print("Runtime stairs: %d checks, %d failures" % [_checks, _failures])
|
||||
_runtime.free()
|
||||
_clock.free()
|
||||
_state.free()
|
||||
quit(1 if _failures else 0)
|
||||
|
||||
func _test_legacy() -> void:
|
||||
_reset()
|
||||
_build(&"guest_corridor", Vector3i(4, 1, 0))
|
||||
_build(&"guest_corridor", Vector3i(4, 2, 0))
|
||||
_expect(_runtime.path_service.find_path(Vector3i(4, 0, 0), Vector3i(4, 2, 0)).is_empty(), "new corridors do not act as invisible stairs")
|
||||
var legacy: Dictionary = _runtime.create_snapshot()
|
||||
var data: Dictionary = legacy[&"systems"][&"runtime"]
|
||||
data[&"runtime_schema_version"] = 8
|
||||
data.erase(&"legacy_stairs")
|
||||
data[&"path"][&"schema_version"] = 4
|
||||
data[&"path"].erase(&"vertical_walk_requires_connector")
|
||||
data[&"path"].erase(&"transit_actor_ids")
|
||||
_expect(_runtime.restore_snapshot(legacy), "actual v8 structure migrates existing vertical corridors")
|
||||
var records: Array = _runtime.legacy_stairs.create_snapshot()[&"stairs"]
|
||||
_expect(records.size() == 2 and _runtime.path_service.get_vertical_walk_requires_connector(), "migration materializes only the two original vertical links")
|
||||
var route: Array = _runtime.path_service.find_path(Vector3i(4, 0, 0), Vector3i(4, 2, 0))
|
||||
_expect(route.size() == 3 and _runtime.path_service.calculate_path_cost(route) == 200, "old corridor stairs retain their original speed and reachability")
|
||||
var canonical: Dictionary = _runtime.create_snapshot()
|
||||
_expect(_runtime.restore_snapshot(canonical) and _runtime.create_snapshot() == canonical, "migrated save is canonical and migration does not repeat")
|
||||
var malformed := canonical.duplicate(true)
|
||||
malformed[&"systems"][&"runtime"][&"legacy_stairs"][&"stairs"].pop_back()
|
||||
_expect(not _runtime.restore_snapshot(malformed) and _runtime.create_snapshot() == canonical, "orphan legacy connector is rejected atomically")
|
||||
var upper_id := _facility_at(Vector3i(4, 2, 0))
|
||||
_expect(_command(&"demolish_facility", {&"instance_id": upper_id}).is_success(), "legacy upper landing can be demolished normally")
|
||||
_expect(_runtime.legacy_stairs.create_snapshot()[&"stairs"].size() == 1, "demolition retires that stair's ownership record")
|
||||
_build(&"guest_corridor", Vector3i(4, 2, 0))
|
||||
_expect(_runtime.path_service.find_path(Vector3i(4, 1, 0), Vector3i(4, 2, 0)).is_empty(), "rebuilding a plain corridor never regenerates a legacy staircase")
|
||||
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "pruned and rebuilt topology remains saveable")
|
||||
|
||||
func _reset() -> void:
|
||||
_clock.reset(1, 6, 0)
|
||||
_state.reset()
|
||||
_expect(_runtime.initialize(_clock, _state, 90416)[&"valid"], "stairs fixture initializes")
|
||||
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
||||
_clock.set_paused(true)
|
||||
|
||||
func _build(id: StringName, origin: Vector3i) -> void:
|
||||
var built := _command(&"build_facility", {&"definition_id": id, &"origin": origin})
|
||||
_expect(built.is_success(), "normal construction accepts %s %s: %s" % [id, origin, built.message])
|
||||
_clock.advance_minutes(10)
|
||||
_expect(not _facility_at(origin).is_empty(), "construction completes %s %s" % [id, origin])
|
||||
|
||||
func _facility_at(origin: Vector3i) -> StringName:
|
||||
for facility: Dictionary in _runtime.construction_service.get_facilities():
|
||||
if facility[&"origin"] == origin:
|
||||
return facility[&"instance_id"]
|
||||
return &""
|
||||
|
||||
func _command(type: StringName, payload: Dictionary) -> CommandResult:
|
||||
_serial += 1
|
||||
return _runtime.submit_command(type, payload, &"manager", &"", 0, -1, StringName("runtime.stairs.%d" % _serial))
|
||||
|
||||
func _expect(ok: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if not ok:
|
||||
_failures += 1
|
||||
push_error(label)
|
||||
else:
|
||||
print("PASS: ", label)
|
||||
|
||||
|
||||
func _test_occupied_stair_demolition() -> void:
|
||||
_reset()
|
||||
_build(&"guest_stairs", Vector3i(4, 1, 0))
|
||||
var stair_id := _facility_at(Vector3i(4, 1, 0))
|
||||
_expect(_command(&"spawn_guest", {&"guest_id": &"occupied.stair", &"guest_type_id": &"traveler", &"gold": 100,
|
||||
&"options": {&"current_cell": Vector3i(4, 1, 0), &"state": GuestRuntime.State.MOVING,
|
||||
&"state_entered_at": _clock.absolute_minute - 1, &"planned_exit_at": _clock.absolute_minute + 100,
|
||||
&"path_handle": {&"target_cell": Vector3i(4, 0, 0)}}}).is_success(), "real guest is standing on upper staircase landing")
|
||||
var paths: Dictionary = _runtime.path_service.create_snapshot()
|
||||
var rejected := _command(&"demolish_facility", {&"instance_id": stair_id})
|
||||
_expect(not rejected.is_success() and rejected.details.get(&"reason") == &"FACILITY_OCCUPIED", "occupied staircase cannot delete a real guest's landing")
|
||||
_expect(_runtime.path_service.create_snapshot() == paths and _runtime.guest_service.get_guest(&"occupied.stair").current_cell == Vector3i(4, 1, 0), "demolition rejection preserves actor projection and position")
|
||||
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "occupied staircase rejection leaves a valid canonical save")
|
||||
_clock.advance_minutes(2)
|
||||
_expect(_runtime.guest_service.get_guest(&"occupied.stair").current_cell == Vector3i(4, 0, 0), "guest physically finishes descending before demolition")
|
||||
_expect(_command(&"demolish_facility", {&"instance_id": stair_id}).is_success(), "empty staircase can be removed after the guest leaves")
|
||||
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "post-descent demolition remains saveable")
|
||||
@@ -0,0 +1 @@
|
||||
uid://blt3w5drpduju
|
||||
@@ -93,7 +93,7 @@ func _verify_mid_ride() -> bool:
|
||||
_expect(bool(_runtime.save_to_path(save_path).get(&"ok", false)), "boarded save writes isolated disk slot")
|
||||
_expect(bool(_runtime.load_from_path(save_path).get(&"ok", false)), "boarded disk slot reloads")
|
||||
var snapshot: Dictionary = _runtime.create_snapshot()
|
||||
_expect(snapshot[&"systems"][&"runtime"][&"runtime_schema_version"] == 8, "runtime schema8 records elevator state")
|
||||
_expect(snapshot[&"systems"][&"runtime"][&"runtime_schema_version"] == 9, "runtime schema9 records elevator state and explicit vertical routing")
|
||||
_expect(_runtime.restore_snapshot(snapshot), "mid-ride Runtime snapshot restores actor/cabin/Path ownership")
|
||||
var shaft: Dictionary = _runtime.elevators.get_report()[&"shafts"][0]
|
||||
var busy := _cmd(&"demolish_facility", {&"instance_id": shaft[&"module_ids"][1]})
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
extends SceneTree
|
||||
|
||||
## Fixed-demand MOVE-06 acceptance. Guests use public spawning plus a fixed
|
||||
## MOVING destination, as in test_runtime_path_traffic. Production Runtime owns
|
||||
## every path choice, walking credit, congestion delay and elevator transition.
|
||||
## This isolates equipment capacity; it does not exercise natural Guest demand,
|
||||
## service purchasing or economic return on investment.
|
||||
const SEED := 806042
|
||||
const POPULATION := 48
|
||||
const WINDOW_MINUTES := 480
|
||||
const START := Vector3i(2, 0, 0)
|
||||
const GOAL := Vector3i(2, 1, 0)
|
||||
const DEMAND_HASH := "5bafa62275370c2a6ce5b4d4f283c3d7679c679a2b3fc6b054b681f2647456cf"
|
||||
const REPORT_PATH := "res://artifacts/transport/runtime-comparison.json"
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
var _runtime: Variant
|
||||
var _clock: Variant
|
||||
var _state: Variant
|
||||
var _start_minute := 0
|
||||
var _records: Dictionary = {}
|
||||
var _trace: Array[Dictionary] = []
|
||||
var _errors: Array[String] = []
|
||||
var _demand_rows: Array[Dictionary] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var global_clock := root.get_node_or_null("GameClock")
|
||||
if global_clock != null: global_clock.set_paused(true)
|
||||
_demand_rows = _demand()
|
||||
_expect(SimulationDigest.compute(_demand_rows) == DEMAND_HASH, "Runtime and pure-service fixtures share the exact seeded 48-person demand manifest")
|
||||
var before := _observe(1, true)
|
||||
var after := _observe(2, true)
|
||||
if before.is_empty() or after.is_empty():
|
||||
_finish()
|
||||
return
|
||||
_expect(before[&"demand_hash"] == after[&"demand_hash"] and before[&"start_minute"] == after[&"start_minute"] and before[&"window_minutes"] == after[&"window_minutes"], "same seed, arrivals, endpoints and absolute observation window in both layouts")
|
||||
_expect(before[&"arrived_count"] == POPULATION and after[&"arrived_count"] == POPULATION, "both layouts receive all 48 real GuestRuntime actors")
|
||||
_expect(before[&"completed_count"] == POPULATION and after[&"completed_count"] == POPULATION, "both layouts physically complete all 48 journeys inside the fixed window")
|
||||
_expect(before[&"unfinished_count"] == 0 and after[&"unfinished_count"] == 0, "no incomplete passenger is omitted from the comparison")
|
||||
_expect(before[&"shortest_walking_steps"] == 4 and after[&"shortest_walking_steps"] == 4, "both physical layouts have equal four-step shortest walking distance to each cabin")
|
||||
_expect(before[&"walking_steps_min"] >= 4 and after[&"walking_steps_min"] >= 4, "actual Runtime guests never shorten the common four-step geometry by a hidden route or teleport")
|
||||
_expect(before[&"walking_steps"][&"samples"] == POPULATION and after[&"walking_steps"][&"samples"] == POPULATION, "actual walking and detour statistics retain all completed guests")
|
||||
_expect(after[&"shaft_usage"].size() == 2, "normal Runtime path choice sends passengers to both installed cabins")
|
||||
_expect(before[&"queue_wait_minutes"][&"samples"] == POPULATION and after[&"queue_wait_minutes"][&"samples"] == POPULATION, "wait statistics include the entire boarded cohort")
|
||||
_expect(_is_improved(before, after), "additional identical cabin improves wait and journey P95 with equal fully completed demand")
|
||||
var replay := _observe(2, false)
|
||||
_expect(after == replay, "fresh same-seed Runtime replay reproduces all outcomes and the full movement event trace hash")
|
||||
var incomplete := after.duplicate(true)
|
||||
incomplete[&"completed_count"] = 0
|
||||
incomplete[&"unfinished_count"] = POPULATION
|
||||
incomplete[&"queue_wait_minutes"] = _stats([])
|
||||
incomplete[&"journey_minutes"] = _stats([])
|
||||
_expect(not _is_improved(before, incomplete), "an unfinished zero-completion result cannot be accepted as faster")
|
||||
var report := {
|
||||
&"scope": "Fixed-demand public-spawn GuestRuntime harness. Actual Runtime routing, movement credit, congestion and paid construction; not natural economic Guest AI or complete P2/S14 acceptance.",
|
||||
&"route_policy": "Production Runtime find_path re-evaluates each walking minute; global explicit vertical connectors; no benchmark blockers or debug simulation jumps. Congestion-driven route reconsideration may add walking; actual detours are reported, never frozen or discarded.",
|
||||
&"completion_policy": "Record actual common-destination arrival, then normal begin_leaving/despawn_guest so completed actors no longer obstruct later arrivals.",
|
||||
&"percentile_policy": "Nearest-rank P95 over observed boarding waits/completed journeys, with unfinished arrivals separately censored. Improvement requires both layouts to finish the entire equal cohort.",
|
||||
&"seed": SEED, &"demand_hash": DEMAND_HASH, &"demand": _demand_rows,
|
||||
&"before": before, &"after": after,
|
||||
&"equipment_cost_difference": {&"gold": after[&"costs"][&"gold"] - before[&"costs"][&"gold"],
|
||||
&"materials": after[&"costs"][&"materials"] - before[&"costs"][&"materials"],
|
||||
&"maintenance_mana_demand": after[&"costs"][&"maintenance_mana_demand"] - before[&"costs"][&"maintenance_mana_demand"]},
|
||||
&"improvement_accepted": _is_improved(before, after), &"checks": _checks, &"failures": _failures}
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path("res://artifacts/transport"))
|
||||
var output := FileAccess.open(REPORT_PATH, FileAccess.WRITE)
|
||||
if output != null: output.store_string(JSON.stringify(report, "\t") + "\n")
|
||||
print("RUNTIME_TRANSPORT_COMPARISON ", JSON.stringify({&"before": _summary(before), &"after": _summary(after), &"improved": report[&"improvement_accepted"]}))
|
||||
_finish()
|
||||
|
||||
|
||||
func _demand() -> Array[Dictionary]:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = SEED
|
||||
var result: Array[Dictionary] = []
|
||||
for index: int in range(POPULATION):
|
||||
result.append({&"actor_id": StringName("comparison/%03d" % index),
|
||||
&"arrived_at": (index / 4) * 15 + rng.randi_range(0, 2),
|
||||
&"from": [START.x, START.y, START.z], &"to": [GOAL.x, GOAL.y, GOAL.z]})
|
||||
result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
if a[&"arrived_at"] != b[&"arrived_at"]: return a[&"arrived_at"] < b[&"arrived_at"]
|
||||
return str(a[&"actor_id"]) < str(b[&"actor_id"]))
|
||||
return result
|
||||
|
||||
|
||||
func _prepare(cabins: int) -> Dictionary:
|
||||
_runtime = load("res://autoload/simulation_runtime.gd").new()
|
||||
_clock = load("res://autoload/game_clock.gd").new()
|
||||
_state = load("res://autoload/game_state.gd").new()
|
||||
_clock.reset(1, 6, 0)
|
||||
_state.reset()
|
||||
_expect(_runtime.initialize(_clock, _state, SEED)[&"valid"], "%d-cabin Runtime initializes at normal starting resources" % cabins)
|
||||
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
||||
_clock.set_paused(true)
|
||||
var builds: Array = [[&"guest_elevator", Vector3i.ZERO],
|
||||
[&"guest_corridor", Vector3i(2, 1, 0)], [ &"guest_corridor", Vector3i(3, 1, 0)]]
|
||||
if cabins == 2: builds.append([&"guest_elevator", Vector3i(4, 0, 0)])
|
||||
var success := true
|
||||
for index: int in range(builds.size()):
|
||||
var result: CommandResult = _runtime.submit_command(&"build_facility", {
|
||||
&"definition_id": builds[index][0], &"origin": builds[index][1]},
|
||||
&"player", &"", 0, _clock.absolute_minute, StringName("comparison.build.%d" % index))
|
||||
success = success and result.is_success()
|
||||
_expect(success, "%d-cabin layout is purchased through normal public construction" % cabins)
|
||||
_clock.advance_minutes(310)
|
||||
var report: Dictionary = _runtime.elevators.get_report()
|
||||
var maintenance := 0
|
||||
var equal_walking := true
|
||||
for shaft: Dictionary in report[&"shafts"]:
|
||||
success = success and shaft[&"operational"] and shaft[&"capacity"] == 4 and shaft[&"travel_minutes_per_floor"] == 10
|
||||
var lower: Vector3i = shaft[&"stops"][0]
|
||||
var upper: Vector3i = shaft[&"stops"][1]
|
||||
var approach: Array[Vector3i] = _runtime.path_service.find_path(START, lower)
|
||||
var departure: Array[Vector3i] = _runtime.path_service.find_path(upper, GOAL)
|
||||
equal_walking = equal_walking and approach.size() == 3 and departure.size() == 3
|
||||
for cell: Vector3i in approach: equal_walking = equal_walking and cell.y == START.y
|
||||
for cell: Vector3i in departure: equal_walking = equal_walking and cell.y == GOAL.y
|
||||
for module: StringName in shaft[&"module_ids"]:
|
||||
var facility: Dictionary = _runtime.construction_service.get_facility(module)
|
||||
var definition: FacilityDefinition = _runtime.definition_registry.get_facility(facility[&"definition_id"])
|
||||
maintenance += definition.maintenance_mana
|
||||
_expect(success and report[&"shafts"].size() == cabins, "%d identical four-person cabins complete normally and receive real network power" % cabins)
|
||||
_expect(equal_walking, "%d-cabin layout has two real approach and two departure walking steps at every boarding position" % cabins)
|
||||
_expect(_runtime.path_service.get_vertical_walk_requires_connector(), "fixture uses production explicit vertical connector policy")
|
||||
_start_minute = _clock.absolute_minute
|
||||
_records = {}
|
||||
_trace = []
|
||||
_errors = []
|
||||
_subscribe()
|
||||
return {&"gold": 5000 - _state.get_resource(&"gold"), &"materials": 50 - _state.get_resource(&"materials"), &"maintenance_mana_demand": maintenance} if success else {}
|
||||
|
||||
|
||||
func _observe(cabins: int, check_midride: bool) -> Dictionary:
|
||||
var costs := _prepare(cabins)
|
||||
if costs.is_empty():
|
||||
_dispose()
|
||||
return {}
|
||||
_spawn_arrivals(0)
|
||||
var checked_phases: Dictionary = {}
|
||||
for offset: int in range(1, WINDOW_MINUTES + 1):
|
||||
_tick(offset)
|
||||
if check_midride:
|
||||
for shaft: Dictionary in _runtime.elevators.get_report()[&"shafts"]:
|
||||
var phase: StringName = shaft[&"phase"]
|
||||
var kind: StringName = &"QUEUED" if shaft[&"waiting_count"] > 0 else phase
|
||||
if kind in [&"QUEUED", &"BOARDING"] and not checked_phases.has(kind):
|
||||
checked_phases[kind] = true
|
||||
_check_replay(offset, kind)
|
||||
if check_midride: _expect(checked_phases.has(&"QUEUED") and checked_phases.has(&"BOARDING"), "%d-cabin comparison exercises both queued and boarding snapshot checkpoints" % cabins)
|
||||
_expect(_errors.is_empty(), "%d-cabin actors retain valid lifecycle, projection, event timing and physical floor transitions: %s" % [cabins, _errors])
|
||||
var result := _report(cabins, costs)
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path("res://artifacts/transport"))
|
||||
var trace_output := FileAccess.open("res://artifacts/transport/runtime-comparison-%d-trace.json" % cabins, FileAccess.WRITE)
|
||||
if trace_output != null: trace_output.store_string(JSON.stringify(_trace, "\t") + "\n")
|
||||
_expect(_runtime.guest_service.get_guests().size() == result[&"unfinished_count"], "only genuinely unfinished guests remain in live Runtime after the observation window")
|
||||
_expect(_runtime.path_service.get_transit_actor_ids().size() == result[&"aboard_count"], "Path transit occupancy equals actual unfinished boarded passengers")
|
||||
_dispose()
|
||||
return result
|
||||
|
||||
|
||||
func _tick(offset: int) -> void:
|
||||
_clock.advance_minutes(1)
|
||||
for guest_id: StringName in _records:
|
||||
var record: Dictionary = _records[guest_id]
|
||||
if record[&"completed_at"] >= 0: continue
|
||||
var guest: GuestRuntime = _runtime.guest_service.get_guest(guest_id)
|
||||
if guest == null:
|
||||
_errors.append("Guest vanished before reaching destination: %s" % guest_id)
|
||||
continue
|
||||
if guest.current_cell != GOAL: continue
|
||||
record[&"completed_at"] = offset
|
||||
_trace.append({&"kind": &"COMPLETED", &"guest_id": guest_id, &"minute": offset})
|
||||
var leaving: Dictionary = _runtime.guest_service.begin_leaving(guest_id, _clock.absolute_minute, &"COMPARISON_DESTINATION_REACHED")
|
||||
var despawned: Dictionary = _runtime.guest_service.despawn_guest(guest_id, _clock.absolute_minute)
|
||||
if not leaving.get(&"ok", false) or not despawned.get(&"ok", false): _errors.append("Normal destination despawn failed: %s" % guest_id)
|
||||
if _runtime.path_service.has_actor_projection(StringName("guest:" + String(guest_id))): _errors.append("Completed actor retained Path projection: %s" % guest_id)
|
||||
_spawn_arrivals(offset)
|
||||
|
||||
|
||||
func _spawn_arrivals(offset: int) -> void:
|
||||
for row: Dictionary in _demand_rows:
|
||||
if row[&"arrived_at"] != offset: continue
|
||||
var guest_id: StringName = row[&"actor_id"]
|
||||
_records[guest_id] = {&"guest_id": guest_id, &"arrived_at": offset, &"requested_at": -1,
|
||||
&"boarded_at": -1, &"alighted_at": -1, &"completed_at": -1, &"walking_steps": 0, &"shaft_id": &""}
|
||||
var result: CommandResult = _runtime.submit_command(&"spawn_guest", {
|
||||
&"guest_id": guest_id, &"guest_type_id": &"traveler", &"gold": 100,
|
||||
&"options": {&"current_cell": START, &"state": GuestRuntime.State.MOVING,
|
||||
&"state_entered_at": _clock.absolute_minute - 1,
|
||||
&"planned_exit_at": _clock.absolute_minute + 2000,
|
||||
&"path_handle": {&"target_cell": GOAL}}},
|
||||
&"world", guest_id, 10, -1, StringName("comparison.spawn.%s" % guest_id))
|
||||
if not result.is_success(): _errors.append("Public spawn rejected: %s" % guest_id)
|
||||
_trace.append({&"kind": &"SPAWNED", &"guest_id": guest_id, &"minute": offset})
|
||||
|
||||
|
||||
func _subscribe() -> void:
|
||||
_runtime.domain_events.subscribe(&"*", &"transport.comparison", Callable(self, "_on_event"))
|
||||
|
||||
|
||||
func _on_event(event: SimulationDomainEvent) -> void:
|
||||
if event.event_type not in [&"GUEST_MOVED", &"ELEVATOR_TRIP_REQUESTED", &"ELEVATOR_BOARDED", &"ELEVATOR_ARRIVED"]: return
|
||||
var guest_id := StringName(String(event.entity_ids[0]).trim_prefix("guest:"))
|
||||
if not _records.has(guest_id): return
|
||||
var record: Dictionary = _records[guest_id]
|
||||
var offset := event.absolute_minute - _start_minute
|
||||
_trace.append({&"kind": event.event_type, &"guest_id": guest_id, &"minute": offset, &"payload": event.payload.duplicate(true)})
|
||||
match event.event_type:
|
||||
&"GUEST_MOVED":
|
||||
var from: Vector3i = event.payload[&"from_cell"]
|
||||
var to: Vector3i = event.payload[&"cell"]
|
||||
if from.y != to.y: _errors.append("Guest walked vertically without a cabin: %s" % guest_id)
|
||||
record[&"walking_steps"] += 1
|
||||
&"ELEVATOR_TRIP_REQUESTED":
|
||||
if record[&"requested_at"] >= 0: _errors.append("Journey requested more than one ride: %s" % guest_id)
|
||||
record[&"requested_at"] = offset
|
||||
record[&"shaft_id"] = event.payload[&"shaft_id"]
|
||||
&"ELEVATOR_BOARDED":
|
||||
record[&"boarded_at"] = offset
|
||||
if int(event.payload.get(&"wait_minutes", -1)) != offset - record[&"requested_at"]: _errors.append("Boarding event wait disagrees with observed request: %s" % guest_id)
|
||||
&"ELEVATOR_ARRIVED":
|
||||
record[&"alighted_at"] = offset
|
||||
if event.payload.get(&"emergency", false) or event.payload.get(&"cancelled", false): _errors.append("Healthy comparison ride unexpectedly cancelled: %s" % guest_id)
|
||||
|
||||
|
||||
func _check_replay(offset: int, label: StringName) -> void:
|
||||
var saved: Dictionary = _runtime.create_snapshot()
|
||||
var records := _records.duplicate(true)
|
||||
var trace := _trace.duplicate(true)
|
||||
var errors := _errors.duplicate()
|
||||
for step: int in range(offset + 1, offset + 13): _tick(step)
|
||||
var expected: Dictionary = _runtime.create_snapshot()
|
||||
var expected_records := _records.duplicate(true)
|
||||
var expected_trace := _trace.duplicate(true)
|
||||
var expected_errors := _errors.duplicate()
|
||||
var restored: bool = _runtime.restore_snapshot(saved)
|
||||
_expect(restored, "%s mid-journey aggregate Runtime snapshot restores" % label)
|
||||
if not restored: return
|
||||
_records = records.duplicate(true)
|
||||
_trace.assign(trace.duplicate(true))
|
||||
_errors.assign(errors)
|
||||
_subscribe()
|
||||
for step: int in range(offset + 1, offset + 13): _tick(step)
|
||||
_expect(_runtime.create_snapshot() == expected and _records == expected_records and _trace == expected_trace and _errors == expected_errors, "%s restore replays the next 12 minutes including scheduled spawns with exact state and event trace" % label)
|
||||
_expect(_runtime.restore_snapshot(saved), "%s checkpoint restores again before continuing the original observation window" % label)
|
||||
_records = records
|
||||
_trace.assign(trace)
|
||||
_errors.assign(errors)
|
||||
_subscribe()
|
||||
|
||||
|
||||
func _report(cabins: int, costs: Dictionary) -> Dictionary:
|
||||
var waits: Array[int] = []
|
||||
var journeys: Array[int] = []
|
||||
var rides: Array[int] = []
|
||||
var walks: Array[int] = []
|
||||
var completed: Array[Dictionary] = []
|
||||
var unfinished: Array[Dictionary] = []
|
||||
var usage: Dictionary = {}
|
||||
var waiting := 0
|
||||
var aboard := 0
|
||||
var detours := 0
|
||||
for guest_id: StringName in _records:
|
||||
var record: Dictionary = _records[guest_id]
|
||||
if record[&"boarded_at"] >= 0: waits.append(record[&"boarded_at"] - record[&"requested_at"])
|
||||
if record[&"alighted_at"] >= 0: rides.append(record[&"alighted_at"] - record[&"boarded_at"])
|
||||
if not String(record[&"shaft_id"]).is_empty(): usage[record[&"shaft_id"]] = int(usage.get(record[&"shaft_id"], 0)) + 1
|
||||
if record[&"completed_at"] >= 0:
|
||||
journeys.append(record[&"completed_at"] - record[&"arrived_at"])
|
||||
walks.append(record[&"walking_steps"])
|
||||
if record[&"walking_steps"] > 4: detours += 1
|
||||
completed.append(record.duplicate(true))
|
||||
else:
|
||||
var trip: Dictionary = _runtime.elevators.get_actor_trip(StringName("guest:" + String(guest_id)))
|
||||
var phase: StringName = trip.get(&"state", &"WALKING")
|
||||
if phase == PathService.ELEVATOR_REQUEST_QUEUED: waiting += 1
|
||||
if phase == PathService.ELEVATOR_REQUEST_BOARDED: aboard += 1
|
||||
unfinished.append({&"guest_id": guest_id, &"state": phase, &"age_minutes": WINDOW_MINUTES - record[&"arrived_at"],
|
||||
&"observed_wait_minutes": WINDOW_MINUTES - record[&"requested_at"] if phase == PathService.ELEVATOR_REQUEST_QUEUED else 0})
|
||||
walks.sort()
|
||||
return {&"shaft_count": cabins, &"costs": costs, &"seed": SEED, &"start_minute": _start_minute,
|
||||
&"window_minutes": WINDOW_MINUTES, &"demand_count": POPULATION, &"demand_hash": SimulationDigest.compute(_demand_rows),
|
||||
&"arrived_count": _records.size(), &"not_arrived_count": POPULATION - _records.size(),
|
||||
&"completed_count": completed.size(), &"unfinished_count": unfinished.size(), &"failed_count": _errors.size(),
|
||||
&"waiting_count": waiting, &"aboard_count": aboard, &"walking_count": unfinished.size() - waiting - aboard,
|
||||
&"queue_wait_minutes": _stats(waits), &"journey_minutes": _stats(journeys), &"ride_minutes": _stats(rides),
|
||||
&"shortest_walking_steps": 4, &"walking_steps": _stats(walks), &"detour_guest_count": detours,
|
||||
&"walking_steps_min": walks[0] if not walks.is_empty() else -1, &"walking_steps_max": walks[-1] if not walks.is_empty() else -1,
|
||||
&"shaft_usage": usage, &"completed_guests": completed, &"unfinished_guests": unfinished,
|
||||
&"event_trace_hash": SimulationDigest.compute(_trace), &"event_count": _trace.size()}
|
||||
|
||||
|
||||
func _stats(values: Array[int]) -> Dictionary:
|
||||
if values.is_empty(): return {&"samples": 0, &"mean": null, &"p95": null, &"max": null}
|
||||
var sorted := values.duplicate()
|
||||
sorted.sort()
|
||||
var total := 0
|
||||
for value: int in sorted: total += value
|
||||
return {&"samples": sorted.size(), &"mean": float(total) / sorted.size(), &"p95": sorted[ceili(sorted.size() * 0.95) - 1], &"max": sorted[-1]}
|
||||
|
||||
|
||||
func _is_improved(before: Dictionary, after: Dictionary) -> bool:
|
||||
return before[&"demand_hash"] == after[&"demand_hash"] and before[&"window_minutes"] == after[&"window_minutes"] \
|
||||
and before[&"completed_count"] == POPULATION and after[&"completed_count"] == POPULATION \
|
||||
and before[&"failed_count"] == 0 and after[&"failed_count"] == 0 \
|
||||
and before[&"queue_wait_minutes"][&"samples"] == POPULATION and after[&"queue_wait_minutes"][&"samples"] == POPULATION \
|
||||
and after[&"queue_wait_minutes"][&"p95"] < before[&"queue_wait_minutes"][&"p95"] \
|
||||
and after[&"journey_minutes"][&"p95"] < before[&"journey_minutes"][&"p95"]
|
||||
|
||||
|
||||
func _summary(report: Dictionary) -> Dictionary:
|
||||
return {&"cabins": report[&"shaft_count"], &"completed": report[&"completed_count"], &"unfinished": report[&"unfinished_count"],
|
||||
&"wait": report[&"queue_wait_minutes"], &"journey": report[&"journey_minutes"], &"walking_steps_min": report[&"walking_steps_min"],
|
||||
&"walking_steps_max": report[&"walking_steps_max"], &"walking_steps": report[&"walking_steps"], &"detour_guest_count": report[&"detour_guest_count"], &"shaft_usage": report[&"shaft_usage"], &"trace_hash": report[&"event_trace_hash"]}
|
||||
|
||||
|
||||
func _dispose() -> void:
|
||||
_runtime.free()
|
||||
_clock.free()
|
||||
_state.free()
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if not condition: _failures += 1
|
||||
print("%s: %s" % ["PASS" if condition else "FAIL", label])
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
print("Runtime transport comparison: %d checks, %d failures" % [_checks, _failures])
|
||||
quit(0 if _failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ct2kere2fgrfe
|
||||
@@ -0,0 +1,271 @@
|
||||
extends SceneTree
|
||||
|
||||
## MOVE-06 component comparison: fixed arrival demand, actual public construction, and the
|
||||
## production Path/Elevator services. These are measurement actors, not the
|
||||
## economic Guest AI. Route choice is committed once at common-lobby arrival.
|
||||
## Both scenarios use Path's production explicit floor-connector policy.
|
||||
const SEED := 806042
|
||||
const POPULATION := 48
|
||||
const WINDOW_MINUTES := 480
|
||||
const START := Vector3i(2, 0, 0)
|
||||
const GOAL := Vector3i(2, 1, 0)
|
||||
const CREDIT_SCALE := 1000
|
||||
const REPORT_PATH := "res://artifacts/transport/path-comparison.json"
|
||||
const DEMAND_PATH := "res://artifacts/transport/comparison-demand.json"
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var global_clock := root.get_node_or_null("GameClock")
|
||||
if global_clock != null: global_clock.set_paused(true)
|
||||
var demand := _demand()
|
||||
_expect(demand.size() == POPULATION and SimulationDigest.compute(demand) == SimulationDigest.compute(_demand()), "seed fixes all 48 actor IDs, arrival minutes, origins and destinations")
|
||||
var one := _prepare(1)
|
||||
var two := _prepare(2)
|
||||
if one.is_empty() or two.is_empty():
|
||||
_finish()
|
||||
return
|
||||
var before := _observe(one, demand, WINDOW_MINUTES)
|
||||
var after := _observe(two, demand, WINDOW_MINUTES)
|
||||
_expect(before[&"demand_hash"] == after[&"demand_hash"] and before[&"demand_count"] == after[&"demand_count"], "both configurations replay the identical demand manifest")
|
||||
_expect(before[&"window_minutes"] == after[&"window_minutes"], "both configurations use the same fixed observation window")
|
||||
_expect(before[&"arrived_count"] == POPULATION and after[&"arrived_count"] == POPULATION, "no passengers are removed to make the improved layout faster")
|
||||
_expect(before[&"completed_count"] + before[&"unfinished_count"] == POPULATION and after[&"completed_count"] + after[&"unfinished_count"] == POPULATION, "completed and censored unfinished populations account for every arrival")
|
||||
_expect(before[&"failed_count"] == 0 and after[&"failed_count"] == 0, "every measurement actor has a legal route and conserved Path ownership")
|
||||
_expect(before[&"walking_steps_min"] == 4 and before[&"walking_steps_max"] == 4 and after[&"walking_steps_min"] == 4 and after[&"walking_steps_max"] == 4, "left and right boarding positions have the same four horizontal walking steps")
|
||||
_expect(after[&"shaft_usage"].size() == 2, "public shortest-path costs distribute demand to both installed cabins")
|
||||
_expect(before[&"completed_count"] == POPULATION and after[&"completed_count"] == POPULATION, "both layouts complete the entire cohort inside the fixed window")
|
||||
_expect(_is_improved(before, after), "second identical cabin improves wait and journey P95 only after both layouts complete equal demand")
|
||||
var replay := _observe(one, demand, WINDOW_MINUTES)
|
||||
_expect(replay == before, "same topology snapshot and seed reproduce all measured outcomes exactly")
|
||||
var censored := _observe(two, demand, 30)
|
||||
_expect(censored[&"unfinished_count"] > 0 and not _is_improved(before, censored), "short-window incomplete observations cannot masquerade as an improvement")
|
||||
var report := {
|
||||
&"scope": "Fixed-demand Path/Elevator actor harness; not natural economic Guest AI or full S14 congestion acceptance.",
|
||||
&"route_policy": "Public find_path at common-lobby arrival; commit that route for this journey; real shared-cabin boarding and minute ticks.",
|
||||
&"topology_policy": "Production explicit vertical connectors; no benchmark floor-separation blockers.",
|
||||
&"seed": SEED, &"demand_count": POPULATION, &"observation_minutes": WINDOW_MINUTES,
|
||||
&"demand_hash": SimulationDigest.compute(demand), &"before": before, &"after": after,
|
||||
&"equipment_cost_difference": {
|
||||
&"gold": two[&"costs"][&"gold"] - one[&"costs"][&"gold"],
|
||||
&"materials": two[&"costs"][&"materials"] - one[&"costs"][&"materials"],
|
||||
&"maintenance_mana_demand": two[&"costs"][&"maintenance_mana_demand"] - one[&"costs"][&"maintenance_mana_demand"]},
|
||||
&"improvement_accepted": _is_improved(before, after), &"checks": _checks, &"failures": _failures}
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path("res://artifacts/transport"))
|
||||
var output := FileAccess.open(REPORT_PATH, FileAccess.WRITE)
|
||||
if output != null: output.store_string(JSON.stringify(report, "\t") + "\n")
|
||||
var manifest := FileAccess.open(DEMAND_PATH, FileAccess.WRITE)
|
||||
if manifest != null: manifest.store_string(JSON.stringify(demand, "\t") + "\n")
|
||||
print("TRANSPORT_COMPARISON ", JSON.stringify({&"before": _summary(before), &"after": _summary(after), &"improved": report[&"improvement_accepted"]}))
|
||||
_finish()
|
||||
|
||||
|
||||
func _demand() -> Array[Dictionary]:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = SEED
|
||||
var result: Array[Dictionary] = []
|
||||
for index: int in range(POPULATION):
|
||||
result.append({&"actor_id": StringName("comparison/%03d" % index),
|
||||
&"arrived_at": (index / 4) * 15 + rng.randi_range(0, 2),
|
||||
&"from": [START.x, START.y, START.z], &"to": [GOAL.x, GOAL.y, GOAL.z]})
|
||||
result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
if a[&"arrived_at"] != b[&"arrived_at"]: return a[&"arrived_at"] < b[&"arrived_at"]
|
||||
return str(a[&"actor_id"]) < str(b[&"actor_id"]))
|
||||
return result
|
||||
|
||||
|
||||
func _prepare(cabins: int) -> Dictionary:
|
||||
var runtime: Variant = load("res://autoload/simulation_runtime.gd").new()
|
||||
var clock: Variant = load("res://autoload/game_clock.gd").new()
|
||||
var state: Variant = load("res://autoload/game_state.gd").new()
|
||||
clock.reset(1, 6, 0)
|
||||
state.reset()
|
||||
_expect(runtime.initialize(clock, state, SEED)[&"valid"], "%d-cabin fixture initializes with normal 5000G/50 materials" % cabins)
|
||||
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
||||
clock.set_paused(true)
|
||||
var builds: Array = [
|
||||
[&"guest_elevator", Vector3i.ZERO],
|
||||
[&"guest_corridor", Vector3i(2, 1, 0)],
|
||||
[&"guest_corridor", Vector3i(3, 1, 0)]]
|
||||
if cabins == 2: builds.append([&"guest_elevator", Vector3i(4, 0, 0)])
|
||||
var success := true
|
||||
for index: int in range(builds.size()):
|
||||
var result: CommandResult = runtime.submit_command(&"build_facility", {
|
||||
&"definition_id": builds[index][0], &"origin": builds[index][1]},
|
||||
&"player", &"", 0, clock.absolute_minute, StringName("comparison.build.%d" % index))
|
||||
_expect(result.is_success(), "%d-cabin case pays for %s at %s" % [cabins, builds[index][0], builds[index][1]])
|
||||
success = success and result.is_success()
|
||||
clock.advance_minutes(310)
|
||||
var report: Dictionary = runtime.elevators.get_report()
|
||||
_expect(report[&"shafts"].size() == cabins, "%d-cabin construction physically completes" % cabins)
|
||||
success = success and report[&"shafts"].size() == cabins
|
||||
var maintenance := 0
|
||||
for shaft: Dictionary in report[&"shafts"]:
|
||||
_expect(shaft[&"operational"] and shaft[&"capacity"] == 4 and shaft[&"travel_minutes_per_floor"] == 10, "comparison cabins have identical power, capacity and speed")
|
||||
for module: StringName in shaft[&"module_ids"]:
|
||||
var facility: Dictionary = runtime.construction_service.get_facility(module)
|
||||
var definition: FacilityDefinition = runtime.definition_registry.get_facility(facility[&"definition_id"])
|
||||
maintenance += definition.maintenance_mana
|
||||
var result := {&"shaft_count": cabins, &"minute": clock.absolute_minute,
|
||||
&"cells": runtime.route_subslot_path_adapter.get_projected_walkable_cells(),
|
||||
&"path_snapshot": runtime.path_service.create_snapshot(),
|
||||
&"elevator_snapshot": runtime.elevators.create_snapshot(),
|
||||
&"costs": {&"gold": 5000 - state.get_resource(&"gold"), &"materials": 50 - state.get_resource(&"materials"),
|
||||
&"maintenance_mana_demand": maintenance}}
|
||||
runtime.free()
|
||||
clock.free()
|
||||
state.free()
|
||||
return result if success else {}
|
||||
|
||||
|
||||
func _observe(prepared: Dictionary, demand: Array[Dictionary], window: int) -> Dictionary:
|
||||
var path := PathService.new()
|
||||
path.rebuild_graph(prepared[&"cells"])
|
||||
path.restore_snapshot(prepared[&"path_snapshot"])
|
||||
_expect(path.get_vertical_walk_requires_connector(), "prepared Runtime uses production strict floor routing without benchmark overrides")
|
||||
var elevators := ElevatorService.new()
|
||||
elevators.configure(path)
|
||||
elevators.restore_snapshot(prepared[&"elevator_snapshot"])
|
||||
var actors: Dictionary = {}
|
||||
var shaft_usage: Dictionary = {}
|
||||
var completed: Array[Dictionary] = []
|
||||
var failed := 0
|
||||
var start_minute: int = prepared[&"minute"]
|
||||
for offset: int in range(window + 1):
|
||||
var minute := start_minute + offset
|
||||
if offset > 0:
|
||||
for arrival: Dictionary in elevators.advance_one_minute(minute):
|
||||
var actor: Dictionary = actors[arrival[&"actor_id"]]
|
||||
actor[&"cell"] = arrival[&"cell"]
|
||||
actor[&"route_index"] += 1
|
||||
actor[&"alighted_at"] = offset
|
||||
actor[&"next_move_at"] = offset + 1
|
||||
path.project_actor_position(arrival[&"actor_id"], arrival[&"cell"], minute)
|
||||
for row: Dictionary in demand:
|
||||
if row[&"arrived_at"] != offset: continue
|
||||
var actor_id: StringName = row[&"actor_id"]
|
||||
path.actor_enter_cell(actor_id, START, minute)
|
||||
var route := path.find_path(START, GOAL, [0])
|
||||
var actor := {&"actor_id": actor_id, &"arrived_at": offset, &"cell": START,
|
||||
&"route": route, &"route_index": 0, &"requested_at": -1, &"boarded_at": -1,
|
||||
&"alighted_at": -1, &"completed_at": -1, &"walking_steps": 0,
|
||||
&"credit": 0, &"next_move_at": offset + 1, &"failed": route.is_empty()}
|
||||
actors[actor_id] = actor
|
||||
if route.is_empty(): failed += 1
|
||||
var actor_ids: Array = actors.keys()
|
||||
actor_ids.sort()
|
||||
for actor_id: StringName in actor_ids:
|
||||
var actor: Dictionary = actors[actor_id]
|
||||
if actor[&"failed"] or actor[&"completed_at"] >= 0: continue
|
||||
var trip := elevators.get_actor_trip(actor_id)
|
||||
if not trip.is_empty():
|
||||
if trip[&"state"] == PathService.ELEVATOR_REQUEST_BOARDED and actor[&"boarded_at"] < 0:
|
||||
actor[&"boarded_at"] = offset
|
||||
continue
|
||||
if actor[&"cell"] == GOAL:
|
||||
actor[&"completed_at"] = offset
|
||||
completed.append(actor.duplicate(true))
|
||||
path.despawn_actor(actor_id)
|
||||
continue
|
||||
if actor[&"next_move_at"] > offset: continue
|
||||
var route: Array = actor[&"route"]
|
||||
var index: int = actor[&"route_index"]
|
||||
var next: Vector3i = route[index + 1]
|
||||
var connector := path.get_step_connector(actor[&"cell"], next)
|
||||
if elevators.owns_connector(connector):
|
||||
var request := elevators.request_trip(actor_id, actor[&"cell"], next, minute)
|
||||
if not request[&"ok"]:
|
||||
actor[&"failed"] = true
|
||||
failed += 1
|
||||
else:
|
||||
actor[&"requested_at"] = offset
|
||||
var shaft: StringName = request[&"trip"][&"shaft_id"]
|
||||
shaft_usage[shaft] = int(shaft_usage.get(shaft, 0)) + 1
|
||||
continue
|
||||
actor[&"credit"] += roundi(path.get_speed_multiplier(actor[&"cell"]) * CREDIT_SCALE)
|
||||
if actor[&"credit"] < CREDIT_SCALE: continue
|
||||
if not path.project_actor_position(actor_id, next, minute):
|
||||
actor[&"failed"] = true
|
||||
failed += 1
|
||||
continue
|
||||
actor[&"cell"] = next
|
||||
actor[&"route_index"] += 1
|
||||
actor[&"walking_steps"] += 1
|
||||
actor[&"credit"] -= CREDIT_SCALE
|
||||
if next == GOAL:
|
||||
actor[&"completed_at"] = offset
|
||||
completed.append(actor.duplicate(true))
|
||||
path.despawn_actor(actor_id)
|
||||
var waits: Array[int] = []
|
||||
var journeys: Array[int] = []
|
||||
var rides: Array[int] = []
|
||||
var walking_steps: Array[int] = []
|
||||
var unfinished: Array[Dictionary] = []
|
||||
var waiting := 0
|
||||
var aboard := 0
|
||||
var walking := 0
|
||||
for actor_id: StringName in actors:
|
||||
var actor: Dictionary = actors[actor_id]
|
||||
if actor[&"boarded_at"] >= 0: waits.append(actor[&"boarded_at"] - actor[&"requested_at"])
|
||||
if actor[&"alighted_at"] >= 0: rides.append(actor[&"alighted_at"] - actor[&"boarded_at"])
|
||||
if actor[&"completed_at"] >= 0:
|
||||
journeys.append(actor[&"completed_at"] - actor[&"arrived_at"])
|
||||
walking_steps.append(actor[&"walking_steps"])
|
||||
else:
|
||||
var state: StringName = elevators.get_actor_trip(actor_id).get(&"state", &"WALKING")
|
||||
if state == PathService.ELEVATOR_REQUEST_QUEUED: waiting += 1
|
||||
elif state == PathService.ELEVATOR_REQUEST_BOARDED: aboard += 1
|
||||
else: walking += 1
|
||||
unfinished.append({&"actor_id": actor_id, &"state": state, &"age_minutes": window - actor[&"arrived_at"],
|
||||
&"observed_wait_minutes": window - actor[&"requested_at"] if state == PathService.ELEVATOR_REQUEST_QUEUED else 0})
|
||||
walking_steps.sort()
|
||||
return {&"shaft_count": prepared[&"shaft_count"], &"costs": prepared[&"costs"],
|
||||
&"window_minutes": window, &"demand_count": demand.size(), &"demand_hash": SimulationDigest.compute(demand),
|
||||
&"arrived_count": actors.size(), &"not_arrived_count": demand.size() - actors.size(),
|
||||
&"completed_count": completed.size(), &"unfinished_count": actors.size() - completed.size(),
|
||||
&"waiting_count": waiting, &"aboard_count": aboard, &"walking_count": walking, &"failed_count": failed,
|
||||
&"queue_wait_minutes": _stats(waits), &"journey_minutes": _stats(journeys), &"ride_minutes": _stats(rides),
|
||||
&"walking_steps_min": walking_steps[0] if not walking_steps.is_empty() else -1,
|
||||
&"walking_steps_max": walking_steps[-1] if not walking_steps.is_empty() else -1,
|
||||
&"shaft_usage": shaft_usage, &"completed_actors": completed, &"unfinished_actors": unfinished}
|
||||
|
||||
|
||||
func _stats(values: Array[int]) -> Dictionary:
|
||||
if values.is_empty(): return {&"samples": 0, &"mean": null, &"p95": null, &"max": null}
|
||||
var sorted := values.duplicate()
|
||||
sorted.sort()
|
||||
var total := 0
|
||||
for value: int in sorted: total += value
|
||||
return {&"samples": sorted.size(), &"mean": float(total) / sorted.size(),
|
||||
&"p95": sorted[maxi(0, ceili(sorted.size() * 0.95) - 1)], &"max": sorted[-1]}
|
||||
|
||||
|
||||
func _is_improved(before: Dictionary, after: Dictionary) -> bool:
|
||||
return before[&"demand_hash"] == after[&"demand_hash"] and before[&"window_minutes"] == after[&"window_minutes"] \
|
||||
and before[&"completed_count"] == before[&"demand_count"] and after[&"completed_count"] == before[&"completed_count"] \
|
||||
and before[&"failed_count"] == 0 and after[&"failed_count"] == 0 \
|
||||
and after[&"queue_wait_minutes"][&"p95"] < before[&"queue_wait_minutes"][&"p95"] \
|
||||
and after[&"journey_minutes"][&"p95"] < before[&"journey_minutes"][&"p95"]
|
||||
|
||||
|
||||
func _summary(report: Dictionary) -> Dictionary:
|
||||
return {&"cabins": report[&"shaft_count"], &"completed": report[&"completed_count"],
|
||||
&"unfinished": report[&"unfinished_count"], &"wait": report[&"queue_wait_minutes"],
|
||||
&"journey": report[&"journey_minutes"], &"shaft_usage": report[&"shaft_usage"]}
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if not condition: _failures += 1
|
||||
print("%s: %s" % ["PASS" if condition else "FAIL", label])
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
print("Path and cabin comparison: %d checks, %d failures" % [_checks, _failures])
|
||||
quit(0 if _failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b5fq4vxbrlmdn
|
||||
@@ -0,0 +1,157 @@
|
||||
extends SceneTree
|
||||
|
||||
## A genuine pre-policy Path4 saved every boarded actor in floor density.
|
||||
## Rebuild that old representation from five publicly spawned Runtime guests,
|
||||
## then verify migration and cross-service rollback with occupied physical cars.
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
var _serial := 0
|
||||
var _runtime: Variant
|
||||
var _clock: Variant
|
||||
var _state: Variant
|
||||
var _arrivals: Dictionary = {}
|
||||
var _original_requests: Dictionary = {}
|
||||
var _arrival_errors: Array[String] = []
|
||||
const GOAL := Vector3i(0, 1, 0)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var global_clock := root.get_node_or_null("GameClock")
|
||||
if global_clock != null: global_clock.set_paused(true)
|
||||
_runtime = load("res://autoload/simulation_runtime.gd").new()
|
||||
_clock = load("res://autoload/game_clock.gd").new()
|
||||
_state = load("res://autoload/game_state.gd").new()
|
||||
_clock.reset(1, 6, 0)
|
||||
_state.reset()
|
||||
_expect(_runtime.initialize(_clock, _state, 90583)[&"valid"], "density migration fixture initializes")
|
||||
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
||||
_clock.set_paused(true)
|
||||
for build: Array in [[&"guest_elevator", Vector3i.ZERO], [&"guest_corridor", Vector3i(4, 1, 0)], [&"guest_corridor", Vector3i(4, 2, 0)]]:
|
||||
_expect(_command(&"build_facility", {&"definition_id": build[0], &"origin": build[1]}).is_success(), "normal construction pays for migration topology")
|
||||
_clock.advance_minutes(10)
|
||||
_clock.advance_minutes(280)
|
||||
for index: int in range(5):
|
||||
var id := StringName("density.guest.%d" % index)
|
||||
_expect(_command(&"spawn_guest", {&"guest_id": id, &"guest_type_id": &"traveler", &"gold": 100,
|
||||
&"options": {&"current_cell": Vector3i.ZERO, &"state": GuestRuntime.State.MOVING,
|
||||
&"state_entered_at": _clock.absolute_minute - 1, &"planned_exit_at": _clock.absolute_minute + 2000,
|
||||
&"path_handle": {&"target_cell": GOAL}}}).is_success(), "public Runtime spawn creates actual passenger %d" % index)
|
||||
_clock.advance_minutes(2)
|
||||
var car: Dictionary = _runtime.elevators.get_report()[&"shafts"][0]
|
||||
_expect(car[&"phase"] == &"BOARDING" and car[&"passenger_count"] == 4 and car[&"waiting_count"] == 1, "four real guests board while the fifth waits at the same floor")
|
||||
_expect(_runtime.path_service.get_current_density(Vector3i.ZERO) == 1 and _runtime.path_service.get_transit_actor_ids().size() == 4, "new physical floor density excludes four occupied cabin seats")
|
||||
var statistics: Dictionary = _runtime.get_elevator_wait_statistics(car[&"shaft_id"])
|
||||
_expect(statistics[&"sample_count"] == 4 and statistics[&"mean_wait_minutes"] == 1.0 and statistics[&"p95_wait_minutes"] == 1 and statistics[&"oldest_wait_minutes"] == 1, "live wait read model reports real boarding events and current queue age")
|
||||
var current: Dictionary = _runtime.create_snapshot()
|
||||
_expect(_runtime.restore_snapshot(current) and _runtime.get_elevator_wait_statistics(car[&"shaft_id"]) == statistics, "wait statistics survive actual Runtime restore without duplicate samples")
|
||||
var legacy := _as_v8_path4(current)
|
||||
var data: Dictionary = legacy[&"systems"][&"runtime"]
|
||||
var old_path: Dictionary = data[&"path"]
|
||||
var old_density := 0
|
||||
for cell: Dictionary in old_path[&"cell_traffic"]:
|
||||
if cell[&"cell"] == Vector3i.ZERO: old_density = cell[&"current_density"]
|
||||
_expect(old_density == 5 and not old_path.has(&"transit_actor_ids") and not old_path.has(&"vertical_walk_requires_connector") and not data.has(&"legacy_stairs"), "legacy fixture has genuine Path4 fields and all five old boarding-anchor occupants")
|
||||
var forged := legacy.duplicate(true)
|
||||
for cell: Dictionary in forged[&"systems"][&"runtime"][&"path"][&"cell_traffic"]:
|
||||
if cell[&"cell"] == Vector3i.ZERO: cell[&"current_density"] = 1
|
||||
_expect(_rejects(forged), "v8 relabel with new excluded-passenger density is rejected before migration")
|
||||
_expect(_runtime.restore_snapshot(legacy), "actual v8 plus Path4 aggregate migrates occupied car and queued passengers")
|
||||
_expect(_runtime.get_elevator_wait_statistics(car[&"shaft_id"])[&"sample_count"] == 0 and _runtime.get_elevator_wait_statistics(car[&"shaft_id"])[&"oldest_wait_minutes"] == 1, "legacy load shows no fabricated boarding samples but retains the actual queue age")
|
||||
_expect(_runtime.elevators.create_snapshot() == data[&"elevators"], "migration preserves cabin phase, seats, queue IDs, travel clocks and operator power exactly")
|
||||
_expect(_runtime.path_service.get_current_density(Vector3i.ZERO) == 1 and _runtime.path_service.get_transit_actor_ids().size() == 4, "migration recomputes floor density without removing any actor or cabin seat")
|
||||
_expect(_runtime.path_service.get_vertical_walk_requires_connector() and _runtime.legacy_stairs.create_snapshot()[&"stairs"].size() == 2, "same migration materializes two old corridor stairs and closes implicit vertical walking")
|
||||
_expect(_runtime.path_service.find_path(Vector3i(4, 0, 0), Vector3i(4, 2, 0)).size() == 3, "old corridor vertical reachability remains available after policy migration")
|
||||
for guest: GuestRuntime in _runtime.guest_service.get_guests():
|
||||
_expect(guest.current_cell == Vector3i.ZERO and _runtime.path_service.get_actor_projected_cell(StringName("guest:" + String(guest.guest_id))) == Vector3i.ZERO, "migration preserves actual passenger's logical boarding anchor")
|
||||
var canonical: Dictionary = _runtime.create_snapshot()
|
||||
_expect(canonical[&"systems"][&"runtime"][&"runtime_schema_version"] == 9 and canonical[&"systems"][&"runtime"][&"path"][&"schema_version"] == 5, "resave emits current Runtime9 and Path5 only")
|
||||
_expect(_runtime.restore_snapshot(canonical) and _runtime.create_snapshot() == canonical, "canonical migrated save reloads exactly without applying density or stairs twice")
|
||||
forged = canonical.duplicate(true)
|
||||
forged[&"systems"][&"runtime"][&"path"][&"transit_actor_ids"].pop_back()
|
||||
_expect(_rejects(forged), "current transit list cannot omit a real boarded guest")
|
||||
forged = canonical.duplicate(true)
|
||||
for cell: Dictionary in forged[&"systems"][&"runtime"][&"path"][&"cell_traffic"]:
|
||||
if cell[&"cell"] == Vector3i.ZERO: cell[&"current_density"] = 5
|
||||
_expect(_rejects(forged), "current save cannot restore old double-counted cabin density")
|
||||
forged = canonical.duplicate(true)
|
||||
forged[&"systems"][&"runtime"][&"legacy_stairs"][&"stairs"].pop_back()
|
||||
_expect(_rejects(forged), "migrated occupied-car snapshot rejects orphan legacy connector metadata")
|
||||
var token: Dictionary = _runtime._capture_event_action_token()
|
||||
var first_guest := &"density.guest.0"
|
||||
_expect(_runtime.guest_service.begin_leaving(first_guest, _clock.absolute_minute, &"TEST_EVENT_EXIT")[&"ok"] and _runtime.guest_service.despawn_guest(first_guest, _clock.absolute_minute)[&"ok"], "event rollback fixture removes a real boarded actor through normal lifecycle")
|
||||
_expect(_runtime._restore_event_action_token(token)[&"ok"] and _runtime.create_snapshot() == canonical, "event action rollback restores guests, seats, Path density, stair ownership and all snapshot participants exactly")
|
||||
for trip: Dictionary in _runtime.elevators.get_report()[&"trips"]:
|
||||
_original_requests[trip[&"request_id"]] = trip[&"actor_id"]
|
||||
_runtime.domain_events.subscribe(&"ELEVATOR_ARRIVED", &"density.migration.arrivals", Callable(self, "_on_arrival"))
|
||||
for step: int in range(80):
|
||||
_clock.advance_minutes(1)
|
||||
for guest: GuestRuntime in _runtime.guest_service.get_guests():
|
||||
if guest.current_cell == GOAL:
|
||||
_runtime.guest_service.begin_leaving(guest.guest_id, _clock.absolute_minute, &"TEST_DESTINATION_REACHED")
|
||||
_runtime.guest_service.despawn_guest(guest.guest_id, _clock.absolute_minute)
|
||||
var exactly_once := _arrivals.size() == 5
|
||||
for request_id: StringName in _original_requests:
|
||||
exactly_once = exactly_once and _arrivals.get(request_id, 0) == 1
|
||||
_expect(exactly_once and _arrival_errors.is_empty(), "all five migrated request IDs reach the real destination exactly once")
|
||||
_expect(_runtime.guest_service.get_guests().is_empty() and _runtime.elevators.get_report()[&"trips"].is_empty() and _runtime.path_service.get_transit_actor_ids().is_empty(), "completed lifecycle leaves no orphan guest, Path transit actor or elevator trip")
|
||||
var final_state: Dictionary = _runtime.create_snapshot()
|
||||
_expect(_runtime.restore_snapshot(final_state) and _runtime.create_snapshot() == final_state, "post-arrival migrated world remains strictly canonical")
|
||||
_runtime.free()
|
||||
_clock.free()
|
||||
_state.free()
|
||||
print("Transport density migration: %d checks, %d failures" % [_checks, _failures])
|
||||
quit(1 if _failures else 0)
|
||||
|
||||
|
||||
func _as_v8_path4(current: Dictionary) -> Dictionary:
|
||||
var result := current.duplicate(true)
|
||||
var data: Dictionary = result[&"systems"][&"runtime"]
|
||||
data[&"runtime_schema_version"] = 8
|
||||
data.erase(&"legacy_stairs")
|
||||
var path: Dictionary = data[&"path"]
|
||||
path[&"schema_version"] = 4
|
||||
path.erase(&"vertical_walk_requires_connector")
|
||||
path.erase(&"transit_actor_ids")
|
||||
var old_densities: Dictionary = {}
|
||||
for actor: Dictionary in path[&"actor_cells"]:
|
||||
old_densities[actor[&"cell"]] = int(old_densities.get(actor[&"cell"], 0)) + 1
|
||||
for cell: Dictionary in path[&"cell_traffic"]:
|
||||
cell[&"current_density"] = int(old_densities.get(cell[&"cell"], 0))
|
||||
# V8 had no boarding telemetry event. Stable ID high-water marks remain
|
||||
# monotonic; retiring those observer-only events does not renumber history.
|
||||
var events: Dictionary = data[&"domain_events"]
|
||||
var retired: Array = []
|
||||
for event: Dictionary in events["recent_events"]:
|
||||
if event["event_type"] == "ELEVATOR_BOARDED": retired.append(event["event_id"])
|
||||
events["recent_events"] = events["recent_events"].filter(func(event: Dictionary) -> bool: return event["event_id"] not in retired)
|
||||
events["published_ids"] = events["published_ids"].filter(func(id: String) -> bool: return id not in retired)
|
||||
return result
|
||||
|
||||
|
||||
func _on_arrival(event: SimulationDomainEvent) -> void:
|
||||
var request_id: StringName = event.payload[&"request_id"]
|
||||
if not _original_requests.has(request_id):
|
||||
_arrival_errors.append("Unrequested duplicate ride: %s" % request_id)
|
||||
return
|
||||
_arrivals[request_id] = int(_arrivals.get(request_id, 0)) + 1
|
||||
if event.payload[&"cell"] != GOAL or event.payload.get(&"emergency", false): _arrival_errors.append("Unexpected migrated destination")
|
||||
|
||||
|
||||
func _command(type: StringName, payload: Dictionary) -> CommandResult:
|
||||
_serial += 1
|
||||
return _runtime.submit_command(type, payload, &"world", &"", 0, -1, StringName("density.migration.%d" % _serial))
|
||||
|
||||
|
||||
func _rejects(candidate: Dictionary) -> bool:
|
||||
var before: Dictionary = _runtime.create_snapshot()
|
||||
return not _runtime.restore_snapshot(candidate) and _runtime.create_snapshot() == before
|
||||
|
||||
|
||||
func _expect(ok: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if not ok: _failures += 1
|
||||
print("%s: %s" % ["PASS" if ok else "FAIL", label])
|
||||
@@ -0,0 +1 @@
|
||||
uid://dv2w40du2y6mx
|
||||
@@ -15,21 +15,28 @@ func _run() -> void:
|
||||
var clock: Variant = fixture[&"clock"]
|
||||
clock.advance_minutes(3)
|
||||
var current: Dictionary = runtime.create_snapshot()
|
||||
_expect(current[&"systems"][&"runtime"][&"runtime_schema_version"] == 8, "Runtime emits schema8 with elevator aggregate")
|
||||
_expect(current[&"systems"][&"runtime"][&"runtime_schema_version"] == 9, "Runtime emits schema9 with elevator and explicit vertical-route aggregates")
|
||||
var legacy := current.duplicate(true)
|
||||
var data: Dictionary = legacy[&"systems"][&"runtime"]
|
||||
data[&"runtime_schema_version"] = 7
|
||||
data.erase(&"elevators")
|
||||
data.erase(&"legacy_stairs")
|
||||
data[&"path"][&"schema_version"] = 3
|
||||
data[&"path"].erase(&"vertical_walk_blockers")
|
||||
_expect(runtime.restore_snapshot(legacy), "genuine v7 without lifts migrates from Path3 into Runtime8")
|
||||
_expect(runtime.create_snapshot() == current, "empty-lift migration preserves every existing system exactly")
|
||||
data[&"path"].erase(&"vertical_walk_requires_connector")
|
||||
data[&"path"].erase(&"transit_actor_ids")
|
||||
_expect(runtime.restore_snapshot(legacy), "genuine v7 without lifts migrates from Path3 into Runtime9")
|
||||
var migrated: Dictionary = runtime.create_snapshot()
|
||||
var expected := current.duplicate(true)
|
||||
# Closing the old implicit-WALK topology bumps only the Path graph revision.
|
||||
expected[&"systems"][&"runtime"][&"path"][&"graph_revision"] += 1
|
||||
_expect(migrated == expected, "empty-lift migration preserves all state except the explicit topology revision")
|
||||
var bad := current.duplicate(true)
|
||||
bad[&"systems"][&"runtime"][&"runtime_schema_version"] = 7
|
||||
_expect(_rejects(runtime, bad), "outer-only downgrade retaining elevator state is rejected atomically")
|
||||
bad = current.duplicate(true)
|
||||
bad[&"systems"][&"runtime"].erase(&"elevators")
|
||||
_expect(_rejects(runtime, bad), "Runtime8 cannot omit elevator aggregate")
|
||||
_expect(_rejects(runtime, bad), "Runtime9 cannot omit elevator aggregate")
|
||||
bad = current.duplicate(true)
|
||||
bad[&"systems"][&"runtime"][&"elevators"][&"current_minute"] -= 1
|
||||
_expect(_rejects(runtime, bad), "elevator clock must match Runtime domain minute")
|
||||
@@ -53,7 +60,7 @@ func _run() -> void:
|
||||
var trip: Dictionary = runtime.elevators.get_actor_trip(actor_id)
|
||||
_expect(trip.get(&"state") == PathService.ELEVATOR_REQUEST_BOARDED, "save fixture owns a physical cabin seat")
|
||||
current = runtime.create_snapshot()
|
||||
_expect(runtime.restore_snapshot(current), "Runtime8 restores actual cabin actor and projected origin crosslinks")
|
||||
_expect(runtime.restore_snapshot(current), "Runtime9 restores actual cabin actor and projected origin crosslinks")
|
||||
bad = current.duplicate(true)
|
||||
bad[&"systems"][&"runtime"][&"elevators"][&"trips"][0][&"actor_id"] = &"staff:missing_person"
|
||||
bad[&"systems"][&"runtime"][&"elevators"][&"shafts"][0][&"boarded"][0] = &"staff:missing_person"
|
||||
@@ -100,13 +107,16 @@ func _test_attendance_migration() -> void:
|
||||
worker.state = StaffRuntime.State.AVAILABLE
|
||||
runtime.facility_service.set_operational_status(facility.instance_id, {&"active_staff_ids": [&"staff_akane"]}, clock.absolute_minute)
|
||||
var logical: Dictionary = runtime.create_snapshot()
|
||||
_expect(_rejects(runtime, logical), "Runtime8 rejects an AVAILABLE employee counted at a distant workplace")
|
||||
_expect(_rejects(runtime, logical), "Runtime9 rejects an AVAILABLE employee counted at a distant workplace")
|
||||
var legacy := logical.duplicate(true)
|
||||
var data: Dictionary = legacy[&"systems"][&"runtime"]
|
||||
data[&"runtime_schema_version"] = 7
|
||||
data.erase(&"elevators")
|
||||
data.erase(&"legacy_stairs")
|
||||
data[&"path"][&"schema_version"] = 3
|
||||
data[&"path"].erase(&"vertical_walk_blockers")
|
||||
data[&"path"].erase(&"vertical_walk_requires_connector")
|
||||
data[&"path"].erase(&"transit_actor_ids")
|
||||
_expect(runtime.restore_snapshot(legacy), "genuine v7 logical attendance migrates into physical attendance")
|
||||
facility = runtime.facility_service.get_instance(facility.instance_id)
|
||||
worker = runtime.staff_service.get_staff(&"staff_akane")
|
||||
|
||||
Reference in New Issue
Block a user