diff --git a/src/game/engine/scenes/tests/fauna_overlay_proof.gd b/src/game/engine/scenes/tests/fauna_overlay_proof.gd index 632523da..3544749b 100644 --- a/src/game/engine/scenes/tests/fauna_overlay_proof.gd +++ b/src/game/engine/scenes/tests/fauna_overlay_proof.gd @@ -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))