OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_journal_index.gd

229 lines · gd ref:
extends Node
## GATE — the JOURNAL INDEX (2026-10-05: "lets just do those fixes to how history works").
##
## LevelPersistence keeps the journal parsed in memory, updated one key at a time from
## GameState.state_changed, instead of re-reading and re-parsing the whole room on every
## fold, History refresh, undo and record write. GameState stays the source of truth, so
## the index must ALWAYS answer exactly what a from-scratch rebuild answers:
##
##   1. after every step of seeded random sequences — brush creates (in gestures),
##      object create/move/prop/delete, undo, redo, seek, a PEER's record, an erase,
##      a level-source change, a map_changing drop — records(), gestures(true),
##      undo/redo depth and find_source() equal a fresh rebuild's
##   2. the Lamport max seq never goes DOWN (an erase must not let a seq be re-minted)
##   3. COST: an object move parses ONE record (not the journal), and its fold skips
##      the brush half; a brush edit's fold applies it
##   4. a gesture's undo/redo goes through the gesture table: one Ctrl+Z takes back all
##      of a 40-record gesture
##   5. THE TAIL FOLD (dc_journal::apply_tail): whenever a step's fold took the tail path
##      (only new brush records since the last fold), a forced FULL fold leaves the level
##      byte-identical (fingerprint); the tail path was actually taken; an undo is not a
##      tail
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_journal_index.tscn

var fails := 0
var checks := 0
var _rng := RandomNumberGenerator.new()


func _ok(what: String, cond: bool, note: String = "") -> void:
	checks += 1
	if not cond:
		fails += 1
	print("  %-64s %s  %s" % [what, "OK" if cond else "**FAIL**", note])


func _ready() -> void:
	var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
	gs.route("world:level:*", 1, "peer")
	var keet: Node = get_tree().root.get_node_or_null("/root/KeetService")
	if keet != null:
		keet.current_member_id = "gate-author"
		keet.current_role = "admin"                 # to write a PEER's record and erase
	var map: Node3D = (load("res://scenes/maps/map_test.tscn") as PackedScene).instantiate()
	add_child(map)
	for i in 6:
		await get_tree().process_frame
	_run(map)
	if checks < 13:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE journal_index: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	get_tree().quit(0 if fails == 0 else 1)


func _box(lo: Vector3, hi: Vector3) -> Array:
	return [1, 0, 0, hi.x, -1, 0, 0, -lo.x, 0, 1, 0, hi.y, 0, -1, 0, -lo.y, 0, 0, 1, hi.z, 0, 0, -1, -lo.z]


## Everything the index answers, as comparable strings.
static func snapshot(persist: Node) -> Dictionary:
	var recs := PackedStringArray()
	for r: Dictionary in (persist.records() as Array):
		recs.append(var_to_str(r))
	recs.sort()
	var gest := PackedStringArray()
	for e: Dictionary in (persist.gestures(true) as Array):
		var kinds: Array = (e["kinds"] as Dictionary).keys()
		kinds.sort()
		gest.append("%s|%s|%d|%s|%d|%s" % [e["g"], e["label"], int(e["count"]), str(e["undone"]), int(e["stamp"]), str(kinds)])
	return {"records": recs, "gestures": gest, "undo": persist.undo_depth(), "redo": persist.redo_depth(),
		"source": var_to_str(persist.find_source()), "live": persist.newest_live_gesture()}


func _diff(a: Dictionary, b: Dictionary) -> String:
	var out := PackedStringArray()
	for k: String in a:
		if a[k] != b[k]:
			out.append(k)
	return ",".join(out)


func _run(map: Node3D) -> void:
	var persist: Node = map.get("_persist")
	var gs: Node = get_tree().root.get_node("/root/GameState")
	_ok("map_test has a journal with an index", persist != null and persist.has_method("index_stats"))
	if persist == null or not persist.has_method("index_stats"):
		return

	# ---- 1 + 2: random sequences vs a rebuild ----
	var mismatches: Array = []
	var seq_drops := 0
	var objs: Array = []
	for run in 3:
		_rng.seed = 1000 + run
		for step in 70:
			var op := _rng.randi_range(0, 10)
			match op:
				0, 1:
					persist.begin_gesture("G")
					for i in _rng.randi_range(1, 6):
						var x := _rng.randf_range(40, 200)
						persist.record("brush.create", {"planes": _box(Vector3(x, 0, 0), Vector3(x + 1, 1, 1)),
							"proj": [], "material_id": 0, "level_id": 0, "sky": false})
					persist.end_gesture()
				2:
					var d: String = map.create_object("marker", Transform3D(Basis.IDENTITY, Vector3(_rng.randf(), 1, 0)))
					if d != "":
						objs.append(d)
				3:
					if not objs.is_empty():
						map.move_object(objs[_rng.randi() % objs.size()], Transform3D(Basis.IDENTITY, Vector3(_rng.randf() * 5, 1, 0)))
				4:
					if not objs.is_empty():
						map.set_object_props(objs[_rng.randi() % objs.size()], {"tint": [_rng.randf(), 0.5, 0.5]})
				5:
					persist.undo_last()
				6:
					persist.redo_last()
				7:
					var g: Array = persist.gestures()
					if not g.is_empty():
						persist.seek(String(g[_rng.randi() % g.size()]["g"]))
				8:
					# A PEER's record arriving (another author's key).
					gs.set_state("world:level:peer-b:op:%d" % (1791300000 + run * 1000 + step), {
						"v": 2, "kind": "brush.create", "undone": false, "g": "peer-b:%d" % step,
						"planes": _box(Vector3(300, 0, step), Vector3(301, 1, step + 1)), "proj": [],
						"material_id": 1, "level_id": 0, "sky": false})
				9:
					# An ERASE (a flatten, or a peer's delete arriving).
					var keys: Array[String] = gs.keys_with_prefix("world:level:")
					if keys.size() > 5:
						gs.erase_state(keys[_rng.randi() % keys.size()])
				10:
					if _rng.randf() < 0.5:
						persist.persist_source("%064d" % _rng.randi_range(1, 3), "gate.map", 1)
					else:
						gs.map_changing.emit("map_test")   # the mirror-drop signal: index stale
			persist.refold()
			var max_before: int = persist._max_seq_seen()
			var inc := snapshot(persist)
			persist.set("_ix_ready", false)                 # force a from-scratch rebuild
			var fresh := snapshot(persist)
			if inc != fresh:
				mismatches.append("run %d step %d op %d: %s" % [run, step, op, _diff(inc, fresh)])
			if persist._max_seq_seen() < max_before and op != 10:
				seq_drops += 1
	_ok("1. incremental index == rebuild after 210 random steps", mismatches.is_empty(),
		"%d mismatch(es) %s" % [mismatches.size(), str(mismatches.slice(0, 3))])
	var st0: Dictionary = persist.index_stats()
	_ok("1. it exercised a real journal", int(st0["records"]) > 100, "%d records" % int(st0["records"]))
	_ok("2. the max seq never went down across erases", seq_drops == 0, "%d drop(s)" % seq_drops)

	# ---- 3: cost ----
	persist.refold()
	var od: String = map.create_object("marker", Transform3D(Basis.IDENTITY, Vector3(0, 1, 0)))
	persist.refold()
	var s1: Dictionary = persist.index_stats()
	map.move_object(od, Transform3D(Basis.IDENTITY, Vector3(1, 1, 0)))
	var f1: Dictionary = persist.last_fold()
	var s2: Dictionary = persist.index_stats()
	_ok("3. an object move parses ONE record", int(s2["parses"]) - int(s1["parses"]) == 1
		and int(s2["rebuilds"]) == int(s1["rebuilds"]), "%d parses, %d rebuilds of %d records" % [
		int(s2["parses"]) - int(s1["parses"]), int(s2["rebuilds"]) - int(s1["rebuilds"]), int(s2["records"])])
	_ok("3. and its fold skips the brush half", String(f1.get("brush_fold", "")) == "skipped", str(f1.get("brush_fold", "")))
	persist.record("brush.create", {"planes": _box(Vector3(500, 0, 0), Vector3(501, 1, 1)),
		"proj": [], "material_id": 0, "level_id": 0, "sky": false})
	persist.refold()
	_ok("3. a brush edit's fold applies the brush half", String(persist.last_fold().get("brush_fold", "")) in ["applied", "tail"],
		persist.last_fold().get("brush_fold", ""))

	# ---- 5: the TAIL FOLD over a seeded sequence (no index rebuilds: one would rightly
	#      disable it). Creates and deletes append (tail); a move pre-applies its edit
	#      (fingerprint changed -> full); an undo changes a record (full).
	_rng.seed = 4242
	var tails := 0
	var tail_bad: Array = []
	var live_keys: Array = []
	persist.refold()
	for step in 80:
		var op := _rng.randi_range(0, 9)
		if op <= 4 or live_keys.is_empty():
			var x := _rng.randf_range(100, 160)
			live_keys.append(persist.record("brush.create", {"planes": _box(Vector3(x, 0, 0), Vector3(x + 1, 1, 1)),
				"proj": [], "material_id": 0, "level_id": 0, "sky": false}))
			persist.refold()
		elif op <= 6:
			var k: String = live_keys.pop_at(_rng.randi() % live_keys.size())
			map.delete_brushes([map.level.brush_by_key(k)])
		elif op == 7:
			map.select_brushes(PackedInt32Array([map.level.brush_by_key(live_keys[_rng.randi() % live_keys.size()])]))
			map.translate_selection(Vector3(0, 0, 0.5))
			persist.refold()
		else:
			persist.undo_last()
			persist.refold()
		if String(persist.last_fold().get("brush_fold", "")) == "tail":
			tails += 1
			var fp_tail: int = map.level.fingerprint()
			persist.set("_ix_tail_ok", false)
			persist.set("_brush_ver_folded", -1)
			persist.refold()                                   # forced FULL fold
			if int(map.level.fingerprint()) != fp_tail:
				tail_bad.append("step %d op %d" % [step, op])
	_ok("5. every tail fold == a forced full fold (fingerprint)", tail_bad.is_empty(),
		"%d bad %s" % [tail_bad.size(), str(tail_bad.slice(0, 3))])
	_ok("5. the tail path was taken", tails >= 20, "%d of 80 steps" % tails)

	# ---- 4: a big gesture through the table ----
	var live0: int = (map.level.get_live_brush_ids() as PackedInt32Array).size()
	persist.begin_gesture("Big")
	for i in 40:
		persist.record("brush.create", {"planes": _box(Vector3(600 + i * 2, 0, 0), Vector3(601 + i * 2, 1, 1)),
			"proj": [], "material_id": 0, "level_id": 0, "sky": false})
	persist.end_gesture()
	persist.refold()
	var g0: Dictionary = (persist.gestures() as Array)[0]
	_ok("4. a 40-record gesture is one History row", String(g0["label"]) == "Big" and int(g0["count"]) == 40, str(g0))
	_ok("4. one Ctrl+Z takes all 40 back", persist.undo_last() != "" and _refolded_live(map, persist) == live0)
	_ok("5. an undo is NOT a tail (it changes records)", String(persist.last_fold().get("brush_fold", "")) == "applied",
		persist.last_fold().get("brush_fold", ""))
	_ok("4. one redo brings all 40 back", persist.redo_last() != "" and _refolded_live(map, persist) == live0 + 40)


func _refolded_live(map: Node3D, persist: Node) -> int:
	persist.refold()
	return (map.level.get_live_brush_ids() as PackedInt32Array).size()