Files
TheTree_mock/tests/transport/test_transport_comparison.gd
T

272 lines
14 KiB
GDScript

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)