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()