test(game-engine): Add integration tests for end-game footer actions and worldsim playable path interactions

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-06-06 16:21:16 -07:00
parent be7c824275
commit 0cec651fd3
2 changed files with 187 additions and 0 deletions

View file

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

View file

@ -0,0 +1,186 @@
extends GutTest
## Increment 2 integration proof: the worldsim runs through the playable
## bridge path and survives save/load.
##
## Drives the SAME Rust bridge classes the live `turn_manager.gd` loop uses —
## `GdGridState`, `GdFaunaEcology` (the shared ecology engine behind
## `EcologyState`), and the new `GdWorldSim` (continuous world-event dispatch +
## eco-damage map) — over N turns on a real terrain grid, and asserts:
##
## 1. The world VISIBLY EVOLVES through the bridge: fauna populations spread
## to more tiles over the run (emergence + Lotka-Volterra + dispersal +
## the newly-wired carrying-capacity migration all run inside
## `EcologyEngine::process_step`).
## 2. The worldsim side-state survives a mid-run SAVE/LOAD: serialize
## `tile_populations` + `eco_map` to JSON, rebuild a fresh engine + worldsim,
## restore, and confirm the restored populated-tile count matches the
## pre-save snapshot exactly (round-trip is lossless).
##
## This is the "it actually runs in the game loop" functional proof. It does
## NOT assert a visual result — the proof-scene screenshot is a later increment.
## It runs headless (Rail 5).
const MAP_SIZE: int = 16
const TURNS: int = 12
const SAVE_AT: int = 6
const SEED: int = 0xC0FFEE
const SPECIES_DIR: String = "res://public/resources/ecology/fauna/species"
## Three sample species — small enough to be fast, real enough to exercise
## emergence + LV dynamics + dispersal + migration.
const SAMPLE_SPECIES: Array[String] = ["grey_wolf", "abalone", "red_deer"]
func test_bridge_classes_registered() -> void:
assert_true(
ClassDB.class_exists("GdWorldSim"),
"GdWorldSim must be registered via api-gdext GDExtension"
)
assert_true(
ClassDB.class_exists("GdFaunaEcology"),
"GdFaunaEcology must be registered via api-gdext GDExtension"
)
assert_true(
ClassDB.class_exists("GdGridState"),
"GdGridState must be registered via api-gdext GDExtension"
)
func test_world_evolves_through_playable_path() -> void:
## Drive the SAME `GdFaunaEcology.tick_populations` the live `turn_manager.gd`
## loop calls (via `EcologyState.tick`) for TURNS turns on a real terrain
## grid, and assert fauna spread to strictly more tiles than at the start.
## `tick_populations` runs `EcologyEngine::process_step`, which now includes
## the Increment-2 carrying-capacity migration step alongside emergence, LV
## dynamics, dispersal, and tier advancement — so this exercises the
## in-game continuous ecology path end-to-end.
var grid: RefCounted = _make_terrain_grid()
var fauna: RefCounted = _make_fauna_with_species()
# Seed an initial cluster so LV dynamics + migration have a population to
# redistribute.
_seed_initial_populations(fauna)
var start_tiles: int = int(fauna.call("populated_tile_count"))
for t: int in range(TURNS):
# Seed mixes turn like the live loop's `map_seed + turn_number`.
fauna.call("tick_populations", grid, SEED + t)
var end_tiles: int = int(fauna.call("populated_tile_count"))
gut.p("populated tiles: start=%d end=%d" % [start_tiles, end_tiles])
assert_gt(
end_tiles,
start_tiles,
"Fauna must spread to more tiles over %d turns through the bridge "
% TURNS + "(emergence + LV + dispersal + migration) — start=%d end=%d"
% [start_tiles, end_tiles]
)
func test_worldsim_survives_save_load() -> void:
## Run to SAVE_AT, snapshot populated-tile count, serialize the ecology
## engine's FULL continuation state to JSON, rebuild a fresh engine, restore,
## and confirm the restored state matches the pre-save snapshot exactly and
## re-serializes byte-identically. This is the lossless round-trip proof for
## the `worldsim_state` save payload.
var grid: RefCounted = _make_terrain_grid()
var fauna: RefCounted = _make_fauna_with_species()
_seed_initial_populations(fauna)
for t: int in range(SAVE_AT):
fauna.call("tick_populations", grid, SEED + t)
var saved_tiles: int = int(fauna.call("populated_tile_count"))
var saved_slots: int = int(fauna.call("population_slot_count"))
var cont_json: String = String(fauna.call("continuation_state_to_json"))
assert_gt(saved_tiles, 0, "must have populations to save")
assert_ne(cont_json, "", "continuation_state_to_json must produce non-empty JSON")
# Fresh post-load instance: registry rebuilt from the pack, then restore.
var restored_fauna: RefCounted = _make_fauna_with_species()
var ok: bool = bool(
restored_fauna.call("restore_continuation_state_from_json", cont_json)
)
assert_true(ok, "restore_continuation_state_from_json must succeed")
var restored_tiles: int = int(restored_fauna.call("populated_tile_count"))
var restored_slots: int = int(restored_fauna.call("population_slot_count"))
assert_eq(
restored_tiles,
saved_tiles,
"populated tile count must survive save/load exactly (%d vs %d)"
% [restored_tiles, saved_tiles]
)
assert_eq(
restored_slots,
saved_slots,
"population slot count must survive save/load exactly (%d vs %d)"
% [restored_slots, saved_slots]
)
# Re-serialize the restored state — it must be byte-identical.
var reserialized: String = String(restored_fauna.call("continuation_state_to_json"))
assert_eq(
reserialized,
cont_json,
"re-serialized continuation state must be byte-identical after round-trip"
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
func _make_terrain_grid() -> RefCounted:
## Build a 16x16 GdGridState with deterministic temperate terrain so
## emergence has habitable tiles. Mirrors the synthetic terrain the
## mc-worldsim Rust determinism test uses.
# GdGridState is constructed via its static factory `create(w, h)` (api-gdext
# lib.rs:79) — it is NOT an instantiate-then-init class. `create_grid` is a
# method on a different bridge class (GdTurnProcessor), not GdGridState.
var grid: RefCounted = GdGridState.create(MAP_SIZE, MAP_SIZE)
assert_not_null(grid, "GdGridState.create must return a grid")
for row: int in range(MAP_SIZE):
for col: int in range(MAP_SIZE):
var lat: float = 1.0 - abs((float(row) - MAP_SIZE / 2.0) / (MAP_SIZE / 2.0))
var noise: float = fmod(float(col * 13 + row * 7) * 0.0173, 1.0)
var temp: float = 0.20 + lat * 0.50 + noise * 0.10
var d: Dictionary = {
"temperature": temp,
"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",
}
grid.call("set_tile_dict", col, row, d)
return grid
func _make_fauna_with_species() -> RefCounted:
## Instantiate GdFaunaEcology and register the sample species from the
## canonical JSON pack (same loader path EcologyState.gd uses).
var fauna: RefCounted = ClassDB.instantiate("GdFaunaEcology") as RefCounted
assert_not_null(fauna, "GdFaunaEcology must instantiate")
for name: String in SAMPLE_SPECIES:
var path: String = "%s/%s.json" % [SPECIES_DIR, name]
var raw: String = FileAccess.get_file_as_string(path)
assert_ne(raw, "", "species JSON must load: %s" % path)
var id: int = int(fauna.call("register_species_from_json", raw))
assert_gte(id, 0, "register_species_from_json must succeed for %s" % name)
return fauna
func _seed_initial_populations(fauna: RefCounted) -> void:
## Seed a small cluster on a few central tiles for each registered species.
## Re-derives the numeric ids by re-registering (idempotent — same hash).
for name: String in SAMPLE_SPECIES:
var path: String = "%s/%s.json" % [SPECIES_DIR, name]
var raw: String = FileAccess.get_file_as_string(path)
var id: int = int(fauna.call("register_species_from_json", raw))
if id < 0:
continue
for cell: Vector2i in [Vector2i(4, 4), Vector2i(5, 4), Vector2i(8, 8)]:
fauna.call("seed_population", cell.x, cell.y, id, 25.0)