OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_roam_brain.gd

85 lines · gd ref:
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