259 lines
15 KiB
GDScript
259 lines
15 KiB
GDScript
extends SceneTree
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
var _path: PathService
|
|
var _elevators: ElevatorService
|
|
var _config: Dictionary
|
|
var _minute := 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
var clock := root.get_node_or_null("GameClock")
|
|
if clock != null: clock.set_paused(true)
|
|
_test_grouping()
|
|
_test_capacity_and_time()
|
|
_test_access_and_shared_cabin()
|
|
_test_restore_and_emergency()
|
|
_test_mutations_and_cancellation()
|
|
_test_snapshot_rejections()
|
|
print("Elevator service: %d checks, %d failures" % [_checks, _failures])
|
|
quit(0 if _failures == 0 else 1)
|
|
|
|
|
|
func _setup(layer := 0, floors := 4) -> void:
|
|
_path = PathService.new()
|
|
var cells: Array[Vector3i] = []
|
|
for y: int in range(floors + 2):
|
|
cells.append(Vector3i(0, y, layer))
|
|
_path.rebuild_graph(cells)
|
|
_elevators = ElevatorService.new()
|
|
_elevators.configure(_path)
|
|
_config = {&"shaft_id": StringName("transport.shaft.%d.0.0.%d" % [layer, 2 if layer == 0 else 3]),
|
|
&"module_ids": [&"module.0", &"module.1"], &"stops": [], &"width": 2 if layer == 0 else 3,
|
|
&"capacity": 4 if layer == 0 else 8, &"travel_minutes_per_floor": 10 if layer == 0 else 5,
|
|
&"access_id": &"" if layer == 0 else &"staff"}
|
|
for y: int in range(floors): (_config[&"stops"] as Array).append(Vector3i(0, y, layer))
|
|
_expect(_elevators.synchronize([_config], 0)[&"ok"], "shaft initializes")
|
|
_minute = 0
|
|
|
|
|
|
func _request(actor: StringName, from: int, to: int, layer := 0) -> Dictionary:
|
|
var access: Array[StringName] = []
|
|
if layer == 1: access.append(&"staff")
|
|
return _elevators.request_trip(actor, Vector3i(0, from, layer), Vector3i(0, to, layer), _minute, access)
|
|
|
|
|
|
func _tick(count := 1) -> Array[Dictionary]:
|
|
var arrivals: Array[Dictionary] = []
|
|
for i: int in range(count):
|
|
_minute += 1
|
|
arrivals.append_array(_elevators.advance_one_minute(_minute))
|
|
return arrivals
|
|
|
|
|
|
func _car() -> Dictionary:
|
|
return _elevators.get_report()[&"shafts"][0]
|
|
|
|
|
|
func _test_grouping() -> void:
|
|
var registry := DefinitionRegistry.new()
|
|
registry.rebuild()
|
|
var records: Array = []
|
|
for row: Array in [[&"g2", &"guest_elevator", Vector3i(0, 2, 0)], [&"g0", &"guest_elevator", Vector3i(0, 0, 0)], [&"g6", &"guest_elevator", Vector3i(0, 6, 0)], [&"b0", &"service_elevator", Vector3i(0, 0, 1)], [&"other", &"reception", Vector3i(6, 0, 0)]]:
|
|
records.append({&"instance_id": row[0], &"definition_id": row[1], &"origin": row[2]})
|
|
var configs := ElevatorService.configurations_from_facilities(records, registry)
|
|
_expect(configs.size() == 3, "adjacent modules form one shaft, gaps and layers form separate shafts")
|
|
_expect(configs[0][&"stops"] == [Vector3i(0, 0, 0), Vector3i(0, 1, 0), Vector3i(0, 2, 0), Vector3i(0, 3, 0)], "each occupied integer floor gets a landing")
|
|
_expect(configs[0][&"module_ids"] == [&"g0", &"g2"], "construction ordering does not change module/stop ordering")
|
|
_expect(configs[2][&"capacity"] == 8 and configs[2][&"travel_minutes_per_floor"] == 5 and configs[2][&"access_id"] == &"staff", "BACK shaft has eight seats, five minutes per floor and staff access")
|
|
records.reverse()
|
|
_expect(ElevatorService.configurations_from_facilities(records, registry) == configs, "configuration generation is deterministic")
|
|
|
|
|
|
func _test_capacity_and_time() -> void:
|
|
_setup()
|
|
_expect(_path.create_snapshot()[&"connectors"].size() == 6, "four stops expose six bidirectional direct links")
|
|
for i: int in range(5): _expect(_request(StringName("guest/%d" % i), 0, 3)[&"ok"], "guest queues without moving")
|
|
_expect(_car()[&"passenger_count"] == 0 and _car()[&"waiting_count"] == 5, "requests do not board before simulation advances")
|
|
_expect(_tick().is_empty() and _car()[&"passenger_count"] == 4 and _car()[&"waiting_count"] == 1, "shared cabin boards at most four FRONT actors")
|
|
_expect(_car()[&"phase"] == &"BOARDING" and _car()[&"remaining_minutes"] == 6, "doors take two minutes plus one per boarding actor")
|
|
_expect(_tick(6).is_empty() and _car()[&"phase"] == &"TRAVEL", "boarding duration elapses before cabin travels")
|
|
_expect(_tick(15).is_empty() and is_equal_approx(_car()[&"visual_floor"], 1.5), "cabin interpolates actual half journey without arriving")
|
|
_expect(_tick(15).is_empty() and _car()[&"phase"] == &"ALIGHTING", "three FRONT floors take thirty minutes before alighting")
|
|
_expect(_tick(5).is_empty(), "passengers remain owned while doors are unloading")
|
|
var arrivals := _tick()
|
|
_expect(arrivals.size() == 4 and _minute == 43, "first four arrivals happen only after both door intervals and travel")
|
|
for arrival: Dictionary in arrivals:
|
|
_expect(arrival[&"cell"] == Vector3i(0, 3, 0) and not arrival[&"emergency"] and _path.get_active_elevator_request(arrival[&"actor_id"]).is_empty(), "arrival transfers actor once and releases Path ownership")
|
|
_expect(_car()[&"phase"] == &"PICKUP" and _car()[&"waiting_count"] == 1, "remaining guest waits for same cabin to return")
|
|
arrivals = _tick(66)
|
|
_expect(arrivals.size() == 1 and arrivals[0][&"actor_id"] == &"guest/4" and _car()[&"phase"] == &"IDLE", "fifth guest waits for real return and completes without starvation")
|
|
_expect(_tick(10).is_empty(), "completed request never arrives twice")
|
|
var read := _elevators.get_report()
|
|
read[&"shafts"][0][&"boarded"].append(&"tamper")
|
|
_expect(_car()[&"boarded"].is_empty(), "report returns defensive copies")
|
|
|
|
|
|
func _test_access_and_shared_cabin() -> void:
|
|
_setup(1)
|
|
_expect(not _elevators.request_trip(&"guest/no_access", Vector3i(0, 0, 1), Vector3i(0, 3, 1), 0)[&"ok"], "BACK elevator rejects actor without staff access")
|
|
for i: int in range(9): _expect(_request(StringName("staff/%d" % i), 0, 3, 1)[&"ok"], "authorized staff queues")
|
|
_tick()
|
|
_expect(_car()[&"passenger_count"] == 8 and _car()[&"waiting_count"] == 1, "BACK cabin shares eight seats across requests")
|
|
_tick(10)
|
|
_expect(_car()[&"phase"] == &"TRAVEL" and _car()[&"remaining_minutes"] == 15, "BACK shaft uses five minutes per floor")
|
|
_tick(15)
|
|
_expect(_car()[&"phase"] == &"ALIGHTING", "BACK travel duration is physical")
|
|
_expect(_tick(10).size() == 8, "BACK eight passengers alight together after paid time")
|
|
_setup()
|
|
_request(&"a", 0, 3)
|
|
_request(&"b", 1, 2)
|
|
_tick()
|
|
_expect(_car()[&"passenger_count"] == 1 and _car()[&"waiting_count"] == 1, "different floor pair cannot create a second cabin")
|
|
_expect(_elevators.get_actor_trip(&"b")[&"state"] == PathService.ELEVATOR_REQUEST_QUEUED, "other connector actor remains waiting while shared car moves")
|
|
_expect(_elevators.owns_connector(_elevators.get_actor_trip(&"a")[&"connector_id"]), "transport identifies its owned path step")
|
|
_expect(not _elevators.owns_connector(&"door.other"), "unrelated door connector is not transport-owned")
|
|
var all := _tick(100)
|
|
_expect(all.size() == 2 and all[0][&"actor_id"] == &"a" and all[1][&"actor_id"] == &"b", "FIFO serves different floor pair after first journey")
|
|
|
|
|
|
func _test_restore_and_emergency() -> void:
|
|
_setup()
|
|
_request(&"rider", 0, 3)
|
|
_tick(20)
|
|
_expect(_car()[&"phase"] == &"TRAVEL" and _car()[&"visual_floor"] > 1.0, "midride fixture is physically between floors")
|
|
var state := _elevators.create_snapshot()
|
|
var path_state := _path.create_snapshot()
|
|
var future := _tick(40)
|
|
var final_state := _elevators.create_snapshot()
|
|
_expect(_path.restore_snapshot(path_state), "Path queue/boarded state restores")
|
|
_expect(_elevators.restore_snapshot(state), "midride save restores against matching Path ownership: %s" % _elevators.last_error)
|
|
_minute = state[&"current_minute"]
|
|
_expect(_tick(40) == future and _elevators.create_snapshot() == final_state, "restored trip produces identical arrivals and final snapshot")
|
|
_setup()
|
|
_request(&"aboard", 0, 3)
|
|
_request(&"waiting", 2, 3)
|
|
_tick(20)
|
|
var released := _elevators.set_powered(_config[&"shaft_id"], false, _minute)
|
|
_expect(released.size() == 2, "power failure releases both waiting and boarded owners")
|
|
_expect(released[0][&"actor_id"] == &"aboard" and released[0][&"cell"] == Vector3i(0, 2, 0) and released[0][&"emergency"], "power failure unloads boarded actor at nearest valid floor")
|
|
_expect(released[1][&"actor_id"] == &"waiting" and released[1][&"cell"] == Vector3i(0, 2, 0) and released[1][&"cancelled"], "waiting cancellation stays on original landing for replan")
|
|
_expect(_car()[&"operator_open"] and not _car()[&"powered"] and not _car()[&"operational"], "power loss does not alter player operation policy")
|
|
_expect(_path.get_active_elevator_request(&"aboard").is_empty() and _elevators.get_actor_trip(&"waiting").is_empty(), "emergency leaves no orphan owner")
|
|
_expect(not _request(&"stopped", 0, 3)[&"ok"], "stopped elevator rejects new boarding")
|
|
_expect(_elevators.restore_snapshot(_elevators.create_snapshot()), "stopped elevator restores with Path outage state")
|
|
_elevators.set_operator_open(_config[&"shaft_id"], false, _minute)
|
|
_elevators.set_powered(_config[&"shaft_id"], true, _minute)
|
|
_expect(not _car()[&"operator_open"] and _car()[&"powered"] and not _car()[&"operational"], "power restoration preserves player-requested stop")
|
|
_expect(_elevators.restore_snapshot(_elevators.create_snapshot()), "player-stopped powered elevator state restores")
|
|
_elevators.set_operator_open(_config[&"shaft_id"], true, _minute)
|
|
_expect(_car()[&"operational"] and _request(&"resumed", 2, 0)[&"ok"], "player reopens restored elevator")
|
|
_expect(_tick(27).size() == 1, "reopened cabin resumes from emergency landing with real travel")
|
|
_setup()
|
|
_request(&"blocked_destination", 0, 3)
|
|
_tick(20)
|
|
_path.configure_cell_traffic(Vector3i(0, 3, 0), 4, 0, true)
|
|
released = _tick()
|
|
_expect(released.size() == 1 and released[0][&"emergency"] and released[0][&"cell"] != Vector3i(0, 3, 0), "blocked destination causes emergency at a valid landing")
|
|
_setup()
|
|
_request(&"held", 0, 3)
|
|
_request(&"waiting_held", 2, 3)
|
|
_tick(15)
|
|
for cell: Vector3i in _config[&"stops"]: _path.configure_cell_traffic(cell, 4, 0, true)
|
|
released = _elevators.set_operator_open(_config[&"shaft_id"], false, _minute)
|
|
_expect(released.size() == 1 and released[0][&"actor_id"] == &"waiting_held" and released[0][&"cancelled"], "all-landings closure still cancels waiting actors at their original position")
|
|
_expect(_car()[&"passenger_count"] == 1 and not _car()[&"operational"] and not _path.get_active_elevator_request(&"held").is_empty(), "no valid landing holds boarded actor safely with ownership")
|
|
var held_remaining: int = _car()[&"remaining_minutes"]
|
|
_tick(20)
|
|
_expect(_car()[&"remaining_minutes"] == held_remaining and _elevators.restore_snapshot(_elevators.create_snapshot()), "held cabin survives elapsed time and save restore without advancing or losing its rider")
|
|
for cell: Vector3i in _config[&"stops"]: _path.configure_cell_traffic(cell)
|
|
_elevators.set_operator_open(_config[&"shaft_id"], true, _minute)
|
|
_expect(_elevators.restore_snapshot(_elevators.create_snapshot()), "repaired cabin resumes with an adjusted physical phase clock")
|
|
_expect(_tick(40).size() == 1, "held actor arrives exactly once after landing repair")
|
|
|
|
|
|
func _test_mutations_and_cancellation() -> void:
|
|
_setup()
|
|
_request(&"waiter", 0, 3)
|
|
var before := _elevators.create_snapshot()
|
|
_expect(not _elevators.synchronize([], _minute)[&"ok"] and _elevators.create_snapshot() == before, "queued rider prevents destructive shaft modification atomically")
|
|
var larger := _config.duplicate(true)
|
|
larger[&"module_ids"].append(&"module.2")
|
|
larger[&"stops"].append(Vector3i(0, 4, 0))
|
|
larger[&"stops"].append(Vector3i(0, 5, 0))
|
|
_expect(not _elevators.synchronize([larger], _minute)[&"ok"], "extension waits until cabin and queues are idle")
|
|
_expect(_elevators.cancel_actor(&"waiter") and _path.get_active_elevator_request(&"waiter").is_empty(), "waiting actor removal cancels Path queue ownership")
|
|
_expect(_elevators.synchronize([larger], _minute)[&"ok"] and _car()[&"stops"].size() == 6, "idle upward extension joins existing physical cabin")
|
|
_expect(_request(&"despawn", 0, 3)[&"ok"], "new request uses fresh request ID after cancellation")
|
|
_tick(2)
|
|
_expect(_elevators.cancel_actor(&"despawn") and _path.get_active_elevator_request(&"despawn").is_empty() and _car()[&"passenger_count"] == 0, "boarded actor despawn removes only seat and ownership")
|
|
_expect(_elevators.restore_snapshot(_elevators.create_snapshot()), "saved doors remain valid after only passenger despawns")
|
|
_expect(_tick(50).is_empty() and _car()[&"phase"] == &"IDLE", "empty cabin safely finishes its motion without resurrecting despawned actor")
|
|
_expect(_elevators.synchronize([], _minute)[&"ok"] and _path.create_snapshot()[&"connectors"].is_empty(), "idle demolition removes all owned connectors")
|
|
_setup()
|
|
_request(&"duplicate", 0, 3)
|
|
_expect(_elevators.restore_snapshot(_elevators.create_snapshot()), "queued snapshot restores with matching floor, request, and Path unit")
|
|
var replay := _request(&"duplicate", 0, 3)
|
|
_expect(replay[&"ok"] and replay[&"replay"] and _car()[&"waiting_count"] == 1, "same actor route request replays without duplicate queue")
|
|
_expect(not _request(&"duplicate", 0, 2)[&"ok"], "actor cannot own another simultaneous destination")
|
|
_expect(_elevators.advance_one_minute(8).is_empty() and _elevators.current_minute == 0, "skipped simulation minutes are rejected")
|
|
_expect(_tick().is_empty() and _elevators.advance_one_minute(1).is_empty() and _car()[&"remaining_minutes"] == 3, "duplicate minute cannot advance cabin twice")
|
|
|
|
|
|
func _test_snapshot_rejections() -> void:
|
|
_setup()
|
|
_request(&"saved", 0, 3)
|
|
_tick(10)
|
|
var saved := _elevators.create_snapshot()
|
|
_expect(_elevators.restore_snapshot(saved), "valid active snapshot restores")
|
|
var bad := saved.duplicate(true)
|
|
bad[&"shafts"][0][&"config"][&"capacity"] = 5
|
|
_reject(bad, saved, "tampered FRONT capacity")
|
|
bad = saved.duplicate(true)
|
|
bad[&"shafts"][0][&"config"][&"travel_minutes_per_floor"] = 1
|
|
_reject(bad, saved, "tampered floor speed")
|
|
bad = saved.duplicate(true)
|
|
bad[&"shafts"][0][&"remaining_minutes"] -= 1
|
|
_reject(bad, saved, "tampered cabin countdown")
|
|
bad = saved.duplicate(true)
|
|
bad[&"trips"][0][&"to"] = Vector3i(0, 2, 0)
|
|
_reject(bad, saved, "tampered destination")
|
|
bad = saved.duplicate(true)
|
|
bad[&"shafts"][0][&"boarded"] = []
|
|
_reject(bad, saved, "missing passenger seat")
|
|
bad = saved.duplicate(true)
|
|
bad[&"trips"] = []
|
|
_reject(bad, saved, "missing active trip")
|
|
bad = saved.duplicate(true)
|
|
bad[&"request_sequence"] = 0
|
|
_reject(bad, saved, "active request sequence reuse")
|
|
bad = saved.duplicate(true)
|
|
bad[&"shafts"][0][&"powered"] = false
|
|
_reject(bad, saved, "inconsistent power and operation")
|
|
bad = saved.duplicate(true)
|
|
bad[&"unexpected"] = true
|
|
_reject(bad, saved, "unknown snapshot field")
|
|
_tick(50)
|
|
saved = _elevators.create_snapshot()
|
|
bad = saved.duplicate(true)
|
|
bad[&"request_sequence"] = 0
|
|
_reject(bad, saved, "retired request sequence reuse")
|
|
_expect(_elevators.restore_snapshot(saved), "completed trip history permits valid restore")
|
|
|
|
|
|
func _reject(bad: Dictionary, before: Dictionary, label: String) -> void:
|
|
_expect(not _elevators.restore_snapshot(bad) and _elevators.create_snapshot() == before, "%s is rejected atomically" % label)
|
|
|
|
|
|
func _expect(condition: bool, label: String) -> void:
|
|
_checks += 1
|
|
if condition: print("PASS: %s" % label)
|
|
else:
|
|
_failures += 1
|
|
push_error("FAIL: %s" % label)
|