extends RefCounted
## An example bot brain — the smallest thing that demonstrates the whole contract.
##
## Run it as a bot with body = none:
##
## BotRegistry.register("beacon", ["map"], "none",
## {"kind": "script_file", "path": "res://bots/beacon.gd"})
##
## WHAT IT DEMONSTRATES, and why each part is here:
##
## _bot_init — claim something in shared state, so the bot is VISIBLE to every
## peer rather than only to the process running it.
## _bot_tick — act on CHANGE, never on a schedule. This is the standing rule for
## the project: event-driven plus reconciliation, no timers. A tick
## that wrote every frame would put 60 Hz of durable traffic on the
## mesh and the log would grow without bound.
## _bot_exit — release the claim. Called by ScriptHost on unload, which the
## registry triggers when any of the bot's bindings ends. This is
## what makes containment OBSERVABLE: change the map and the key is
## gone, because the bot was bound to it.
##
## `state:` is deliberate. From STATE_ROUTES it is DURABLE, role peer, with no map
## scope and no author scope — the one generic key space a bot can write without
## reasoning about authorship. `world:terrain:edit:` and `world:level:` are
## author-scoped and belong to the edit journal (LevelPersistence), which is the
## builder path rather than the hello-world path.
const KEY_PREFIX := "state:bot:beacon:"
var _ticks: int = 0
var _last_published: int = -1
## Publish only when the count crosses this, so writes are sparse and bounded.
const PUBLISH_EVERY := 600
func _bot_init() -> void:
if GameState == null:
return
GameState.set_state(KEY_PREFIX + "alive", true)
GameState.set_state(KEY_PREFIX + "started_at", Time.get_unix_time_from_system())
print("[beacon] up — %salive is true" % KEY_PREFIX)
func _bot_tick(_delta: float) -> void:
_ticks += 1
# Reconciliation, not a timer: compare against what was last published and
# write only when it actually differs. Nothing here measures elapsed time.
var milestone: int = _ticks / PUBLISH_EVERY
if milestone != _last_published:
_last_published = milestone
if GameState != null:
GameState.set_state(KEY_PREFIX + "ticks", _ticks)
func _bot_exit() -> void:
if GameState == null:
return
# Release the claim. If this is skipped the key outlives the bot, which is the
# data half of exactly the leak the containment work removed.
GameState.erase_state(KEY_PREFIX + "alive")
GameState.erase_state(KEY_PREFIX + "ticks")
GameState.erase_state(KEY_PREFIX + "started_at")
print("[beacon] down — keys released after %d ticks" % _ticks)