test(ecology): ✅ Add/fix test cases for golden vectors to validate edge cases and reference inputs in the ecology module
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
4158267121
commit
2ca21ca5a1
1 changed files with 562 additions and 0 deletions
562
engine/tests/unit/test_ecology_golden_vectors.gd
Normal file
562
engine/tests/unit/test_ecology_golden_vectors.gd
Normal file
|
|
@ -0,0 +1,562 @@
|
|||
extends GutTest
|
||||
## M2a ecology golden vector tests.
|
||||
## Seed 42, 20×20 map, snapshots at turns 0/10/50.
|
||||
## Verifies: flora growth, quality progression, predator-prey oscillation,
|
||||
## biome recomputation, habitat suitability.
|
||||
|
||||
const GameMapScript = preload("res://engine/src/map/game_map.gd")
|
||||
const TileScript = preload("res://engine/src/map/tile.gd")
|
||||
const HexUtilsScript = preload("res://engine/src/map/hex_utils.gd")
|
||||
const EcosystemScript = preload("res://engine/src/modules/ecology/ecosystem.gd")
|
||||
const EcologyDBScript = preload("res://engine/src/modules/ecology/ecology_db.gd")
|
||||
const BiomeModelScript = preload("res://engine/src/models/world/biome_model.gd")
|
||||
const LandFaunaScript = preload("res://engine/src/models/world/land_fauna.gd")
|
||||
const MarineFaunaScript = preload("res://engine/src/models/world/marine_fauna.gd")
|
||||
|
||||
const MAP_W: int = 20
|
||||
const MAP_H: int = 20
|
||||
const SEED: int = 42
|
||||
|
||||
var game_map: RefCounted # GameMap
|
||||
var ecology_db: RefCounted # EcologyDB
|
||||
var ecosystem: RefCounted # EcosystemOrchestrator
|
||||
|
||||
# Snapshot storage
|
||||
var snap0: Dictionary = {} # "col,row" -> Dictionary
|
||||
var snap10: Dictionary = {}
|
||||
var snap50: Dictionary = {}
|
||||
|
||||
|
||||
func _tile_hash(seed: int, col: int, row: int) -> float:
|
||||
## Deterministic per-tile hash matching the TypeScript test.
|
||||
var h: int = seed * 374761393 + col * 668265263 + row * 2147483647
|
||||
h = (h ^ (h >> 13)) * 1274126177
|
||||
h = h ^ (h >> 16)
|
||||
return float(h & 0x7FFFFFFF) / float(0x7FFFFFFF)
|
||||
|
||||
|
||||
func _build_test_grid() -> void:
|
||||
game_map = GameMapScript.new()
|
||||
# Use WrapMode.NONE (0) for hard-edge test map — no wrapping.
|
||||
game_map.initialize(MAP_W, MAP_H, 0)
|
||||
|
||||
for row in range(MAP_H):
|
||||
for col in range(MAP_W):
|
||||
var tile: Resource = TileScript.new()
|
||||
var axial: Vector2i = HexUtilsScript.offset_to_axial(Vector2i(col, row))
|
||||
tile.position = axial
|
||||
|
||||
var h: float = _tile_hash(SEED, col, row)
|
||||
var h2: float = _tile_hash(SEED + 1, col, row)
|
||||
var h3: float = _tile_hash(SEED + 2, col, row)
|
||||
|
||||
# Temperature by latitude + noise
|
||||
var lat_factor: float = 1.0 - absf(float(row) - float(MAP_H) / 2.0) / (float(MAP_H) / 2.0)
|
||||
tile.temperature = clampf(lat_factor * 0.8 + h * 0.2, 0.0, 1.0)
|
||||
tile.moisture = clampf(h2 * 0.7 + 0.15, 0.0, 1.0)
|
||||
|
||||
# Elevation: edges are water
|
||||
var edge_dist: int = mini(mini(col, row), mini(MAP_W - 1 - col, MAP_H - 1 - row))
|
||||
tile.elevation = 0.1 if edge_dist <= 1 else 0.3 + h3 * 0.4
|
||||
|
||||
var is_water_tile: bool = edge_dist <= 1
|
||||
tile.substrate_id = "deep_water" if edge_dist == 0 else ("shallow_water" if edge_dist == 1 else ("highland" if tile.elevation > 0.6 else "lowland"))
|
||||
tile.biome_id = "ocean" if is_water_tile else "grassland"
|
||||
tile.quality = 2
|
||||
tile.wind_direction = int(h * 6.0)
|
||||
tile.wind_speed = 0.5
|
||||
|
||||
if is_water_tile:
|
||||
tile.water_body_id = 0
|
||||
tile.depth_from_coast = edge_dist
|
||||
tile.reef_health = 1.0
|
||||
else:
|
||||
tile.water_body_id = -1
|
||||
tile.depth_from_coast = -1
|
||||
|
||||
game_map.set_tile(axial, tile)
|
||||
|
||||
|
||||
func _inject_biome_data() -> void:
|
||||
## Inject proof biome definitions matching the TS BIOME_DEFS directly
|
||||
## into DataLoader's internal _biomes dictionary.
|
||||
var biomes: Array = [
|
||||
{
|
||||
"id": "temperate_forest",
|
||||
"name": "Temperate Forest",
|
||||
"quality_range": [1, 5],
|
||||
"temp_range": [0.35, 0.65],
|
||||
"moisture_range": [0.4, 0.8],
|
||||
"flora_climax": {"canopy": 0.9, "undergrowth": 0.7, "fungi": 0.5},
|
||||
"fauna_capacity": 12,
|
||||
},
|
||||
{
|
||||
"id": "tropical_rainforest",
|
||||
"name": "Tropical Rainforest",
|
||||
"quality_range": [1, 5],
|
||||
"temp_range": [0.65, 1.0],
|
||||
"moisture_range": [0.6, 1.0],
|
||||
"flora_climax": {"canopy": 1.0, "undergrowth": 0.9, "fungi": 0.8},
|
||||
"fauna_capacity": 16,
|
||||
},
|
||||
{
|
||||
"id": "grassland",
|
||||
"name": "Grassland",
|
||||
"quality_range": [1, 4],
|
||||
"temp_range": [0.3, 0.7],
|
||||
"moisture_range": [0.2, 0.5],
|
||||
"flora_climax": {"canopy": 0.1, "undergrowth": 0.8, "fungi": 0.2},
|
||||
"fauna_capacity": 8,
|
||||
},
|
||||
{
|
||||
"id": "desert",
|
||||
"name": "Desert",
|
||||
"quality_range": [1, 3],
|
||||
"temp_range": [0.5, 1.0],
|
||||
"moisture_range": [0.0, 0.2],
|
||||
"flora_climax": {"canopy": 0.0, "undergrowth": 0.1, "fungi": 0.0},
|
||||
"fauna_capacity": 3,
|
||||
},
|
||||
{
|
||||
"id": "boreal_forest",
|
||||
"name": "Boreal Forest",
|
||||
"quality_range": [1, 5],
|
||||
"temp_range": [0.15, 0.4],
|
||||
"moisture_range": [0.3, 0.7],
|
||||
"flora_climax": {"canopy": 0.7, "undergrowth": 0.4, "fungi": 0.6},
|
||||
"fauna_capacity": 8,
|
||||
},
|
||||
{
|
||||
"id": "tundra",
|
||||
"name": "Tundra",
|
||||
"quality_range": [1, 3],
|
||||
"temp_range": [0.0, 0.2],
|
||||
"moisture_range": [0.1, 0.5],
|
||||
"flora_climax": {"canopy": 0.0, "undergrowth": 0.2, "fungi": 0.1},
|
||||
"fauna_capacity": 4,
|
||||
},
|
||||
# Additional biomes the GDScript classifier produces but TS doesn't have:
|
||||
# These need at least stub definitions so DataLoader.get_biome() doesn't return null.
|
||||
{
|
||||
"id": "temperate_grassland",
|
||||
"name": "Temperate Grassland",
|
||||
"quality_range": [1, 4],
|
||||
"temp_range": [0.25, 0.55],
|
||||
"moisture_range": [0.3, 0.6],
|
||||
"flora_climax": {"canopy": 0.05, "undergrowth": 0.8, "fungi": 0.15},
|
||||
"fauna_capacity": 8,
|
||||
},
|
||||
{
|
||||
"id": "savanna",
|
||||
"name": "Savanna",
|
||||
"quality_range": [1, 4],
|
||||
"temp_range": [0.55, 1.0],
|
||||
"moisture_range": [0.2, 0.4],
|
||||
"flora_climax": {"canopy": 0.15, "undergrowth": 0.6, "fungi": 0.1},
|
||||
"fauna_capacity": 6,
|
||||
},
|
||||
{
|
||||
"id": "chaparral",
|
||||
"name": "Chaparral",
|
||||
"quality_range": [1, 3],
|
||||
"temp_range": [0.25, 0.55],
|
||||
"moisture_range": [0.1, 0.3],
|
||||
"flora_climax": {"canopy": 0.05, "undergrowth": 0.4, "fungi": 0.05},
|
||||
"fauna_capacity": 5,
|
||||
},
|
||||
{
|
||||
"id": "tropical_dry_forest",
|
||||
"name": "Tropical Dry Forest",
|
||||
"quality_range": [1, 5],
|
||||
"temp_range": [0.55, 1.0],
|
||||
"moisture_range": [0.4, 0.7],
|
||||
"flora_climax": {"canopy": 0.6, "undergrowth": 0.5, "fungi": 0.3},
|
||||
"fauna_capacity": 10,
|
||||
},
|
||||
{
|
||||
"id": "polar_desert",
|
||||
"name": "Polar Desert",
|
||||
"quality_range": [1, 2],
|
||||
"temp_range": [0.0, 0.1],
|
||||
"moisture_range": [0.0, 0.3],
|
||||
"flora_climax": {"canopy": 0.0, "undergrowth": 0.0, "fungi": 0.0},
|
||||
"fauna_capacity": 1,
|
||||
},
|
||||
{
|
||||
"id": "swamp",
|
||||
"name": "Swamp",
|
||||
"quality_range": [1, 4],
|
||||
"temp_range": [0.4, 1.0],
|
||||
"moisture_range": [0.7, 1.0],
|
||||
"flora_climax": {"canopy": 0.4, "undergrowth": 0.8, "fungi": 0.6},
|
||||
"fauna_capacity": 10,
|
||||
},
|
||||
{
|
||||
"id": "ocean",
|
||||
"name": "Ocean",
|
||||
"quality_range": [1, 3],
|
||||
"temp_range": [0.0, 1.0],
|
||||
"moisture_range": [0.0, 1.0],
|
||||
"flora_climax": {"canopy": 0.0, "undergrowth": 0.0, "fungi": 0.0},
|
||||
"fauna_capacity": 0,
|
||||
},
|
||||
]
|
||||
|
||||
for biome_data: Dictionary in biomes:
|
||||
var biome: RefCounted = BiomeModelScript.from_dict(biome_data)
|
||||
DataLoader._biomes[biome.id] = biome
|
||||
|
||||
# Inject vegetation params (matches TS VEG/SUC/DES defaults)
|
||||
DataLoader._raw["world_flora"] = {
|
||||
"vegetation": {
|
||||
"growth_rate": 0.02,
|
||||
"decay_rate": 0.03,
|
||||
"shade_cap": 0.7,
|
||||
"drought_decay_multiplier": 1.5,
|
||||
"fungi_undergrowth_threshold": 0.3,
|
||||
"fungi_regrowth_bonus_cap": 2.0,
|
||||
},
|
||||
"succession": {
|
||||
"stability_turns": 50,
|
||||
"canopy_threshold": 0.8,
|
||||
"regrowth_stages": [
|
||||
{"stage": 0, "turns_to_advance": 10, "canopy_target": 0.0, "undergrowth_target": 0.1, "fungi_target": 0.0},
|
||||
{"stage": 1, "turns_to_advance": 15, "canopy_target": 0.1, "undergrowth_target": 0.3, "fungi_target": 0.05},
|
||||
{"stage": 2, "turns_to_advance": 20, "canopy_target": 0.4, "undergrowth_target": 0.5, "fungi_target": 0.2},
|
||||
{"stage": 3, "turns_to_advance": 25, "canopy_target": 0.7, "undergrowth_target": 0.6, "fungi_target": 0.4},
|
||||
],
|
||||
},
|
||||
"desertification": {
|
||||
"moisture_threshold": 0.2,
|
||||
"turns_required": 30,
|
||||
"decay_multiplier": 2.0,
|
||||
"recovery_rate": 1,
|
||||
},
|
||||
}
|
||||
|
||||
# Inject land fauna params
|
||||
var lf := LandFaunaScript.new()
|
||||
lf.undergrowth_weight = 0.6
|
||||
lf.canopy_weight = 0.2
|
||||
lf.fungi_weight = 0.2
|
||||
lf.habitat_abandon_threshold = 0.3
|
||||
lf.habitat_abandon_turns = 10
|
||||
lf.habitat_thriving_threshold = 0.7
|
||||
DataLoader._land_fauna = lf
|
||||
|
||||
|
||||
func _setup_ecology_db() -> void:
|
||||
ecology_db = EcologyDBScript.new()
|
||||
|
||||
# Create test species: a grass-like producer, a deer-like herbivore, a wolf-like predator
|
||||
var grass_id: int = ecology_db.add_species({
|
||||
"trait_hash": "grass_producer_42",
|
||||
"name": "Meadow Grass",
|
||||
"size": "tiny",
|
||||
"diet": "producer",
|
||||
"habitat": "terrestrial",
|
||||
"locomotion": "sessile",
|
||||
"reproduction": "spore",
|
||||
"thermal": "mesothermic",
|
||||
"social": "colonial",
|
||||
"min_quality": 1,
|
||||
"max_quality": 5,
|
||||
"growth_rate": 0.1,
|
||||
"carrying_capacity": 50,
|
||||
"migration_range": 0,
|
||||
"maturity_age": 1,
|
||||
"max_age": 100,
|
||||
"quality_up_threshold": 15,
|
||||
})
|
||||
|
||||
var deer_id: int = ecology_db.add_species({
|
||||
"trait_hash": "deer_herbivore_42",
|
||||
"name": "Forest Deer",
|
||||
"size": "medium",
|
||||
"diet": "herbivore",
|
||||
"habitat": "terrestrial",
|
||||
"locomotion": "quadruped",
|
||||
"reproduction": "viviparous",
|
||||
"thermal": "endothermic",
|
||||
"social": "herd",
|
||||
"min_quality": 1,
|
||||
"max_quality": 5,
|
||||
"growth_rate": 0.02,
|
||||
"carrying_capacity": 10,
|
||||
"migration_range": 3,
|
||||
"maturity_age": 5,
|
||||
"max_age": 40,
|
||||
"quality_up_threshold": 20,
|
||||
})
|
||||
|
||||
var wolf_id: int = ecology_db.add_species({
|
||||
"trait_hash": "wolf_predator_42",
|
||||
"name": "Grey Wolf",
|
||||
"size": "medium",
|
||||
"diet": "carnivore",
|
||||
"habitat": "terrestrial",
|
||||
"locomotion": "quadruped",
|
||||
"reproduction": "viviparous",
|
||||
"thermal": "endothermic",
|
||||
"social": "pack",
|
||||
"min_quality": 1,
|
||||
"max_quality": 5,
|
||||
"growth_rate": 0.01,
|
||||
"carrying_capacity": 6,
|
||||
"migration_range": 4,
|
||||
"maturity_age": 5,
|
||||
"max_age": 30,
|
||||
"quality_up_threshold": 20,
|
||||
})
|
||||
|
||||
# Set up food web: wolf eats deer
|
||||
ecology_db.add_food_web_edge(wolf_id, deer_id, 0.5)
|
||||
|
||||
# Place initial creatures on interior land tiles
|
||||
# 10 grass producers, 5 deer herbivores, 2 wolf predators
|
||||
var land_tiles: Array[Vector2i] = []
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var tile: Variant = game_map.tiles[axial]
|
||||
if tile.substrate_id not in ["deep_water", "shallow_water", "lake_bed"]:
|
||||
land_tiles.append(axial)
|
||||
|
||||
# Place grass on first 10 land tiles
|
||||
for i in range(mini(10, land_tiles.size())):
|
||||
var pos: Vector2i = land_tiles[i]
|
||||
var off: Vector2i = HexUtilsScript.axial_to_offset(pos)
|
||||
ecology_db.add_creature(off.x, off.y, grass_id, 2, 0, 1.0)
|
||||
|
||||
# Place deer on tiles 5-9
|
||||
for i in range(5, mini(10, land_tiles.size())):
|
||||
var pos: Vector2i = land_tiles[i]
|
||||
var off: Vector2i = HexUtilsScript.axial_to_offset(pos)
|
||||
ecology_db.add_creature(off.x, off.y, deer_id, 2, 0, 1.0)
|
||||
|
||||
# Place wolves on tiles 5-6
|
||||
for i in range(5, mini(7, land_tiles.size())):
|
||||
var pos: Vector2i = land_tiles[i]
|
||||
var off: Vector2i = HexUtilsScript.axial_to_offset(pos)
|
||||
ecology_db.add_creature(off.x, off.y, wolf_id, 2, 0, 1.0)
|
||||
|
||||
|
||||
func _snapshot(label: String) -> Dictionary:
|
||||
var snap: Dictionary = {}
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var tile: Variant = game_map.tiles[axial]
|
||||
var off: Vector2i = HexUtilsScript.axial_to_offset(axial)
|
||||
var key: String = "%d,%d" % [off.x, off.y]
|
||||
snap[key] = {
|
||||
"col": off.x,
|
||||
"row": off.y,
|
||||
"biome_id": tile.biome_id,
|
||||
"canopy_cover": tile.canopy_cover,
|
||||
"undergrowth": tile.undergrowth,
|
||||
"fungi_network": tile.fungi_network,
|
||||
"quality": tile.quality,
|
||||
"habitat_suitability": tile.habitat_suitability,
|
||||
"drought_counter": tile.drought_counter,
|
||||
}
|
||||
return snap
|
||||
|
||||
|
||||
func before_all() -> void:
|
||||
# Load terrain data for tile.is_water() to work
|
||||
DataLoader.load_theme("age-of-four")
|
||||
# Build test grid and inject ecology data
|
||||
_build_test_grid()
|
||||
_inject_biome_data()
|
||||
_setup_ecology_db()
|
||||
ecosystem = EcosystemScript.new()
|
||||
|
||||
# Turn 0 snapshot
|
||||
snap0 = _snapshot("turn0")
|
||||
|
||||
# Run 10 turns
|
||||
for t in range(10):
|
||||
ecosystem.process_turn(game_map, ecology_db, SEED + t)
|
||||
snap10 = _snapshot("turn10")
|
||||
|
||||
# Run 40 more turns (total 50)
|
||||
for t in range(10, 50):
|
||||
ecosystem.process_turn(game_map, ecology_db, SEED + t)
|
||||
snap50 = _snapshot("turn50")
|
||||
|
||||
|
||||
# -- Turn 0 --
|
||||
|
||||
func test_turn0_all_land_tiles_start_zero_flora() -> void:
|
||||
for key: String in snap0:
|
||||
var s: Dictionary = snap0[key]
|
||||
if s["biome_id"] == "ocean":
|
||||
continue
|
||||
assert_eq(s["canopy_cover"], 0.0, "canopy should be 0 at turn 0")
|
||||
assert_eq(s["undergrowth"], 0.0, "undergrowth should be 0 at turn 0")
|
||||
assert_eq(s["fungi_network"], 0.0, "fungi should be 0 at turn 0")
|
||||
|
||||
|
||||
func test_turn0_edge_tiles_are_water() -> void:
|
||||
var s: Dictionary = snap0["0,0"]
|
||||
assert_eq(s["biome_id"], "ocean", "corner tile should be ocean")
|
||||
|
||||
|
||||
func test_turn0_interior_tiles_have_valid_biome() -> void:
|
||||
var s: Dictionary = snap0["10,10"]
|
||||
assert_ne(s["biome_id"], "ocean", "center tile should not be ocean")
|
||||
assert_ne(s["biome_id"], "", "center tile should have a biome")
|
||||
|
||||
|
||||
# -- Turn 10 --
|
||||
|
||||
func test_turn10_undergrowth_growing() -> void:
|
||||
var total_ug: float = 0.0
|
||||
var count: int = 0
|
||||
for key: String in snap10:
|
||||
var s: Dictionary = snap10[key]
|
||||
if s["biome_id"] == "ocean":
|
||||
continue
|
||||
total_ug += s["undergrowth"]
|
||||
count += 1
|
||||
if count > 0:
|
||||
assert_gt(total_ug / float(count), 0.0, "avg undergrowth should be > 0 after 10 turns")
|
||||
|
||||
|
||||
func test_turn10_habitat_suitability_positive() -> void:
|
||||
var found: bool = false
|
||||
for key: String in snap10:
|
||||
var s: Dictionary = snap10[key]
|
||||
if s["biome_id"] != "ocean" and s["habitat_suitability"] > 0.0:
|
||||
found = true
|
||||
break
|
||||
assert_true(found, "at least one land tile should have positive habitat suitability")
|
||||
|
||||
|
||||
func test_turn10_quality_not_maxed() -> void:
|
||||
var s: Dictionary = snap10["10,10"]
|
||||
assert_lt(s["quality"], 5, "quality should not be maxed at turn 10")
|
||||
|
||||
|
||||
# -- Turn 50 --
|
||||
|
||||
func test_turn50_canopy_grown_on_land() -> void:
|
||||
var max_canopy: float = 0.0
|
||||
for key: String in snap50:
|
||||
var s: Dictionary = snap50[key]
|
||||
if s["biome_id"] == "ocean":
|
||||
continue
|
||||
max_canopy = maxf(max_canopy, s["canopy_cover"])
|
||||
assert_gt(max_canopy, 0.0, "some land tile should have canopy > 0 by turn 50")
|
||||
|
||||
|
||||
func test_turn50_undergrowth_developed() -> void:
|
||||
var total_ug: float = 0.0
|
||||
var land_count: int = 0
|
||||
for key: String in snap50:
|
||||
var s: Dictionary = snap50[key]
|
||||
if s["biome_id"] in ["ocean", "desert", "tundra", "polar_desert"]:
|
||||
continue
|
||||
total_ug += s["undergrowth"]
|
||||
land_count += 1
|
||||
if land_count > 0:
|
||||
assert_gt(total_ug / float(land_count), 0.05, "avg undergrowth > 0.05 by turn 50")
|
||||
|
||||
|
||||
func test_turn50_quality_differentiated() -> void:
|
||||
var qualities: Dictionary = {}
|
||||
for key: String in snap50:
|
||||
var s: Dictionary = snap50[key]
|
||||
if s["biome_id"] == "ocean":
|
||||
continue
|
||||
qualities[s["quality"]] = true
|
||||
assert_gte(qualities.size(), 2, "should have at least 2 distinct quality tiers by turn 50")
|
||||
|
||||
|
||||
func test_turn50_at_least_one_q3_or_higher() -> void:
|
||||
var max_q: int = 0
|
||||
for key: String in snap50:
|
||||
var s: Dictionary = snap50[key]
|
||||
if s["biome_id"] != "ocean":
|
||||
max_q = maxi(max_q, s["quality"])
|
||||
assert_gte(max_q, 3, "at least one tile should reach Q3+ by turn 50")
|
||||
|
||||
|
||||
func test_turn50_ecosystem_health_valid() -> void:
|
||||
assert_gt(ecosystem.global_health, 0.0, "global health should be > 0")
|
||||
assert_lte(ecosystem.global_health, 1.0, "global health should be <= 1")
|
||||
|
||||
|
||||
func test_turn50_desert_tiles_low_canopy() -> void:
|
||||
for key: String in snap50:
|
||||
var s: Dictionary = snap50[key]
|
||||
if s["biome_id"] == "desert":
|
||||
assert_lt(s["canopy_cover"], 0.1, "desert tiles should have very low canopy")
|
||||
|
||||
|
||||
# -- Creature lifecycle --
|
||||
|
||||
func test_turn50_predator_population_not_zero() -> void:
|
||||
## Wolf population should oscillate, not crash to zero (Lotka-Volterra).
|
||||
var wolf_sp: Dictionary = ecology_db.get_species_by_hash("wolf_predator_42")
|
||||
if wolf_sp.is_empty():
|
||||
pass_test("wolf species not found — skip (may be unregistered)")
|
||||
return
|
||||
var wolves: Array = ecology_db.get_creatures_by_species(wolf_sp.get("id", -1))
|
||||
# At least some wolves should survive 50 turns if prey exists
|
||||
var deer_sp: Dictionary = ecology_db.get_species_by_hash("deer_herbivore_42")
|
||||
var deer: Array = ecology_db.get_creatures_by_species(deer_sp.get("id", -1))
|
||||
# If deer survive, wolves should too (oscillation, not collapse)
|
||||
if deer.size() > 0:
|
||||
assert_gt(wolves.size(), 0, "wolf population should not crash to zero while prey exists")
|
||||
|
||||
|
||||
func test_turn50_herbivore_exists() -> void:
|
||||
## At least some deer should survive.
|
||||
var deer_sp: Dictionary = ecology_db.get_species_by_hash("deer_herbivore_42")
|
||||
if deer_sp.is_empty():
|
||||
pass_test("deer species not found — skip")
|
||||
return
|
||||
var deer: Array = ecology_db.get_creatures_by_species(deer_sp.get("id", -1))
|
||||
assert_gt(deer.size(), 0, "deer population should persist after 50 turns")
|
||||
|
||||
|
||||
func test_turn50_creature_quality_progression() -> void:
|
||||
## At least one creature should have progressed from Q2 to Q3 by turn 50.
|
||||
var all_ids: Array = []
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var off: Vector2i = HexUtilsScript.axial_to_offset(axial)
|
||||
for c: Dictionary in ecology_db.get_creatures_on_tile(off.x, off.y):
|
||||
all_ids.append(c.get("id", -1))
|
||||
|
||||
var found_q3: bool = false
|
||||
for cid: int in all_ids:
|
||||
var cr: Dictionary = ecology_db.get_creature(cid)
|
||||
if cr.get("quality", 1) >= 3:
|
||||
found_q3 = true
|
||||
break
|
||||
assert_true(found_q3, "at least one creature should reach Q3 by turn 50")
|
||||
|
||||
|
||||
# -- Flora-fauna interaction --
|
||||
|
||||
func test_turn50_habitat_correlates_with_flora() -> void:
|
||||
## Tiles with more flora should generally have higher habitat suitability.
|
||||
var high_flora: Array[float] = []
|
||||
var low_flora: Array[float] = []
|
||||
for key: String in snap50:
|
||||
var s: Dictionary = snap50[key]
|
||||
if s["biome_id"] == "ocean":
|
||||
continue
|
||||
var flora_sum: float = s["canopy_cover"] + s["undergrowth"] + s["fungi_network"]
|
||||
if flora_sum > 0.5:
|
||||
high_flora.append(s["habitat_suitability"])
|
||||
elif flora_sum < 0.1:
|
||||
low_flora.append(s["habitat_suitability"])
|
||||
if high_flora.size() > 0 and low_flora.size() > 0:
|
||||
var avg_high: float = 0.0
|
||||
for v: float in high_flora:
|
||||
avg_high += v
|
||||
avg_high /= float(high_flora.size())
|
||||
var avg_low: float = 0.0
|
||||
for v: float in low_flora:
|
||||
avg_low += v
|
||||
avg_low /= float(low_flora.size())
|
||||
assert_gt(avg_high, avg_low, "high-flora tiles should have higher habitat suitability")
|
||||
Loading…
Add table
Reference in a new issue