extends SceneTree
## GATE — the ROAM brain (creatures → bots, #114 phase 2): the creatures' old wander as a
## brain, and the default for a placed bot (user 2026-10-13).
## R1 a bot placed with no brain chosen runs roam
## R2 the authority publishes its targets (world:bots:pos:), each within the radius
## of home — the ONE path every peer's body walks
## R3 it moves on: several different targets over a few pauses
## R4 the radius and pause settings reach the brain
## R5 every step is one a body walks: <= GroundCrawler.SNAP_DISTANCE (8 m), beyond
## which set_target TELEPORTS the body (a 30 m radius once jumped 47 m)
##
## Run: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_roam_brain.gd
const HOME := Vector3(6, 0.5, -3)
const RADIUS := 30.0
var ok := true
var checks := 0
var _frame := 0
var _map: Node
var _reg: Node
var _gs: Node
var _sh: Node
var _dot := ""
var _id := ""
var _seen: Array[Vector3] = []
func _p(c: bool, m: String, got: Variant = "") -> void:
checks += 1
print(("ok " if c else "FAIL: ") + m + ("" if str(got) == "" else " (" + str(got) + ")"))
if not c:
ok = false
func _process(_d: float) -> bool:
_frame += 1
if _frame == 1:
_map = (load("res://scenes/maps/map_flat.tscn") as PackedScene).instantiate()
root.add_child(_map)
return false
if _frame == 30:
_reg = root.get_node("/root/BotRegistry")
_gs = root.get_node("/root/GameState")
_sh = root.get_node("/root/ScriptHost")
_dot = _map.create_object("bot", Transform3D(Basis(), HOME))
var n: Node = _map.object_node(_dot)
_id = n.bot_id() if n != null else ""
_p(n != null and String(n.brain_id()) == "roam", "R1 a bot placed with no brain chosen runs roam",
n.brain_id() if n != null else "none")
_p(_sh.is_loaded(_id), "R1 its brain is running")
_map.set_object_props(_dot, {"p.radius": RADIUS, "p.pause": 0.5})
var b: Object = (_sh.get("_loaded") as Dictionary).get(_id, {}).get("instance")
_p(b != null and is_equal_approx(float(b.get("radius")), RADIUS) and is_equal_approx(float(b.get("pause")), 0.5),
"R4 its radius and pause settings reach the brain", [b.get("radius"), b.get("pause")] if b != null else "none")
return false
if _frame > 30 and _frame < 330:
var v: Variant = _gs.get_state("world:bots:pos:" + _id)
if v is Array and (v as Array).size() >= 3:
var p := Vector3(v[0], v[1], v[2])
if _seen.is_empty() or not _seen[-1].is_equal_approx(p):
_seen.append(p)
return false
if _frame == 330:
_p(not _seen.is_empty(), "R2 the authority publishes its targets (world:bots:pos:)", _seen.size())
var inside := true
for p: Vector3 in _seen:
if Vector2(p.x - HOME.x, p.z - HOME.z).length() > RADIUS + 0.01:
inside = false
_p(inside, "R2 each within %.0f m of home" % RADIUS, _seen)
_p(_seen.size() >= 3, "R3 it moves on: several different targets in 5 s at a 0.5-1 s pause", _seen.size())
# R5: every step is one a body WALKS — GroundCrawler.set_target TELEPORTS to a
# target over SNAP_DISTANCE (8 m) from its head.
var longest := 0.0
for i in range(1, _seen.size()):
longest = maxf(longest, Vector2(_seen[i].x - _seen[i - 1].x, _seen[i].z - _seen[i - 1].z).length())
_p(longest <= GroundCrawler.SNAP_DISTANCE, "R5 no step is long enough to teleport the body (<= 8 m)", longest)
if checks < 7: # a gate that died early must not read as a pass
ok = false
print("FAIL: only %d checks ran -- the gate died before its assertions" % checks)
print("\n%d checks — %s" % [checks, "PASS" if ok else "FAIL"])
quit(0 if ok else 1)
return true
return false