Files
TheTree_mock/tests/s1/test_path_service.gd
Takeshi b84bade045 Save The Tree implementation and acceptance work in progress
Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
2026-10-11 15:58:00 +09:00

213 lines
8.4 KiB
GDScript

extends SceneTree
const PathServiceScript = preload("res://scripts/world/path_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S1: deterministic PathService")
_test_deterministic_tie_break()
_test_layer_separation_and_connector()
_test_connector_access_and_direction()
_test_cache_invalidation_and_revision()
_test_connector_only_snapshot()
_test_cross_layer_reason_and_live_changes()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_deterministic_tie_break() -> void:
var service = PathServiceScript.new()
var passable := [
Vector3i(0, 0, 0), Vector3i(1, 0, 0), Vector3i(2, 0, 0),
Vector3i(0, 1, 0), Vector3i(2, 1, 0),
Vector3i(0, 2, 0), Vector3i(1, 2, 0), Vector3i(2, 2, 0),
]
_expect(service.rebuild_graph(passable), "array of Vector3i cells rebuilds the path graph")
var expected: Array[Vector3i] = [
Vector3i(0, 1, 0),
Vector3i(0, 0, 0),
Vector3i(1, 0, 0),
Vector3i(2, 0, 0),
Vector3i(2, 1, 0),
]
var first: Array[Vector3i] = service.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0), service.FRONT)
var second: Array[Vector3i] = service.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0), service.FRONT)
_expect(first == expected, "equal-cost routes use coordinate dictionary order as the tie break")
_expect(second == first, "the cached route exactly reproduces the calculated route")
_expect(service.cache_hits == 1 and service.cache_misses == 1, "path cache records one miss followed by one hit")
func _test_layer_separation_and_connector() -> void:
var service = PathServiceScript.new()
var passable_set := {}
for layer in [service.FRONT, service.BACK]:
for x in range(3):
passable_set[Vector3i(x, 0, layer)] = true
_expect(service.rebuild_graph(passable_set), "Dictionary set can supply passable cells without a WorldService dependency")
_expect(
service.find_path(Vector3i(0, 0, 0), Vector3i(2, 0, 1)).is_empty(),
"FRONT and BACK never connect through ordinary cardinal edges"
)
_expect(
service.configure_connector(
&"service_door",
Vector3i(2, 0, 0),
Vector3i(0, 0, 1),
120,
true
),
"an explicit layer connector is accepted"
)
var connected_path: Array[Vector3i] = service.find_path(Vector3i(0, 0, 0), Vector3i(2, 0, 1))
_expect(
connected_path.has(Vector3i(2, 0, 0)) and connected_path.has(Vector3i(0, 0, 1)),
"an explicit connector joins the two isolated layers"
)
_expect(
service.find_path(Vector3i(0, 0, 0), Vector3i(2, 0, 1), service.FRONT).is_empty(),
"an access-layer restriction rejects a route that enters BACK"
)
func _test_connector_access_and_direction() -> void:
var service = PathServiceScript.new()
var cells := [Vector3i(0, 0, 0), Vector3i(0, 2, 0)]
service.rebuild_graph(cells)
service.configure_connector(
&"staff_stairs",
Vector3i(0, 0, 0),
Vector3i(0, 2, 0),
150,
false,
&"staff"
)
_expect(
service.find_path(Vector3i(0, 0, 0), Vector3i(0, 2, 0)).is_empty(),
"a restricted connector requires its access id"
)
var staff_profile := {
&"allowed_layers": [service.FRONT],
&"connector_access_ids": [&"staff"],
}
_expect(
service.find_path(Vector3i(0, 0, 0), Vector3i(0, 2, 0), staff_profile).size() == 2,
"an access profile enables its matching connector"
)
_expect(
service.find_path(Vector3i(0, 2, 0), Vector3i(0, 0, 0), staff_profile).is_empty(),
"a one-way connector cannot be traversed in reverse"
)
func _test_cache_invalidation_and_revision() -> void:
var service = PathServiceScript.new()
var passable := [
Vector3i(0, 0, 0), Vector3i(1, 0, 0), Vector3i(2, 0, 0),
Vector3i(0, 1, 0), Vector3i(2, 1, 0),
Vector3i(0, 2, 0), Vector3i(1, 2, 0), Vector3i(2, 2, 0),
]
service.rebuild_graph(passable)
var initial_revision: int = service.graph_revision
service.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0))
_expect(service.cached_path_count() == 1, "a calculated path is cached")
_expect(
service.invalidate_cache_for_cells([Vector3i(1, 0, 0)]) == 1,
"local invalidation removes a cached path that crosses an affected cell"
)
_expect(service.cached_path_count() == 0, "local invalidation leaves no matching stale cache entry")
service.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0))
_expect(service.invalidate_all_cache() == 1, "full invalidation reports and removes every cache entry")
passable.erase(Vector3i(1, 0, 0))
service.rebuild_graph(passable)
_expect(service.graph_revision == initial_revision + 1, "a changed topology advances graph_revision exactly once")
var rebuilt_path: Array[Vector3i] = service.find_path(Vector3i(0, 1, 0), Vector3i(2, 1, 0))
_expect(rebuilt_path.has(Vector3i(1, 2, 0)), "rebuild discards the old route and uses the remaining corridor")
func _test_connector_only_snapshot() -> void:
var cells := [Vector3i(0, 0, 0), Vector3i(0, 0, 1)]
var original = PathServiceScript.new()
original.rebuild_graph(cells)
original.configure_connector(
&"lift_a",
Vector3i(0, 0, 0),
Vector3i(0, 0, 1),
250,
true,
&"lift_pass"
)
original.find_path(
Vector3i(0, 0, 0),
Vector3i(0, 0, 1),
null,
[&"lift_pass"]
)
var snapshot: Dictionary = original.create_snapshot()
_expect(
snapshot.has(&"connectors") and not snapshot.has(&"graph") and not snapshot.has(&"cache") and not snapshot.has(&"passable_cells"),
"snapshot persists connector configuration but no derived graph or cache"
)
var restored = PathServiceScript.new()
_expect(restored.restore_snapshot(snapshot), "connector configuration restores independently of world cells")
_expect(restored.rebuild_graph(cells), "restored service rebuilds its graph from authoritative passable cells")
_expect(
restored.find_path(
Vector3i(0, 0, 0),
Vector3i(0, 0, 1),
null,
[&"lift_pass"]
).size() == 2,
"restored connector produces the same cross-layer route after rebuild"
)
func _test_cross_layer_reason_and_live_changes() -> void:
var service = PathServiceScript.new()
var start := Vector3i(0, 0, 0)
var bridge_front := Vector3i(1, 0, 0)
var bridge_back := Vector3i(1, 0, 1)
var goal := Vector3i(2, 0, 1)
service.rebuild_graph([start,bridge_front,bridge_back,goal])
service.configure_connector(&"staff_door",bridge_front,bridge_back,120,false,&"staff")
_expect(service.find_path_report(start,goal)[&"reason"] == service.ROUTE_GOAL_UNREACHABLE,"guest cannot cross a staff-only layer door")
var forward: Array[Vector3i] = service.find_path(start,goal,null,[&"staff"])
_expect(forward == [start,bridge_front,bridge_back,goal],"staff retains the exact forward cross-layer path")
_expect(service.find_path_report(goal,start,null,[&"staff"])[&"reason"] == service.ROUTE_GOAL_UNREACHABLE,"one-way layer door remains unreachable in reverse")
service.set_connector_enabled(&"staff_door",false)
_expect(service.find_path_report(start,goal,null,[&"staff"])[&"reason"] == service.ROUTE_CONNECTOR_UNAVAILABLE,"disabled permitted crossing retains connector-unavailable diagnosis")
service.set_connector_enabled(&"staff_door",true)
service.set_cell_temporary_block(bridge_front,true)
_expect(service.find_path_report(start,goal,null,[&"staff"])[&"reason"] == service.ROUTE_TEMPORARILY_BLOCKED,"blocked permitted crossing retains temporary-block diagnosis")
service.set_cell_temporary_block(start,true)
_expect(service.find_path_report(start,goal,null,[&"staff"])[&"reason"] == service.ROUTE_START_BLOCKED,"blocked endpoint precedence is preserved")
_expect(service.find_path_report(start,goal,service.FRONT,[&"staff"])[&"reason"] == service.ROUTE_ACCESS_LAYER_DENIED,"layer restriction still precedes blocked endpoint")
service.set_cell_temporary_block(start,false)
service.set_cell_temporary_block(bridge_front,false)
_expect(service.find_path(start,goal,null,[&"staff"]) == forward,"reenabled and unblocked crossing restores exact path")
service.configure_connector(&"staff_door",bridge_front,bridge_back,120,true,&"staff")
_expect(service.find_path(goal,start,null,[&"staff"]) == [goal,bridge_back,bridge_front,start],"current bidirectional configuration restores the reverse path")
service.remove_connector(&"staff_door")
_expect(service.find_path_report(start,goal,null,[&"staff"])[&"reason"] == service.ROUTE_GOAL_UNREACHABLE,"connector removal immediately restores unreachable result")
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)