feat(scenes): Add ecological simulation test scene with GDScript logic, unique identifier, and TSCN structure

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-06-06 17:24:49 -07:00
parent b0069856f8
commit f5cf076f15
3 changed files with 204 additions and 0 deletions

View file

@ -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_<ts>.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()

View file

@ -0,0 +1 @@
uid://bf47tl87kj58y

View file

@ -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")