OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_builder_bot.gd

123 lines · gd ref:
extends SceneTree
## Gate: the builder bot actually stacks, and stops.
##
## The point of this bot is that you can CONFIRM it by looking, so the gate
## checks the two things looking would tell you: blocks appear one at a time, and
## the bot's own body follows the stack up. It also checks the bound, because an
## unbounded builder in a durable journal is not a testbed, it is a leak that
## replays on every join.
##
## It drives the REAL map_flat and the REAL brain. A stub map would prove the
## brain calls a method; only the real one proves a block exists afterwards.

var ok := true
var _ran := false


## ⛔ current_map MUST BE A DECLARED PROPERTY. The first version of this gate used
## a bare Node3D and game.set("current_map", map); Object.set() on an undeclared
## property silently does nothing, so the brain's game.get("current_map") read
## null and placed nothing — and the gate blamed the brain. hyper-games #77
## records the same trap with players_node.
class StubGame extends Node3D:
	var current_map: Node = null


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 builder_bot: %s" % ("PASS" if ok else "FAIL"))
	if not ok:
		quit(1)
	return true


func _run() -> void:
	var brain_script: GDScript = load("res://bots/builder.gd")
	_p(brain_script != null, "builder.gd loads")
	if brain_script == null:
		return

	# A Game/current_map the brain can find, using the real map scene.
	var game := StubGame.new()
	game.name = "Game"
	root.add_child(game)
	var ps: PackedScene = load("res://scenes/maps/map_flat.tscn")
	var map: Node = ps.instantiate()
	game.add_child(map)
	game.current_map = map
	var objs: Node = map.get_node_or_null(^"SceneObjects")
	_p(objs != null, "the map brought its objects manager")
	if objs == null:
		return

	var brain: Object = brain_script.new()
	brain.set("bot_id", "gate_builder")
	brain.call("_bot_init")

	var interval: float = float(brain_script.get("BUILD_INTERVAL"))
	# The bound is the `count` setting since b762a15 (it was the const MAX_BLOCKS). This
	# line read the const for a month after it was gone: a script error ended _run
	# here and the gate still printed PASS on 2 checks.
	var maxb: int = int(brain.get("count"))
	_p(interval > 0.0 and maxb > 0, "the brain declares an interval (%.1fs) and a bound (%d)" % [interval, maxb])

	# ---- one block at a time, not all at once ----
	_p(objs.count() == 0, "nothing is built before the first tick")
	brain.call("_bot_tick", interval + 0.01)
	_p(objs.count() == 1, "one tick past the interval places ONE block (%d)" % objs.count())

	# A tick that does not cross the interval must place nothing.
	brain.call("_bot_tick", interval * 0.25)
	_p(objs.count() == 1, "a tick inside the interval places nothing (%d)" % objs.count())

	# ---- it stacks: each block above the last ----
	brain.call("_bot_tick", interval + 0.01)
	_p(objs.count() == 2, "the second tick places a second block (%d)" % objs.count())
	var ys: Array[float] = []
	for e: Dictionary in objs.entries():
		var n: Node = e.get("node")
		if n is Node3D:
			ys.append((n as Node3D).global_position.y)
	ys.sort()
	_p(ys.size() == 2, "two placed blocks found")
	if ys.size() == 2:
		var layer: float = float(brain_script.get("LAYER_HEIGHT"))
		_p(abs((ys[1] - ys[0]) - layer) < 0.01,
			"and the second sits exactly one course above the first (%.2f, want %.2f)" % [ys[1] - ys[0], layer])

	# ---- it is BOUNDED ----
	for _i in range(maxb + 4):
		brain.call("_bot_tick", interval + 0.01)
	_p(objs.count() == maxb,
		"it stops at MAX_BLOCKS and keeps stopping (%d, want %d)" % [objs.count(), maxb])

	# ---- and it reports, so it is answerable without looking ----
	var gs: Node = root.get_node_or_null("GameState")
	if gs != null and gs.has_method("get_state"):
		var placed: Variant = gs.get_state("state:bot:gate_builder:placed")
		_p(int(placed) == maxb, "it published its progress (placed=%s)" % str(placed))

	# SELF-CHECK: with no map that can hold objects, it must place nothing rather
	# than error — otherwise the assertions above could be passing because
	# _bot_tick does something unconditional.
	game.current_map = null
	var brain2: Object = brain_script.new()
	brain2.set("bot_id", "gate_builder_nomap")
	brain2.call("_bot_init")
	var before: int = objs.count()
	brain2.call("_bot_tick", interval + 0.01)
	_p(objs.count() == before,
		"self-check: with no buildable map it places nothing (%d)" % objs.count())

	map.free()
	game.free()