feat(@projects/@magic-civilization): add fauna overlay proof test suite

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-06-07 21:37:16 -07:00
parent 7dd4f6c894
commit 482baf301d

View file

@ -1,55 +1,74 @@
extends Node2D
## p2-80 render-hook proof — the fauna overlay makes the living world VISIBLE
## on the playable map.
## p2-80 render-hook proof — the fauna overlay makes the living world VISIBLE on
## the playable map, FOG-GATED, over a REAL worldgen distribution.
##
## Drives the SAME pieces the live game uses: the `EcologyState` autoload engine
## (`GdFaunaEcology`, ticked each turn by `turn_manager`) over a real terrain
## grid, and the production `FaunaOverlayRenderer` node (the exact node
## `world_map.gd` instantiates). After N turns of continuous ecology, it toggles
## the "wildlife_habitat" lens on (`EventBus.map_overlay_changed`) and fires
## `EventBus.worldsim_updated` — exactly the signals the live turn loop emits —
## then screenshots the result.
## This is the faithful close of the p2-80 render-hook bullet. It drives the
## EXACT production turn sequence — no synthetic grid, no hand-seeded clusters:
##
## The visible green→yellow fauna tint spreading across tiles is the "pixels"
## proof that the worldsim is no longer invisible during play. Self-capturing
## (models worldsim_ecology_proof.gd): renders, screenshots, quits. Headless via
## weston (Rail 5 + scripts/ui-proof-capture.sh).
## 1. Real worldgen via the production `MapGenerator` → a real `GameMap`
## (registered as `GameState`'s primary-layer map, so the fauna overlay's
## fog gate reads `GameState.get_game_map()` just like in play).
## 2. Real players; a partial radius reveal around the human founder so the map
## is EXPLORED-near / UNEXPLORED-far — the contrast that proves the fog gate
## actually filters.
## 3. The production turn loop: `Climate.process_turn(game_map, t, seed)` builds
## and syncs the Rust `GdGridState`, then `EcologyState.tick(climate._grid,
## …)` runs world-genesis seeding + Lotka-Volterra dynamics over it — the
## same two calls `turn_manager` makes every turn.
## 4. The production `FaunaOverlayRenderer` node, toggled on via the real
## `wildlife_habitat` lens signal and refreshed via `worldsim_updated`.
##
## Fog is ON (this proof never sets FORCE_DISABLE_FOGOFWAR — it asserts the flag
## reads false at runtime). Player 0 is human, so the overlay's `_view_player_index`
## resolves to 0, aligned with the revealed player. The printed `fog_gate_pass`
## count (tiles that are BOTH populated AND visible) is the success metric: the
## screenshot only proves the bullet if that number is > 0 and < the full
## populated set (i.e. the gate is filtering, not pass-through).
##
## Self-capturing (models worldsim_ecology_proof.gd): renders, screenshots, quits.
## Headless via weston (Rail 5 + scripts/ui-proof-capture.sh).
const MapGeneratorScript: GDScript = preload("res://engine/src/generation/map_generator.gd")
const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd")
const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd")
const ClimateScript: GDScript = preload("res://engine/src/modules/climate/climate.gd")
const FaunaOverlayRendererScript: GDScript = preload(
"res://engine/src/rendering/fauna_overlay_renderer.gd"
)
const OUTPUT_DIR: String = "user://screenshots"
const SPECIES_DIR: String = "res://public/resources/ecology/fauna/species"
const SAMPLE_SPECIES: Array[String] = ["grey_wolf", "abalone", "red_deer"]
const MAP_W: int = 14
const MAP_H: int = 10
const TURNS: int = 12
const SEED: int = 0xC0FFEE
## Seed clusters so the continuous ecology (emergence is throttled) has live
## populations to evolve over the run — the live game reaches the same state via
## emergence over more turns; the overlay code path is identical either way.
const SEED_CELLS: Array[Vector2i] = [
Vector2i(4, 4), Vector2i(5, 4), Vector2i(4, 5), Vector2i(10, 6),
]
## Real new-game settings (mirrors gameplay_arc_proof). A duel map is small
## enough to render legibly yet large enough that a partial reveal leaves a
## visibly-dark unexplored frontier.
const NEW_GAME: Dictionary = {
"seed": 5, "map_type": "continents", "map_size": "duel", "num_players": 2,
}
## Enough turns for seeding to bootstrap (turn 0) and the LV dynamics to regulate
## to a stable distribution (matches the seed-5 autoplay verification arc).
const TURNS: int = 25
## Fraction of the map radius revealed around the human founder. < 1.0 so a dark
## unexplored frontier remains — the visible contrast that proves the fog gate.
const REVEAL_FRACTION: float = 0.5
var _grid: RefCounted = null
var _game_map: RefCounted = null
var _climate: RefCounted = null
var _fauna_overlay: Node2D = null
var _terrain_tiles: Array[Vector2i] = []
var _all_positions: Array[Vector2i] = []
var _view_index: int = 0
var _captured: bool = false
var _start_tiles: int = 0
var _end_tiles: int = 0
func _ready() -> void:
RenderingServer.set_default_clear_color(Color(0.04, 0.05, 0.06))
RenderingServer.set_default_clear_color(Color(0.03, 0.04, 0.05))
get_viewport().size = Vector2i(1920, 1080)
DisplayServer.window_set_size(Vector2i(1920, 1080))
await get_tree().process_frame
_run_live_ecology()
_build_real_game()
_run_production_turn_loop()
_reveal_partial()
_setup_overlay_and_camera()
_print_stats()
@ -58,45 +77,86 @@ func _ready() -> void:
_capture_and_quit()
func _run_live_ecology() -> void:
_grid = _make_terrain_grid()
# Use the real autoload engine — the overlay reads it via tile_densities().
EcologyState.reset()
var fauna: RefCounted = EcologyState.fauna_ecology
if fauna == null:
push_error("FaunaOverlayProof: EcologyState.fauna_ecology unavailable (GdFaunaEcology missing)")
## Stand up a real game exactly as the production boot path does: load content,
## generate a real map, register it as the primary layer, add real players, and
## name player 0 the local human viewer.
func _build_real_game() -> void:
DataLoader.load_theme("age-of-dwarves")
DataLoader.load_world("earth")
ThemeAssets.set_theme("age-of-dwarves")
GameState.initialize_game(NEW_GAME)
var gen: RefCounted = MapGeneratorScript.new()
_game_map = gen.generate(NEW_GAME)
if _game_map == null:
push_error("FaunaOverlayProof: MapGenerator returned null")
get_tree().quit(1)
return
for name: String in SAMPLE_SPECIES:
var raw: String = FileAccess.get_file_as_string("%s/%s.json" % [SPECIES_DIR, name])
if raw == "":
continue
var id: int = int(fauna.call("register_species_from_json", raw))
if id < 0:
continue
for cell: Vector2i in SEED_CELLS:
fauna.call("seed_population", cell.x, cell.y, id, 25.0)
GameState.get_primary_layer()["map"] = _game_map
for pos: Vector2i in _game_map.tiles:
_all_positions.append(pos)
_start_tiles = int(fauna.call("populated_tile_count"))
# Tick the live continuous ecology path N turns (EcologyState.tick is what
# turn_manager calls each turn).
for i: int in range(int(NEW_GAME["num_players"])):
var player: PlayerScript = PlayerScript.new()
player.index = i
# Player 0 is the local human viewer — the overlay's _view_player_index
# prefers the human, so the fog gate resolves to slot 0, aligned with the
# reveal below. (Exercises the human-prefer branch of _view_player_index.)
player.is_human = i == 0
player.player_name = "Clan %d" % (i + 1)
player.race_id = "dwarf"
GameState.players.append(player)
var start: Vector2i = Vector2i.ZERO
if i < _game_map.start_positions.size():
start = _game_map.start_positions[i]
var founder: UnitScript = UnitScript.new("dwarf_founder", i, start)
founder.id = "founder_%d" % i
player.units.append(founder)
_view_index = 0
## The production per-turn worldsim pair, ticked TURNS times: climate physics
## (which owns the Rust GdGridState the ecology runs on) then the fauna engine.
## Identical to turn_manager's sequence — `EcologyState.tick` lazily registers
## the species library and runs world-genesis seeding on the first tick.
func _run_production_turn_loop() -> void:
_climate = ClimateScript.new()
EcologyState.reset()
for t: int in range(TURNS):
EcologyState.tick(_grid, SEED + t)
_end_tiles = int(fauna.call("populated_tile_count"))
_climate.process_turn(_game_map, t, int(NEW_GAME["seed"]))
var grid: RefCounted = _climate.get("_grid") as RefCounted
if grid == null:
push_error("FaunaOverlayProof: climate built no GdGridState")
get_tree().quit(1)
return
EcologyState.tick(grid, int(NEW_GAME["seed"]) + t)
## Reveal a connected region around the human founder so the map is explored-near
## / unexplored-far. Visibility 2 (currently visible) clears the fog gate; tiles
## left at 0 (unexplored) stay dark and must NOT draw fauna — the proof's contrast.
func _reveal_partial() -> void:
var center: Vector2i = GameState.players[0].units[0].position
var max_d: int = 1
for pos: Vector2i in _all_positions:
max_d = maxi(max_d, HexUtilsScript.hex_distance(center, pos))
var radius: int = maxi(2, int(round(float(max_d) * REVEAL_FRACTION)))
for tile: Resource in _game_map.get_tiles_in_range(center, radius):
if tile != null:
tile.set_visibility(_view_index, 2)
func _setup_overlay_and_camera() -> void:
_fauna_overlay = FaunaOverlayRendererScript.new()
_fauna_overlay.name = "FaunaOverlayRenderer"
add_child(_fauna_overlay)
# Drive it with the exact signals the live loop uses.
# Drive it with the exact signals the live loop emits.
EventBus.map_overlay_changed.emit("wildlife_habitat")
EventBus.worldsim_updated.emit(TURNS)
# Frame the whole map: compute the tile-pixel bounding box and fit a camera.
var min_p: Vector2 = Vector2(INF, INF)
var max_p: Vector2 = Vector2(-INF, -INF)
for pos: Vector2i in _terrain_tiles:
for pos: Vector2i in _all_positions:
var o: Vector2 = HexUtilsScript.axial_to_pixel(pos)
min_p = min_p.min(o)
max_p = max_p.max(o + Vector2(HexUtilsScript.HEX_WIDTH, HexUtilsScript.HEX_HEIGHT))
@ -110,42 +170,46 @@ func _setup_overlay_and_camera() -> void:
cam.make_current()
func _make_terrain_grid() -> RefCounted:
var grid: RefCounted = GdGridState.create(MAP_W, MAP_H)
_terrain_tiles.clear()
for row: int in range(MAP_H):
for col: int in range(MAP_W):
var lat: float = 1.0 - absf((float(row) - MAP_H / 2.0) / (MAP_H / 2.0))
var noise: float = fmod(float(col * 13 + row * 7) * 0.0173, 1.0)
grid.call("set_tile_dict", col, row, {
"temperature": 0.20 + lat * 0.50 + noise * 0.10,
"moisture": 0.30 + noise * 0.40,
"elevation": 0.20 + noise * 0.30,
"habitat_suitability": 0.4 + noise * 0.4,
"quality": 3,
"biome_id": "temperate_forest",
})
_terrain_tiles.append(Vector2i(col, row))
return grid
## Dim terrain backdrop, shaded by the viewer's fog so the explored/unexplored
## boundary is legible behind the fauna tint: unexplored tiles read near-black,
## explored land reads dim green.
func _draw() -> void:
# Dim terrain backdrop so the fauna tint reads against land, not the void.
for pos: Vector2i in _terrain_tiles:
for pos: Vector2i in _all_positions:
var o: Vector2 = HexUtilsScript.axial_to_pixel(pos)
var poly: PackedVector2Array = PackedVector2Array()
for v: Vector2 in HexUtilsScript.hex_polygon:
poly.append(v + o)
draw_colored_polygon(poly, Color(0.12, 0.16, 0.12, 1.0))
draw_polyline(poly + PackedVector2Array([poly[0]]), Color(0.0, 0.0, 0.0, 0.35), 2.0)
var tile: Resource = _game_map.get_tile(pos)
var explored: bool = tile != null and int(tile.get_visibility(_view_index)) >= 1
var shade: Color = Color(0.11, 0.15, 0.11, 1.0) if explored else Color(0.04, 0.05, 0.06, 1.0)
draw_colored_polygon(poly, shade)
draw_polyline(poly + PackedVector2Array([poly[0]]), Color(0.0, 0.0, 0.0, 0.30), 1.5)
func _print_stats() -> void:
print("=== p2-80 Fauna Overlay Proof ===")
print("Grid: %dx%d, %d turns, seed %d" % [MAP_W, MAP_H, TURNS, SEED])
print("Populated tiles: start=%d end=%d" % [_start_tiles, _end_tiles])
var fog_off: bool = EnvConfig.get_bool("FORCE_DISABLE_FOGOFWAR")
var dens: Dictionary = EcologyState.tile_densities()
print("tile_densities() returned %d populated tiles for the overlay" % dens.size())
var revealed: int = 0
for pos: Vector2i in _all_positions:
var t: Resource = _game_map.get_tile(pos)
if t != null and int(t.get_visibility(_view_index)) >= 1:
revealed += 1
var gate_pass: int = 0
for pos: Vector2i in dens:
if float(dens[pos]) <= 0.0:
continue
var t: Resource = _game_map.get_tile(pos)
if t != null and int(t.get_visibility(_view_index)) >= 1:
gate_pass += 1
print("=== p2-80 Fauna Overlay Proof (real worldgen, fog-gated) ===")
print("Map: %s, %d turns, seed %d, %d tiles" % [
NEW_GAME["map_size"], TURNS, int(NEW_GAME["seed"]), _all_positions.size()
])
print("FORCE_DISABLE_FOGOFWAR (must be false): %s" % str(fog_off))
print("view_player_index (must be 0/human): %d" % _view_index)
print("populated tiles (live distribution): %d" % dens.size())
print("revealed tiles (fog-cleared): %d / %d" % [revealed, _all_positions.size()])
print("fog_gate_pass (populated AND visible — drawn): %d" % gate_pass)
print("Overlay visible=%s" % str(_fauna_overlay.visible))