extends SceneTree
## GATE — TerraceGeissGraph, the Geiss field draped on the random terraces (2026-10-06).
## C1 the combined graph compiles to proc_density (both builders in one graph, every
## node on a GPU path)
## C2 the composition is EXACT: combined(x, y, z) == Geiss(x, y - T(x, z), z), sampled
## against a separate TerraceGraph and a separate UnderseaGeissGraph (ground_y 0)
## C3 the knobs stay distinct: every terrace knob as "knob:<name>", every Geiss knob as
## "knob:geiss.<name>"
## C4 a Geiss knob reaches the field (prewarp 0 changes it) and a terrace knob does too
## C5 CAVES: nothing carved where the mask is 0 or above the roof line (entrances only
## via walls); beside risers below the roof, chambers hollow 5-60 % of the rock;
## negative control: an impossible threshold carves nothing
## CPU, through evaluate_socket with a constant Vector in place of Position.
##
## Run: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_terrace_geiss_graph.gd
var ok := true
var checks := 0
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 _init() -> void:
_run()
print("\n%d checks — %s" % [checks, "PASS" if ok else "FAIL"])
quit(0 if ok else 1)
func _at(ids: Dictionary, p: Vector3, what: String = "out") -> float:
var g: ProcNodeGraph = ids["graph"] if ids.has("graph") else ids["_g"]
g.get_node(ids["p"]).set("value", p)
return float(g.evaluate_socket(ids[what], 0))
func _run() -> void:
# ---- C1 ----
var shipped := TerraceGeissGraph.make()
_p(String(shipped.compile_to_glsl_function("proc_density", false, PackedStringArray())).contains("float proc_density"),
"C1 the combined graph compiles to proc_density", shipped.get_node_count())
# EXACTLY ONE Group Output, fed by "out". The GPU compiler takes the FIRST one; with a
# sub-builder's left in the graph the map rendered the pre-cave surface, and every
# other check here passed because they evaluate nodes by id, not the output.
var probe := TerraceGeissGraph.build(ProcNodeGraph.new(), null)
var pg: ProcNodeGraph = probe["graph"]
var gos: Array[int] = []
for i in pg.get_node_count():
if pg.get_node(i) is GroupOutputNode: gos.append(i)
var fed := -1
var cf := pg.get_connections_flat()
for i in range(0, cf.size(), 4):
if gos.size() == 1 and cf[i + 2] == gos[0] and cf[i + 3] == 0: fed = cf[i]
_p(gos.size() == 1 and fed == int(probe["out"]), "C1 exactly ONE Group Output, fed by out (the caves)",
"%d outputs, fed by %d, out %d" % [gos.size(), fed, int(probe["out"])])
# ---- C2 ----
var c := TerraceGeissGraph.build(ProcNodeGraph.new(), Vector3.ZERO)
var tb := TerraceGraph.new()
var t := tb.build(ProcNodeGraph.new(), Vector3.ZERO)
t["_g"] = tb.g
var gb := UnderseaGeissGraph.new()
var gg := gb.build(ProcNodeGraph.new(), Vector3.ZERO, {"ground_y": 0.0})
gg["_g"] = gb.g
var worst := 0.0
var rng := RandomNumberGenerator.new()
rng.seed = 11
for i in 40:
var x := rng.randf_range(-5000, 5000)
var z := rng.randf_range(-5000, 5000)
var tv := _at(t, Vector3(x, 0, z), "T")
var y := tv + rng.randf_range(-30.0, 30.0)
worst = maxf(worst, absf(_at(c, Vector3(x, y, z), "surface") - _at(gg, Vector3(x, y - tv, z))))
_p(worst < 1e-3, "C2 surface(x,y,z) == Geiss(x, y - T, z) at 40 points within 30 m of the terrace", worst)
# ---- C3 ----
var kn := UnderseaGeissGraph.knobs_from(shipped)
var missing: Array = []
for key: String in TerraceGraph.TKNOBS:
if not kn.has(key): missing.append(key)
for key: String in UnderseaGeissGraph.KNOBS:
if not kn.has(TerraceGeissGraph.GEISS_PREFIX + key): missing.append(TerraceGeissGraph.GEISS_PREFIX + key)
_p(missing.is_empty(), "C3 terrace knobs bare, Geiss knobs prefixed '%s' (%d knobs)" % [TerraceGeissGraph.GEISS_PREFIX, kn.size()], missing)
# ---- C4 ----
var flat := TerraceGeissGraph.build(ProcNodeGraph.new(), Vector3.ZERO, {}, {"prewarp": 0.0})
var steep := TerraceGeissGraph.build(ProcNodeGraph.new(), Vector3.ZERO, {"step": 40.0}, {})
var dg := 0
var dt := 0
for i in 20:
var p := Vector3(rng.randf_range(-5000, 5000), 0.0, rng.randf_range(-5000, 5000))
p.y = _at(c, p, "T") + rng.randf_range(-10.0, 10.0)
if absf(_at(flat, p) - _at(c, p)) > 0.01: dg += 1
if absf(_at(steep, p) - _at(c, p)) > 0.01: dt += 1
_p(dg > 10 and dt > 10, "C4 a Geiss knob (prewarp) and a terrace knob (step) both reach the field", "%d / %d of 20" % [dg, dt])
# ---- C5 caves ----
var roof := float(TerraceGeissGraph.CAVE_KNOBS["cave_roof"])
var none := TerraceGeissGraph.build(ProcNodeGraph.new(), Vector3.ZERO, {}, {}, {"cave_threshold": 1000.0})
var leak := 0 # carved where the mask is 0, or above the roof line
var masked := 0 # samples with the mask fully on
var carved := 0 # ...of which rock (surface > 0) became hollow (out < 0)
var rock := 0
var ctl := 0 # negative control: carved with an impossible threshold
for i in 3000:
var p := Vector3(rng.randf_range(-6000, 6000), 0.0, rng.randf_range(-6000, 6000))
var tv := _at(c, p, "T")
p.y = tv + rng.randf_range(-70.0, 10.0)
var m := _at(c, p, "cave_mask")
var sv := _at(c, p, "surface")
var ov := _at(c, p)
# CARVED = solid became hollow. Not "value changed": deep in the rock the guarded
# cut (~100 - L) can be the min() and lower d's VALUE while it stays solid —
# measured 13 such samples, none of them a sign change. Near the surface (|d| < 5,
# where the mesh is) the value must be exactly d's, or the walls move.
if (m == 0.0 or p.y > tv - roof) and ((sv > 0.0) != (ov > 0.0) or (absf(sv) < 5.0 and absf(ov - sv) > 1e-4)):
leak += 1
if m >= 0.99 and sv > 0.0:
masked += 1
rock += 1
if ov < 0.0:
carved += 1
if sv > 0.0 and _at(none, p) < 0.0:
ctl += 1
_p(leak == 0, "C5 nothing is carved where the mask is 0 or above the roof line (T - %.0f m): entrances via walls only" % roof, leak)
_p(masked >= 30 and carved > 0.05 * rock and carved < 0.6 * rock,
"C5 beside risers below the roof, chambers hollow 5-60 % of the rock", "%d of %d rock samples" % [carved, rock])
_p(ctl == 0, "C5 negative control: an impossible cave_threshold carves nothing", ctl)