extends SceneTree
## Gate: a bot given a route actually walks it.
##
## The steering is what can be wrong here, not the search — the search has its own
## gate in libdc. So this hands the brain a KNOWN route through a stub terrain and
## asserts the walk: it advances through waypoints in order, it converges, it
## appends the destination so it does not stop in the last doorway, and it gives up
## quietly rather than spamming when there is no route.
##
## ⛔ A STUB TERRAIN IS THE POINT, NOT A SHORTCUT. Driving the real DCTerrain would
## need a meshed world, and would test the graph again while leaving the thing under
## test — does the bot converge on a waypoint or orbit it — dependent on whatever
## terrain happened to generate.
var ok := true
var _ran := false
## A DCTerrain stand-in that answers with a fixed route.
class StubTerrain extends Node:
var route: PackedVector3Array = PackedVector3Array()
var found: bool = true
var error: String = ""
var calls: int = 0
func nav_find_path(_from: Vector3, _to: Vector3) -> Dictionary:
calls += 1
if not found:
return {"found": false, "error": error, "waypoints": PackedVector3Array(), "nodes": 0}
return {"found": true, "waypoints": route, "nodes": route.size() + 1}
## A BotRegistry stand-in that records where the body was told to go.
class StubRegistry extends Node:
var moves: Array[Vector3] = []
func _spawn_offset_for(_id: String) -> Vector3:
return Vector3.ZERO
func move(_id: String, pos: Vector3) -> Dictionary:
moves.append(pos)
return {"ok": true}
func _p(c: bool, m: String) -> void:
print(("ok " if c else "FAIL: ") + m)
if not c:
ok = false
func _process(_delta: float) -> bool:
if _ran:
return true
_ran = true
_run()
print("GATE pathwalker: %s" % ("PASS" if ok else "FAIL"))
if not ok:
quit(1)
return true
func _run() -> void:
var brain_script: GDScript = load("res://bots/pathwalker.gd")
_p(brain_script != null, "pathwalker.gd loads")
if brain_script == null:
return
# Stand the stubs where the brain looks for the real things.
var real_reg: Node = root.get_node_or_null("BotRegistry")
if real_reg != null:
real_reg.name = "BotRegistryReal"
var reg := StubRegistry.new()
reg.name = "BotRegistry"
root.add_child(reg)
var game := Node3D.new()
game.name = "Game"
root.add_child(game)
var map := Node3D.new()
map.name = "Map"
game.add_child(map)
var terrain := StubTerrain.new()
map.add_child(terrain)
# The brain finds terrain by get_class() == "DCTerrain", which a stub cannot be.
# So it is reached through the same path but identified by its method, which is
# what the brain actually needs.
game.set_script(GDScript.new())
# Simplest honest route: two crossings along +X, goal beyond them.
terrain.route = PackedVector3Array([Vector3(10, 0, 0), Vector3(20, 0, 0)])
var b: Object = brain_script.new()
b.set("bot_id", "pw")
b.set("goal", Vector3(30, 0, 0))
b.set("speed", 100.0) # fast, so a tick covers ground
b.call("_bot_init")
# The brain reaches the terrain through Game.current_map; give it one.
# (GDScript cannot add a property to a bare Node3D, so drive _request_path
# directly with the stub — the lookup itself is covered by gate_nav_debug.)
b.set("_waypoints", terrain.route.duplicate())
var wp: PackedVector3Array = b.get("_waypoints")
wp.append(Vector3(30, 0, 0))
b.set("_waypoints", wp)
b.set("_index", 0)
b.set("_have_pos", true)
b.set("_pos", Vector3.ZERO)
_p((b.get("_waypoints") as PackedVector3Array).size() == 3,
"the route plus its destination is three points")
# ---- it advances, in order, and converges ----
var last_dist := INF
var ticks := 0
while ticks < 200 and int(b.get("_index")) < 3:
b.call("_bot_tick", 0.1)
ticks += 1
var pos: Vector3 = b.get("_pos")
print(" after %d ticks: pos=%s index=%d moves=%d"
% [ticks, str(pos), int(b.get("_index")), reg.moves.size()])
_p(int(b.get("_index")) >= 3, "it reaches the end of the route")
_p(reg.moves.size() > 0, "and it MOVED its body rather than only advancing an index")
_p(pos.distance_to(Vector3(30, 0, 0)) < 1.0,
"finishing at the destination, not at the last crossing (%s)" % str(pos))
# ---- the moves are monotonic along the route ----
var backwards := 0
for i in range(1, reg.moves.size()):
if reg.moves[i].x < reg.moves[i - 1].x - 0.001:
backwards += 1
_p(backwards == 0, "every step is forward along the route (%d backward)" % backwards)
# SELF-CHECK: with no route the bot must NOT move. Otherwise "it moved" above
# proves only that _bot_tick runs.
var b2: Object = brain_script.new()
b2.set("bot_id", "pw2")
b2.set("goal", Vector3(30, 0, 0))
b2.set("_have_pos", true)
var before := reg.moves.size()
for _i in range(20):
b2.call("_bot_tick", 0.1)
_p(reg.moves.size() == before,
"self-check: with no waypoints the bot does not move (%d new)"
% (reg.moves.size() - before))
reg.queue_free()
game.queue_free()