OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / dc_level_journal_gate.gd

176 lines · gd ref:
extends SceneTree
## HEADLESS GATE — the journal fold, in the engine, with the pocket cache watching.
##
## hyper-games #22 §5 items 2/5. The offline oracles (dc-terrain
## tests/test_journal_fold.cpp, test_brush_identity.cpp, test_carve_identity.cpp)
## prove the fold is pure, convergent and correctly ordered. They cannot see the
## thing that decides whether undo is USABLE: DCLevel's pocket cache.
##
## The cache is keyed by the identity of a pocket's inputs, and undo is now "mark
## the record undone and re-fold" rather than a flag flip. So a re-fold that
## reproduces the same state MUST reproduce the same keys, or every undo re-runs
## Dual Contouring over every pocket in the level — the 5.9ms -> 422ms regression
## the cache was built to kill, landing on the one interaction that has to feel
## instant. `pockets_rebuilt` in get_stats() counts exactly that, so it is the
## number this gate reads.
##
## Run:
##   cd /storage/projects/code/godot/hyper-tps
##   "$HYPER_TPS_GODOT" --headless --script res://scenes/test/dc_level_journal_gate.gd
##
## Synthetic brushes on purpose: no START.MAP, no WAD, no absolute dev paths, so
## this runs anywhere the extension builds.

var fails := 0


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


func _eqi(what: String, got: int, want: int) -> void:
	var c := got == want
	if not c:
		fails += 1
	print("  %-56s %-6d want %-6d %s" % [what, got, want, "OK" if c else "**FAIL**"])


## An axis-aligned box as six half-spaces, the flat (nx,ny,nz,d) layout add_brush takes.
func _box(lo: Vector3, hi: Vector3) -> PackedFloat32Array:
	return PackedFloat32Array([
		-1, 0, 0, -lo.x,   1, 0, 0, hi.x,
		0, -1, 0, -lo.y,   0, 1, 0, hi.y,
		0, 0, -1, -lo.z,   0, 0, 1, hi.z,
	])


func _carve_rec(author: String, seq: int, pos: Vector3) -> Dictionary:
	return {
		"author": author, "seq": seq, "kind": "carve", "undone": false,
		"carve": {"type": "sphere", "pos": pos, "radius": 0.6},
	}


## ON THE FIRST FRAME, NOT IN _init. A SceneTree script's _init runs before the
## root window is in the tree, so a node added there never gets _ready — DCLevel's
## MeshInstance3D children are created in _ready, and _build_pockets early-returns
## without a pocket node. Measured: is_inside_tree() false, ms_pocket 0.012, and
## pockets_rebuilt stuck at 0 for a build that never happened. The gate would have
## passed on nothing.
var _done := false


func _process(_delta: float) -> bool:
	if _done:
		return true
	_done = true
	_run()
	return true


func _run() -> void:
	print("=== DCLevel journal gate (headless) ===\n")
	if not ClassDB.class_exists("DCLevel"):
		printerr("DCLevel is not registered — build dc-terrain first")
		quit(1)
		return

	var lvl: Node3D = ClassDB.instantiate("DCLevel")
	get_root().add_child(lvl)

	# THREE WELL-SEPARATED BRUSHES, one carve each, so the clusters are independent
	# and locality is observable. Sharing a cluster would make "only the touched
	# pocket rebuilt" unmeasurable.
	var keys: Array[String] = []
	for i in 3:
		var x := float(i) * 20.0
		var id: int = lvl.add_brush(_box(Vector3(x, -2, -2), Vector3(x + 4, 2, 2)), 1,
									PackedInt32Array(), 0, false, "")
		keys.append(lvl.get_brush_key(id))
	print("-- the import is named deterministically --")
	var want_keys: Array[String] = ["i:0:0", "i:0:1", "i:0:2"]
	_ok("imported brushes take i:<level>:<n>", keys == want_keys)

	var journal: Array = [
		_carve_rec("alice", 100, Vector3(1.0, 0.0, 0.0)),
		_carve_rec("alice", 101, Vector3(21.0, 0.0, 0.0)),
		_carve_rec("bob", 102, Vector3(41.0, 0.0, 0.0)),
	]

	print("\n-- folding the journal builds the pockets --")
	var st: Dictionary = lvl.apply_journal(journal)
	lvl.rebuild()
	_eqi("records applied", int(st["applied"]), 3)
	_eqi("nothing skipped", int(st["skipped"]), 0)
	var s: Dictionary = lvl.get_stats()
	_eqi("three carves", int(s["carves"]), 3)
	_eqi("three pockets", int(s["pockets"]), 3)
	_eqi("all three built", int(s["pockets_rebuilt"]), 3)
	var fp_all: int = lvl.fingerprint()

	print("\n-- THE GATE: re-folding the same journal rebuilds NOTHING --")
	# Local handles, generations and carve ids must all come back identical, or the
	# cache misses on geometry that did not change.
	st = lvl.apply_journal(journal)
	lvl.rebuild()
	s = lvl.get_stats()
	_eqi("pockets still three", int(s["pockets"]), 3)
	_eqi("pockets REBUILT", int(s["pockets_rebuilt"]), 0)
	_ok("and the state is identical", lvl.fingerprint() == fp_all)

	print("\n-- undo is LOCAL: one cluster goes, the others are reused --")
	# The interactive case. Undoing the FIRST carve used to renumber every later
	# carve id and (separately) every pocket depended on every carve, so this was a
	# full-level DC pass.
	var undone: Array = journal.duplicate(true)
	(undone[0] as Dictionary)["undone"] = true
	st = lvl.apply_journal(undone)
	lvl.rebuild()
	s = lvl.get_stats()
	_eqi("the undone record is counted as such", int(st["undone"]), 1)
	_eqi("two carves left", int(s["carves"]), 2)
	_eqi("two pockets left", int(s["pockets"]), 2)
	_eqi("neither survivor was rebuilt", int(s["pockets_rebuilt"]), 0)

	print("\n-- redo restores the exact prior state --")
	st = lvl.apply_journal(journal)
	lvl.rebuild()
	s = lvl.get_stats()
	_eqi("three pockets again", int(s["pockets"]), 3)
	_eqi("only the returning one was rebuilt", int(s["pockets_rebuilt"]), 1)
	_ok("the state matches before the undo", lvl.fingerprint() == fp_all)

	print("\n-- a created brush survives a re-fold with the same handle --")
	var create: Dictionary = {
		"author": "carol", "seq": 200, "kind": "brush.create", "undone": false,
		"planes": _box(Vector3(60, -2, -2), Vector3(64, 2, 2)),
		"proj": PackedInt32Array(), "material_id": 2, "level_id": 0, "sky": false,
	}
	var j2: Array = journal.duplicate(true)
	j2.append(create)
	lvl.apply_journal(j2)
	lvl.rebuild()
	var made: int = lvl.brush_by_key("carol:200")
	_ok("the create is named by the record's dot", made != 0)
	lvl.apply_journal(j2)
	lvl.rebuild()
	_eqi("and keeps its local handle across a re-fold", lvl.brush_by_key("carol:200"), made)

	print("\n-- undoing the create tombstones it, without forgetting it --")
	var j3: Array = j2.duplicate(true)
	(j3[3] as Dictionary)["undone"] = true
	lvl.apply_journal(j3)
	lvl.rebuild()
	_ok("the brush is gone from the live set",
		bool((lvl.get_brush(made) as Dictionary).get("tombstoned", false)))
	_eqi("but its name still resolves", lvl.brush_by_key("carol:200"), made)
	lvl.apply_journal(j2)
	lvl.rebuild()
	_ok("and redo brings it back live",
		not bool((lvl.get_brush(made) as Dictionary).get("tombstoned", true)))

	print("\n%s" % ("FAILED" if fails else "PASS"))
	quit(1 if fails else 0)