Files
TheTree_mock/tests/transport/test_runtime_transport_comparison.gd
T

338 lines
20 KiB
GDScript

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)