OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_pathwalker.gd

148 lines · gd ref:
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()