OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_home_starter.gd

165 lines · gd ref:
extends SceneTree
## Gate H6 (rules half) — docs/home-world-design.md §6: a release's STARTER MAP.
##
## build(): a dev's home-room map -> the starter that ships
##   1. keeps the map's records and plain keys, verbatim (keys AND raw values)
##   2. drops world:players:* (positions are per player)
##   3. drops undone journal records (the starter is what you see, not your history)
##   4. keeps library entries (room-global world:assets:*) only if the map uses them
##   5. finds the content hashes the map references
## seed(): the starter -> a player's home, ONCE per map
##   6. writes only keys the home does not have; keeps what is there
##   7. sets world:starter:applied, and a second seed writes nothing
##   8. refuses a starter with an unknown format, writing nothing
##
## Pure functions over a fake state; no daemon, room or filesystem.

const S := preload("res://scenes/maps/home_starter.gd")

const MAP := "map_test"
const HASH_A := "aaaa000000000000000000000000000000000000000000000000000000000001"
const HASH_B := "bbbb000000000000000000000000000000000000000000000000000000000002"

var ok := true
var _ran := false
var _finished := false


class FakeState:
	extends RefCounted
	var kv: Dictionary = {}
	var writes: Array[String] = []
	var refuse := ""          # a key this state drops, as GameState's can_write does

	func has_state(k: String) -> bool:
		return kv.has(k)

	func get_state(k: String, d: Variant = null) -> Variant:
		return kv.get(k, d)

	func set_state(k: String, v: Variant) -> void:
		writes.append(k)
		if k == refuse:
			return
		kv[k] = v


## The real GameState + KeetService autoloads (no daemon: the durable send fails
## harmlessly, the local mirror is what is checked).
func _gamestate(st: Dictionary) -> void:
	var gs: Node = root.get_node_or_null("GameState")
	var keet: Node = root.get_node_or_null("KeetService")
	_p(gs != null and keet != null, "10. GameState + KeetService autoloads present")
	if gs == null or keet == null:
		return
	gs.install_routes()
	gs.current_map_id = MAP
	keet.home_room_id = "gate-home"
	keet.current_role = "admin"
	var r0: Dictionary = gs.seed_home_starter("gate-other", st)
	_p(r0.is_empty() and not gs.has_state("world:level:dev:op:10"), "10. another room is never seeded")
	keet.current_role = "peer"            # the role RPC has not answered yet
	var r1: Dictionary = gs.seed_home_starter("gate-home", st)
	_p(bool(r1.get("deferred", false)) and not gs.has_state(S.MARKER),
			"10. before the role is known: deferred, no marker (r=%s)" % str(r1))
	keet.current_role = "admin"
	keet.role_changed.emit("gate-home", "admin")
	_p(gs.has_state("world:level:dev:op:10") and gs.has_state("world:level:dev:op:12")
			and gs.has_state(S.MARKER), "10. role arrives (admin): seeded, dev-authored records accepted")
	var r2: Dictionary = gs.seed_home_starter("gate-home", st)
	_p(bool(r2.get("skipped", false)), "10. second entry: skipped")
	keet.home_room_id = ""
	keet.current_role = "peer"


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()
	_p(_finished, "the gate ran to the end (no script error aborted it)")
	print("GATE home_starter: %s" % ("PASS" if ok else "FAIL"))
	quit(0 if ok else 1)
	return true


func _k(rest: String) -> String:
	return "map:%s:%s" % [MAP, rest]


func _run() -> void:
	var live := var_to_str({"v": 2, "kind": "brush.create", "undone": false, "g": "dev:10", "planes": [0.0, 1.0, 0.0, 2.0]})
	var undone := var_to_str({"v": 2, "kind": "brush.create", "undone": true, "undone_at": 12, "g": "dev:11"})
	var obj := var_to_str({"v": 2, "kind": "object.create", "undone": false, "type": "model",
		"props": {"model": HASH_A}})
	var sky := var_to_str(11.5)
	var entries := {
		_k("world:level:dev:op:10"): live,
		_k("world:level:dev:op:11"): undone,
		_k("world:level:dev:op:12"): obj,
		_k("world:sky:current_time"): sky,
		_k("world:players:dev"): var_to_str({"pos": [1, 2, 3]}),
	}
	var assets := {
		"world:assets:model:crate": var_to_str({"oid": HASH_A, "size": 10, "ext": "glb"}),
		"world:assets:model:unused": var_to_str({"oid": HASH_B, "size": 10, "ext": "glb"}),
	}
	var st: Dictionary = S.build(MAP, "res://scenes/maps/map_test.tscn", entries, assets, "0.0.test")
	var e: Dictionary = st.get("entries", {})
	_p(int(st.get("format", 0)) == S.FORMAT, "format stamped")
	_p(e.get(_k("world:level:dev:op:10")) == live and e.get(_k("world:sky:current_time")) == sky,
			"1. records and plain keys kept verbatim")
	_p(not e.has(_k("world:players:dev")), "2. player positions dropped")
	_p(not e.has(_k("world:level:dev:op:11")), "3. undone records dropped")
	var a: Dictionary = st.get("assets", {})
	_p(a.has("world:assets:model:crate") and not a.has("world:assets:model:unused"),
			"4. only the library entries the map uses (assets=%s)" % str(a.keys()))
	var hs: Array = S.referenced_hashes(st)
	_p(hs.has(HASH_A) and not hs.has(HASH_B), "5. referenced content found (%s)" % str(hs))

	# seed
	var home := FakeState.new()
	home.kv["world:sky:current_time"] = 7.0      # the player already changed the sky
	var r: Dictionary = S.seed(st, home)
	_p(home.kv.has("world:level:dev:op:10") and home.kv.has("world:level:dev:op:12"),
			"6. absent records written (plain keys)")
	_p(float(home.kv["world:sky:current_time"]) == 7.0, "6. a key the home has is kept (sky stays 7.0)")
	_p(home.kv["world:level:dev:op:10"] is Dictionary, "6. values written as Variants, not raw strings")
	_p(home.kv.has("world:assets:model:crate"), "6. the used library entry is written")
	_p(home.kv.has(S.MARKER), "7. world:starter:applied set (r=%s)" % str(r))
	var n := home.writes.size()
	var r2: Dictionary = S.seed(st, home)
	_p(home.writes.size() == n and bool(r2.get("skipped", false)), "7. a second seed writes nothing")

	# 7b. the file round trip: the starter ships as JSON (format comes back a float)
	var from_file: Variant = JSON.parse_string(JSON.stringify(st, "  ", true))
	var viajson := FakeState.new()
	var rj: Dictionary = S.seed(from_file, viajson)
	_p(int(rj.get("written", 0)) == 4 and viajson.kv.has(S.MARKER)
			and viajson.kv["world:level:dev:op:10"] is Dictionary,
			"7b. a starter read back from JSON seeds the same (r=%s)" % str(rj))

	# 9. a write the home REFUSES: no marker, so the next load tries again
	var picky := FakeState.new()
	picky.refuse = "world:level:dev:op:12"
	var r4: Dictionary = S.seed(st, picky)
	_p(int(r4.get("refused", 0)) == 1 and not picky.kv.has(S.MARKER),
			"9. a refused write leaves no marker (r=%s)" % str(r4))

	# 10. GameState: only at Home, and only once the role (admin at Home) is known
	_gamestate(st)

	var bad := st.duplicate(true)
	bad["format"] = S.FORMAT + 1
	var fresh := FakeState.new()
	var r3: Dictionary = S.seed(bad, fresh)
	_p(fresh.writes.is_empty() and String(r3.get("error", "")) != "",
			"8. unknown format refused, nothing written (r=%s)" % str(r3))
	_finished = true