338 lines
20 KiB
GDScript
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)
|