OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_ghost_creatures.gd

162 lines · gd ref:
extends SceneTree
## GATE — ghost creatures (demo sprint §5), on the REAL wiring: game.tscn as /root/Game,
## its own change_map, and creatures as what they are now — BOTS with a creature body
## (#114 phase 5: the creature spawner and its listener are retired). The ghosts were
## creatures drawn frozen in a map they no longer belonged to, or drawn twice.
##   C1  a map-bound creature bot's body is inside the current map
##   C2  the authority draws each creature ONCE (its simulated body), not a twin
##   C3  a declared row from an earlier session is REVIVED (simulated), not drawn frozen
##   C4  after change_map no creature body from the old map is left anywhere
##   C5  an authority that LOSES authority stops simulating but still draws each
##       creature once, in its map; TAKING it back simulates them again
##   A1  the elected authority writes bot registry rows whatever its Keet role; a peer
##       that is NOT the authority still needs mod
##   B1  a map-bound bot row HELD for map A (it arrived before A existed) is dropped by
##       the map change, not revived into map B
##
## Run: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_ghost_creatures.gd

const MAP_A := "res://scenes/maps/map_flat.tscn"
const MAP_B := "res://scenes/maps/map_test.tscn"
const LIVE := "ghost-hopper"
const STALE := "ghost-stale"

var ok := true
var checks := 0
var _frame := 0
var _game: Node
var _br: Node
var _gs: Node


func _p(c: bool, m: String, got: Variant = "") -> void:
	checks += 1
	print(("ok   " if c else "FAIL: ") + m + ("" if str(got) == "" else "  (" + str(got) + ")"))
	if not c:
		ok = false


func _creature_nodes() -> Array:
	var out: Array = []
	for n in root.find_children("*", "", true, false):
		var s: Script = n.get_script()
		if s != null and ["GroundCrawler", "FlyingSerpent", "Hopper"].has(String(s.get_global_name())) \
				and not n.is_queued_for_deletion():
			out.append(n)
	return out


func _named(id: String) -> Array:
	var out: Array = []
	for n in _creature_nodes():
		if String(n.name) == "Bot_" + id or String(n.name).begins_with("Bot_" + id + "@"):
			out.append(n)
	return out


func _process(_d: float) -> bool:
	_frame += 1
	match _frame:
		1:
			_game = (load("res://scenes/game/game.tscn") as PackedScene).instantiate()
			_game.name = "Game"
			root.add_child(_game)
		30:
			_game.call("change_map", MAP_A, Vector3.ZERO)
		45:
			_stage_a()
		47:
			_stage_c5()
		50:
			# B1 setup: what BotRegistry._revive does when the row's map is not there yet.
			(_br.get("_pending_rows") as Dictionary)["gate_held_bot"] = {"bindings": ["map"],
				"row": {"body": "position", "brain": {"kind": "none"}}}
			_br.set_process(true)        # as _revive does when it holds a row
			_game.call("change_map", MAP_B, Vector3.ZERO)
		65:
			_stage_b()
			print("\n%d checks — %s" % [checks, "PASS" if ok else "FAIL"])
			quit(0 if ok else 1)
			return true
	return false


func _stage_a() -> void:
	_br = root.get_node("/root/BotRegistry")
	_gs = root.get_node("/root/GameState")
	var map: Node = _game.get("current_map")
	# The registry is mod-gated; offline the gate has no role, and a refused write is
	# silent — every check below would pass vacuously. Grant it, as other gates do.
	var keet: Node = root.get_node_or_null("/root/KeetService")
	if keet != null:
		keet.current_member_id = "gate-author"
	_gs.route("world:bots:registry:*", 1, "peer")
	_p(map != null and String(map.scene_file_path) == MAP_A, "on map A", map.scene_file_path if map != null else "none")
	var r: Dictionary = _br.register(LIVE, ["map"], "hopper", {"kind": "none"}, Vector3(4, 0, 4))
	_p(bool(r.get("ok", false)), "a creature bot (hopper) is registered on map A", r)
	# ---- C1 ----
	var body: Node3D = _br.body_of(LIVE)
	_p(body != null and map.is_ancestor_of(body), "C1 its body is inside the current map",
		str(body.get_path()) if body != null else "none")
	# ---- C2 ----
	_p(_named(LIVE).size() == 1, "C2 the authority draws it once, not a twin", _named(LIVE).size())
	# ---- C3: an earlier session's declaration arrives with the room's state ----
	_gs.set_state(_br.KEY_MAP_REGISTRY + STALE, {"body": "crawler", "brain": {"kind": "none"}, "pos": [6.0, 0.0, 6.0]})
	_p(_br.is_registered(STALE), "C3 a declared row is revived — simulated by the authority")
	_p(_named(STALE).size() == 1, "C3 and drawn once (no frozen passive copy)", _named(STALE).size())
	_stage_authority()


## C5: the election moves away and back (menu_main._elect_authority calls
## BotRegistry.on_authority_changed after set_multiplayer_authority).
func _stage_c5() -> void:
	var map: Node = _game.get("current_map")
	_game.set_multiplayer_authority(999)
	_br.on_authority_changed()
	var lost_body := _named(LIVE)
	_p(not _br.is_registered(LIVE), "C5 lost authority: it no longer simulates the bot")
	_p(lost_body.size() == 1 and map.is_ancestor_of(lost_body[0]), "C5 but still draws it once, in its map",
		lost_body.size())
	_game.set_multiplayer_authority(1)
	_br.on_authority_changed()
	_p(_br.is_registered(LIVE) and _br.is_registered(STALE), "C5 taking it back: it simulates them again")
	_p(_named(LIVE).size() == 1 and _named(STALE).size() == 1, "C5 still drawn once each",
		[_named(LIVE).size(), _named(STALE).size()])


## A1: the route table as shipped (registry = mod + authority) — the gate's grant undone.
func _stage_authority() -> void:
	var keet: Node = root.get_node_or_null("/root/KeetService")
	var role_was: Variant = keet.get("current_role") if keet != null else null
	_gs.install_routes()
	if keet != null:
		keet.set("current_role", "peer")
	var key := "world:bots:registry:bot_a1"
	_p(_gs.can_write(key), "A1 the authority (role peer) may write a bot registry row")
	_game.set_multiplayer_authority(999)          # this peer is no longer the authority
	_p(not _game.is_multiplayer_authority() and not _gs.can_write(key),
		"A1 a non-authority peer (role peer) may not")
	if keet != null:
		keet.set("current_role", "mod")
	_p(_gs.can_write(key), "A1 a non-authority mod still may (the role path is unchanged)")
	_p(not _gs.can_write("world:settings:current_map") or String(keet.get("current_role")) == "admin",
		"A1 the exception is bot rows only (world:settings stays admin)")
	_game.set_multiplayer_authority(1)
	if keet != null:
		keet.set("current_role", role_was if role_was != null else "")
	_gs.route("world:bots:registry:*", 1, "peer")


func _stage_b() -> void:
	var map: Node = _game.get("current_map")
	_p(map != null and String(map.scene_file_path) == MAP_B, "on map B", map.scene_file_path if map != null else "none")
	var left := _creature_nodes()
	var where: Array = []
	for n in left:
		where.append(str((n as Node).get_path()))
	_p(left.is_empty(), "C4 no creature from map A is left anywhere", where)
	var revived: bool = (_br.get("_bots") as Dictionary).has("gate_held_bot")
	_p(not revived and not (_br.get("_pending_rows") as Dictionary).has("gate_held_bot"),
		"B1 a row held for map A is dropped by the map change, not revived in map B",
		"revived" if revived else "still held" if (_br.get("_pending_rows") as Dictionary).has("gate_held_bot") else "")