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:
parent
b0069856f8
commit
f5cf076f15
3 changed files with 204 additions and 0 deletions
197
src/game/engine/scenes/tests/worldsim_ecology_proof.gd
Normal file
197
src/game/engine/scenes/tests/worldsim_ecology_proof.gd
Normal 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()
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://bf47tl87kj58y
|
||||
6
src/game/engine/scenes/tests/worldsim_ecology_proof.tscn
Normal file
6
src/game/engine/scenes/tests/worldsim_ecology_proof.tscn
Normal 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")
|
||||
Loading…
Add table
Reference in a new issue