OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / bots / beacon.gd

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