diff --git a/src/game/engine/scenes/tests/worldsim_ecology_proof.gd b/src/game/engine/scenes/tests/worldsim_ecology_proof.gd new file mode 100644 index 00000000..75c190fb --- /dev/null +++ b/src/game/engine/scenes/tests/worldsim_ecology_proof.gd @@ -0,0 +1,197 @@ +extends Node2D +## Increment 3a — Living-World Ecology Proof Scene. +## +## Drives the SAME Rust bridge the live `turn_manager.gd` loop ticks every turn +## (`GdFaunaEcology.tick_populations` → `EcologyEngine::process_step`, which now +## includes the Increment-2 carrying-capacity migration step alongside +## emergence, Lotka-Volterra dynamics, dispersal, and tier advancement) against +## a real terrain `GdGridState`, then renders fauna distribution BEFORE vs AFTER +## N turns side-by-side. The visible spread of fauna across the map is the +## "pixels" proof that the living world evolves through the playable path. +## +## Self-capturing (models climate_proof.gd): renders, screenshots +## `user://screenshots/worldsim_ecology_proof_.png`, and quits. + +const CELL: int = 22 +const MARGIN: Vector2i = Vector2i(24, 48) +const OUTPUT_DIR: String = "user://screenshots" + +const MAP_W: int = 26 +const MAP_H: int = 16 +const TURNS: int = 20 +const SEED: int = 0xC0FFEE +const SPECIES_DIR: String = "res://public/resources/ecology/fauna/species" +const SAMPLE_SPECIES: Array[String] = ["grey_wolf", "abalone", "red_deer"] + +var _grid: RefCounted = null +var _fauna: RefCounted = null +var _before: Dictionary = {} # (col,row) → total population at turn 0 +var _after: Dictionary = {} # (col,row) → total population at turn N +var _before_tiles: int = 0 +var _after_tiles: int = 0 +var _peak_pop: float = 1.0 +var _captured: bool = false + + +func _ready() -> void: + RenderingServer.set_default_clear_color(Color.BLACK) + get_viewport().size = Vector2i(1920, 1080) + DisplayServer.window_set_size(Vector2i(1920, 1080)) + await get_tree().process_frame + + _run_sim() + _print_stats() + queue_redraw() + + for _i: int in range(10): + await get_tree().process_frame + _capture_and_quit() + + +func _run_sim() -> void: + _grid = _make_terrain_grid() + _fauna = ClassDB.instantiate("GdFaunaEcology") as RefCounted + _register_and_seed() + + # Snapshot BEFORE. + _before = _snapshot_populations() + _before_tiles = int(_fauna.call("populated_tile_count")) + + # Tick the live continuous ecology path N turns. + for t: int in range(TURNS): + _fauna.call("tick_populations", _grid, SEED + t) + + # Snapshot AFTER. + _after = _snapshot_populations() + _after_tiles = int(_fauna.call("populated_tile_count")) + + # Peak population for color normalisation. + for v: Variant in _after.values(): + _peak_pop = maxf(_peak_pop, float(v)) + for v: Variant in _before.values(): + _peak_pop = maxf(_peak_pop, float(v)) + + +func _make_terrain_grid() -> RefCounted: + var grid: RefCounted = GdGridState.create(MAP_W, MAP_H) + 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", + }) + return grid + + +func _register_and_seed() -> void: + 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 [Vector2i(6, 5), Vector2i(7, 5), Vector2i(6, 6), Vector2i(18, 10)]: + _fauna.call("seed_population", cell.x, cell.y, id, 25.0) + + +func _snapshot_populations() -> Dictionary: + var out: Dictionary = {} + for row: int in range(MAP_H): + for col: int in range(MAP_W): + var slots: Array = _fauna.call("populations_on_tile", col, row) + if slots.is_empty(): + continue + var total: float = 0.0 + for s: Dictionary in slots: + total += float(s.get("population", 0.0)) + if total > 0.01: + out[Vector2i(col, row)] = total + return out + + +func _print_stats() -> void: + print("=== Increment 3a — Living-World Ecology Proof ===") + print("Grid: %dx%d, %d turns, seed %d" % [MAP_W, MAP_H, TURNS, SEED]) + print("Populated tiles: before=%d after=%d (delta +%d)" % [ + _before_tiles, _after_tiles, _after_tiles - _before_tiles + ]) + print("Total slots after: %d" % int(_fauna.call("population_slot_count"))) + print("Peak tile population: %.2f" % _peak_pop) + + +func _draw() -> void: + var font: Font = ThemeDB.fallback_font + draw_string( + font, Vector2(MARGIN.x, 28), + "Increment 3a — Living World: fauna evolve through the live bridge " + + "(emergence + Lotka-Volterra + dispersal + migration) — %dx%d, %d turns" + % [MAP_W, MAP_H, TURNS], + HORIZONTAL_ALIGNMENT_LEFT, -1, 18, Color.WHITE + ) + + _draw_panel(MARGIN.x, 56, "BEFORE — turn 0 (seeded clusters): %d tiles" % _before_tiles, _before) + var panel2_x: float = MARGIN.x + MAP_W * CELL + 60 + _draw_panel(panel2_x, 56, "AFTER — turn %d (spread): %d tiles" % [TURNS, _after_tiles], _after) + + # Verdict banner. + var verdict_y: float = 56 + 28 + MAP_H * CELL + 36 + var grew: bool = _after_tiles > _before_tiles + draw_string( + font, Vector2(MARGIN.x, verdict_y), + "VERDICT: %s — populated tiles %d → %d (%+d). The world is alive and evolving each turn." + % ["PASS" if grew else "FAIL", _before_tiles, _after_tiles, _after_tiles - _before_tiles], + HORIZONTAL_ALIGNMENT_LEFT, -1, 16, + Color(0.4, 0.9, 0.4) if grew else Color(0.9, 0.4, 0.4) + ) + draw_string( + font, Vector2(MARGIN.x, verdict_y + 26), + "Colour = total fauna population on tile (dark green → bright yellow). " + + "Grey grid = land tiles with no fauna.", + HORIZONTAL_ALIGNMENT_LEFT, -1, 13, Color(0.75, 0.75, 0.75) + ) + + +func _draw_panel(ox: float, oy: float, label: String, pops: Dictionary) -> void: + var font: Font = ThemeDB.fallback_font + draw_string(font, Vector2(ox, oy - 6), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 15, Color.WHITE) + for row: int in range(MAP_H): + for col: int in range(MAP_W): + var x: float = ox + col * CELL + var y: float = oy + row * CELL + var key: Vector2i = Vector2i(col, row) + var color: Color = Color(0.18, 0.18, 0.18) # empty land + if pops.has(key): + var frac: float = clampf(float(pops[key]) / _peak_pop, 0.0, 1.0) + # dark green → bright yellow ramp + color = Color(0.15 + frac * 0.80, 0.35 + frac * 0.55, 0.10) + draw_rect(Rect2(x, y, CELL - 2, CELL - 2), color) + + +func _capture_and_quit() -> void: + if _captured: + return + _captured = true + DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(OUTPUT_DIR)) + var image: Image = get_viewport().get_texture().get_image() + if image == null: + push_error("WorldsimEcologyProof: failed to get viewport image") + get_tree().quit(1) + return + var ts: String = Time.get_datetime_string_from_system().replace(":", "-").replace("T", "_") + var abs_path: String = ProjectSettings.globalize_path( + "%s/worldsim_ecology_proof_%s.png" % [OUTPUT_DIR, ts] + ) + var err: Error = image.save_png(abs_path) + if err == OK: + print("SCREENSHOT_PATH:%s" % abs_path) + print("Screenshot: %dx%d saved" % [image.get_width(), image.get_height()]) + else: + push_error("WorldsimEcologyProof: save failed: %s" % error_string(err)) + get_tree().quit() diff --git a/src/game/engine/scenes/tests/worldsim_ecology_proof.gd.uid b/src/game/engine/scenes/tests/worldsim_ecology_proof.gd.uid new file mode 100644 index 00000000..5457fb63 --- /dev/null +++ b/src/game/engine/scenes/tests/worldsim_ecology_proof.gd.uid @@ -0,0 +1 @@ +uid://bf47tl87kj58y diff --git a/src/game/engine/scenes/tests/worldsim_ecology_proof.tscn b/src/game/engine/scenes/tests/worldsim_ecology_proof.tscn new file mode 100644 index 00000000..bbd91442 --- /dev/null +++ b/src/game/engine/scenes/tests/worldsim_ecology_proof.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://c7w0rld5imec0pr0"] + +[ext_resource type="Script" path="res://engine/scenes/tests/worldsim_ecology_proof.gd" id="1_script"] + +[node name="WorldsimEcologyProof" type="Node2D"] +script = ExtResource("1_script")