205 lines
14 KiB
GDScript
205 lines
14 KiB
GDScript
extends SceneTree
|
|
|
|
const PathType = preload("res://scripts/world/path_service.gd")
|
|
const ProjectorType = preload("res://scripts/world/facility_infrastructure_projector.gd")
|
|
const AdapterType = preload("res://scripts/world/route_subslot_path_adapter.gd")
|
|
const SpatialType = preload("res://scripts/world/spatial_subslot_service.gd")
|
|
const GUEST = preload("res://data/facilities/guest_elevator.tres")
|
|
const SERVICE = preload("res://data/facilities/service_elevator.tres")
|
|
|
|
class ControlledPath:
|
|
extends PathService
|
|
var fail_set := false
|
|
var fail_remove := false
|
|
func set_vertical_walk_blockers(owner_id: StringName, cells: Array[Vector3i]) -> bool:
|
|
var result := super.set_vertical_walk_blockers(owner_id, cells)
|
|
if fail_set:
|
|
fail_set = false
|
|
return false
|
|
return result
|
|
func remove_vertical_walk_blockers(owner_id: StringName) -> bool:
|
|
var result := super.remove_vertical_walk_blockers(owner_id)
|
|
if fail_remove:
|
|
fail_remove = false
|
|
return false
|
|
return result
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
func _run() -> void:
|
|
_test_static_shaft_and_cabin_edges()
|
|
_test_owner_boundaries_and_cache()
|
|
_test_snapshot_validation_and_legacy()
|
|
_test_projection_and_rollback()
|
|
print("Elevator topology: %d checks, %d failures" % [_checks, _failures])
|
|
quit(_failures)
|
|
|
|
func _test_static_shaft_and_cabin_edges() -> void:
|
|
var path := PathType.new()
|
|
var cells: Array[Vector3i] = [Vector3i(-1, 0, 0), Vector3i(0, -1, 0), Vector3i(0, 0, 0), Vector3i(0, 1, 0), Vector3i(0, 2, 0), Vector3i(1, 0, 0), Vector3i(1, 1, 0), Vector3i(2, 1, 0)]
|
|
_expect(path.rebuild_graph(cells), "materialize ordinary graph")
|
|
_expect(not path.find_path(Vector3i(-1, 0, 0), Vector3i(2, 1, 0)).is_empty(), "legacy plain corridor graph still walks vertically")
|
|
_expect(path.set_vertical_walk_blockers(&"shaft.a", [Vector3i(0, 0, 0), Vector3i(0, 1, 0), Vector3i(1, 0, 0), Vector3i(1, 1, 0)]), "mark entire two-column shaft")
|
|
_expect(path.find_path(Vector3i(-1, 0, 0), Vector3i(2, 1, 0)).is_empty(), "neither shaft column admits ordinary climbing")
|
|
_expect(path.last_no_route_reason == path.ROUTE_GOAL_UNREACHABLE, "diagnostic graph cannot invent walk bypass")
|
|
_expect(path.find_path(Vector3i(0, -1, 0), Vector3i(0, 0, 0)).is_empty(), "edge touching bottom of shaft is suppressed")
|
|
_expect(path.find_path(Vector3i(0, 1, 0), Vector3i(0, 2, 0)).is_empty(), "edge touching top of shaft is suppressed")
|
|
_expect(path.find_path(Vector3i(-1, 0, 0), Vector3i(1, 0, 0)).size() == 3, "horizontal floor access through shaft remains")
|
|
_expect(path.configure_elevator(&"transport.a", Vector3i(0, 0, 0), Vector3i(0, 1, 0)), "separate cabin connector enables rides")
|
|
_expect(not path.find_path(Vector3i(-1, 0, 0), Vector3i(2, 1, 0)).is_empty(), "cabin restores reachable vertical route")
|
|
var edge := path.get_edge_report(Vector3i(0, 0, 0), Vector3i(0, 1, 0))
|
|
_expect(edge.get(&"movement_type") == path.MOVEMENT_ELEVATOR and edge.get(&"connector_id") == &"transport.a", "movement driver sees elevator edge")
|
|
_expect(path.get_step_connector(Vector3i(0, 1, 0), Vector3i(0, 0, 0)) == &"transport.a", "reverse ride resolves same bidirectional connector")
|
|
_expect(path.get_step_connector(Vector3i(-1, 0, 0), Vector3i(0, 0, 0)).is_empty(), "floor step has no cabin owner")
|
|
edge[&"connector_id"] = &"forged"
|
|
_expect(path.get_step_connector(Vector3i(0, 0, 0), Vector3i(0, 1, 0)) == &"transport.a", "edge report is defensive")
|
|
_expect(path.set_connector_operational_state(&"transport.a", path.CONNECTOR_STOPPED), "stop car without removing shaft")
|
|
_expect(path.find_path(Vector3i(-1, 0, 0), Vector3i(2, 1, 0)).is_empty(), "stopped car cannot be bypassed")
|
|
_expect(path.last_no_route_reason == path.ROUTE_CONNECTOR_UNAVAILABLE, "diagnostic recognizes unavailable car")
|
|
_expect(path.get_edge_report(Vector3i(0, 0, 0), Vector3i(0, 1, 0)).is_empty(), "stopped car has no usable step")
|
|
_expect(path.remove_connector(&"transport.a"), "remove transport connector independently")
|
|
_expect(path.find_path(Vector3i(0, 0, 0), Vector3i(0, 1, 0)).is_empty(), "removing cabin retains shaft walk suppression")
|
|
_expect(path.configure_elevator(&"transport.staff", Vector3i(0, 0, 0), Vector3i(0, 1, 0), {}, &"staff"), "configure service-only cabin")
|
|
_expect(path.find_path(Vector3i(0, 0, 0), Vector3i(0, 1, 0), [0], []).is_empty(), "guest cannot use service cabin")
|
|
_expect(path.get_step_connector(Vector3i(0, 0, 0), Vector3i(0, 1, 0), [&"staff"]) == &"transport.staff", "staff access identifies service cabin")
|
|
|
|
func _test_owner_boundaries_and_cache() -> void:
|
|
var path := PathType.new()
|
|
var bottom := Vector3i(0, 0, 0)
|
|
var top := Vector3i(0, 1, 0)
|
|
path.rebuild_graph([bottom, top])
|
|
path.find_path(bottom, top)
|
|
var revision := path.graph_revision
|
|
_expect(path.cached_path_count() > 0, "seed walk path cache")
|
|
_expect(path.set_vertical_walk_blockers(&"owner.a", [bottom]), "first owner blocks one endpoint")
|
|
_expect(path.graph_revision > revision and path.cached_path_count() == 0, "blocker edit invalidates cached routes")
|
|
var snapshot := path.create_snapshot()
|
|
_expect(path.set_vertical_walk_blockers(&"owner.a", [bottom]) and path.create_snapshot() == snapshot, "same owner configuration is idempotent")
|
|
_expect(path.set_vertical_walk_blockers(&"owner.b", [bottom]), "independent owner can overlap")
|
|
_expect(path.remove_vertical_walk_blockers(&"owner.a") and path.find_path(bottom, top).is_empty(), "removing one owner retains other's restriction")
|
|
var defensive := path.get_vertical_walk_blockers()
|
|
defensive[&"owner.b"].clear()
|
|
_expect(path.find_path(bottom, top).is_empty(), "blocker read model cannot mutate graph")
|
|
_expect(path.remove_vertical_walk_blockers(&"owner.b") and path.find_path(bottom, top) == [bottom, top], "last owner removal restores ordinary topology")
|
|
snapshot = path.create_snapshot()
|
|
_expect(not path.set_vertical_walk_blockers(&"", [bottom]), "reject empty owner")
|
|
_expect(not path.set_vertical_walk_blockers(&" bad", [bottom]), "reject noncanonical owner")
|
|
_expect(not path.set_vertical_walk_blockers(&"owner", []), "empty replacement must use explicit removal")
|
|
_expect(not path.set_vertical_walk_blockers(&"owner", [bottom, bottom]), "reject duplicate owned cell")
|
|
_expect(not path.set_vertical_walk_blockers(&"owner", [Vector3i(4, 4, 0)]), "reject unknown graph cell")
|
|
_expect(not path.remove_vertical_walk_blockers(&"unknown"), "reject removing unknown owner")
|
|
_expect(path.create_snapshot() == snapshot, "invalid blocker mutations leave full state unchanged")
|
|
|
|
func _test_snapshot_validation_and_legacy() -> void:
|
|
var path := PathType.new()
|
|
var cells: Array[Vector3i] = [Vector3i(0, 0, 0), Vector3i(0, 1, 0), Vector3i(1, 0, 0)]
|
|
path.rebuild_graph(cells)
|
|
path.set_vertical_walk_blockers(&"shaft.a", [cells[0], cells[1]])
|
|
path.set_vertical_walk_blockers(&"shaft.b", [cells[2]])
|
|
var snapshot := path.create_snapshot()
|
|
_expect(snapshot[&"schema_version"] == 4, "Path v4 declares shaft ownership")
|
|
var restored := PathType.new()
|
|
_expect(restored.restore_snapshot(snapshot) and restored.rebuild_graph(cells), "restore shaft topology before authoritative graph materialization")
|
|
_expect(restored.create_snapshot() == snapshot and restored.find_path(cells[0], cells[1]).is_empty(), "round trip restores exact restrictions and revision")
|
|
var malformed_cases: Array[Dictionary] = []
|
|
var missing := snapshot.duplicate(true)
|
|
missing.erase(&"vertical_walk_blockers")
|
|
malformed_cases.append(missing)
|
|
var owner_duplicate := snapshot.duplicate(true)
|
|
owner_duplicate[&"vertical_walk_blockers"].append(owner_duplicate[&"vertical_walk_blockers"][0].duplicate(true))
|
|
malformed_cases.append(owner_duplicate)
|
|
var wrong_order := snapshot.duplicate(true)
|
|
wrong_order[&"vertical_walk_blockers"].reverse()
|
|
malformed_cases.append(wrong_order)
|
|
var wrong_cells := snapshot.duplicate(true)
|
|
wrong_cells[&"vertical_walk_blockers"][0][&"cells"].reverse()
|
|
malformed_cases.append(wrong_cells)
|
|
var unknown_cell := snapshot.duplicate(true)
|
|
unknown_cell[&"vertical_walk_blockers"][0][&"cells"] = [Vector3i(999, 999, 0)]
|
|
malformed_cases.append(unknown_cell)
|
|
var duplicate_cell := snapshot.duplicate(true)
|
|
duplicate_cell[&"vertical_walk_blockers"][0][&"cells"] = [cells[0], cells[0]]
|
|
malformed_cases.append(duplicate_cell)
|
|
var unknown_field := snapshot.duplicate(true)
|
|
unknown_field[&"vertical_walk_blockers"][0][&"extra"] = true
|
|
malformed_cases.append(unknown_field)
|
|
var empty_cells := snapshot.duplicate(true)
|
|
empty_cells[&"vertical_walk_blockers"][0][&"cells"] = []
|
|
malformed_cases.append(empty_cells)
|
|
for malformed: Dictionary in malformed_cases:
|
|
_expect(not restored.restore_snapshot(malformed), "reject malformed shaft ownership snapshot")
|
|
_expect(restored.create_snapshot() == snapshot, "snapshot rejection is atomic")
|
|
for version: int in [1, 2, 3]:
|
|
var legacy := snapshot.duplicate(true)
|
|
legacy[&"schema_version"] = version
|
|
_expect(not restored.restore_snapshot(legacy), "old version with retained shaft field cannot disable safety")
|
|
legacy.erase(&"vertical_walk_blockers")
|
|
_expect(restored.restore_snapshot(legacy), "true legacy snapshot lacking shaft field migrates")
|
|
_expect(restored.get_vertical_walk_blockers().is_empty() and not restored.find_path(cells[0], cells[1]).is_empty(), "legacy plain topology remains compatible")
|
|
_expect(restored.restore_snapshot(snapshot), "return to current shaft snapshot")
|
|
|
|
func _test_projection_and_rollback() -> void:
|
|
var context := _context()
|
|
var guest := _instance(GUEST, &"guest", Vector3i(0, 0, 0))
|
|
var service := _instance(SERVICE, &"service", Vector3i(4, 0, 1))
|
|
_expect(context.projector.project_facility(&"project.guest", GUEST, guest).is_success(), "project guest elevator ROUTE footprint")
|
|
_expect(context.projector.project_facility(&"project.service", SERVICE, service).is_success(), "project service elevator ROUTE footprint")
|
|
_expect(context.source.get_route_cells().size() == 10, "all occupied 2x2 and 3x2 cells are floor-accessible")
|
|
_expect(context.projector.get_projection(&"guest")[&"connector_id"] == &"", "projector owns static shaft separately from transport connector")
|
|
_expect(context.projector.validate_invariants()[&"ok"], "shaft owner and record crosslinks match")
|
|
var saved := _snapshots(context)
|
|
_expect(context.projector.restore_snapshot(saved.projector) and context.projector.create_snapshot() == saved.projector, "current elevator projection snapshot roundtrip preserves history and static ownership")
|
|
context.path.remove_vertical_walk_blockers(&"infra.shaft:guest")
|
|
_expect(context.projector.is_projection_stale(), "missing shaft owner makes infrastructure stale")
|
|
_expect(not context.projector.restore_snapshot(saved.projector), "metadata restore cannot accept missing static shaft restriction")
|
|
_expect(context.path.restore_snapshot(saved.path), "repair exact shaft state")
|
|
_expect(context.projector.validate_invariants()[&"ok"], "restored shaft repairs infrastructure crosslink")
|
|
var orphan_cells: Array[Vector3i] = [Vector3i(0, 0, 0)]
|
|
context.path.set_vertical_walk_blockers(&"infra.shaft:orphan", orphan_cells)
|
|
_expect(context.projector.is_projection_stale(), "untracked managed shaft owner rejected")
|
|
context.path.remove_vertical_walk_blockers(&"infra.shaft:orphan")
|
|
_expect(context.projector.unproject_facility(&"remove.guest", &"guest").is_success(), "unproject guest shaft with its route")
|
|
_expect(not context.path.get_vertical_walk_blockers().has(&"infra.shaft:guest") and not context.path.has_cell(Vector3i(0, 0, 0)), "demolition removes static owner and route cells")
|
|
_expect(context.path.get_vertical_walk_blockers().has(&"infra.shaft:service") and context.projector.validate_invariants()[&"ok"], "independent service shaft remains intact")
|
|
var controlled := ControlledPath.new()
|
|
var failure_context := _context(controlled)
|
|
var before := _snapshots(failure_context)
|
|
controlled.fail_set = true
|
|
_expect(not failure_context.projector.project_facility(&"fail.project", GUEST, guest).is_success(), "injected shaft mutation failure rejects project")
|
|
_expect(_snapshots(failure_context) == before, "failed shaft project restores source, route adapter, Path, and metadata exactly")
|
|
_expect(failure_context.projector.project_facility(&"fail.project", GUEST, guest).is_success(), "failed transaction leaves request id available")
|
|
before = _snapshots(failure_context)
|
|
controlled.fail_remove = true
|
|
_expect(not failure_context.projector.unproject_facility(&"fail.remove", &"guest").is_success(), "injected shaft removal failure rejects demolition")
|
|
_expect(_snapshots(failure_context) == before, "failed shaft removal restores every dependency exactly")
|
|
_expect(failure_context.projector.unproject_facility(&"fail.remove", &"guest").is_success(), "failed demolition remains retryable")
|
|
|
|
func _context(path_value: PathService = null) -> Dictionary:
|
|
var source := SpatialType.new()
|
|
var path := path_value if path_value != null else PathType.new()
|
|
var adapter := AdapterType.new()
|
|
_expect(adapter.configure(source, path, []), "configure route adapter")
|
|
var projector := ProjectorType.new()
|
|
_expect(projector.configure(adapter, source, path), "configure infrastructure projector")
|
|
return {&"source": source, &"path": path, &"adapter": adapter, &"projector": projector}
|
|
|
|
func _instance(definition: FacilityDefinition, instance_id: StringName, origin: Vector3i) -> Dictionary:
|
|
var cells: Array[Vector3i] = []
|
|
for y: int in range(definition.footprint.y):
|
|
for x: int in range(definition.footprint.x):
|
|
cells.append(origin + Vector3i(x, y, 0))
|
|
return {&"instance_id": instance_id, &"definition_id": definition.id, &"origin": origin, &"rotation": 0, &"layer": definition.layer, &"occupied_cells": cells, &"construction_cost": definition.construction_cost.duplicate(true), &"source_job_id": StringName("build." + String(instance_id)), &"state": &"ACTIVE", &"completed_at": 300}
|
|
|
|
func _snapshots(context: Dictionary) -> Dictionary:
|
|
return {&"source": context.source.create_snapshot(), &"path": context.path.create_snapshot(), &"adapter": context.adapter.create_snapshot(), &"projector": context.projector.create_snapshot()}
|
|
|
|
func _expect(ok: bool, label: String) -> void:
|
|
_checks += 1
|
|
if not ok:
|
|
_failures += 1
|
|
push_error(label)
|