feat(@projects/@magic-civilization): ✨ add fauna overlay proof test suite
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
7dd4f6c894
commit
482baf301d
1 changed files with 146 additions and 82 deletions
|
|
@ -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))
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue