Mi2dRPGamEng/tests/test_terrain_worldgen.py

206 lines
7.5 KiB
Python

from pathlib import Path
import numpy as np
import pytest
from engine.defs import DefRegistry
from engine.entity import Entity, EntityPosition
from engine.worldgen.terrain import (
CAVE_MIN_HEIGHT_DIFF,
MAX_HEIGHT,
SAND_Z,
STONE_MIN_Z,
WATER_MAX_Z,
build_terrain_map,
generate_terrain_map,
terrain_for,
)
from engine.world import World
DEFS_DIR = Path(__file__).resolve().parent.parent / "resources" / "defs"
@pytest.fixture()
def registry():
return DefRegistry.load(DEFS_DIR)
class _FixedRng:
"""A random.Random look-alike that always returns a fixed value from random()."""
def __init__(self, value: float):
self.value = value
def random(self) -> float:
return self.value
# --- terrain_for classification -----------------------------------------------------------------
def test_low_elevations_are_water():
for z in range(WATER_MAX_Z + 1):
assert terrain_for(z, moisture=0.5) == "water"
def test_shoreline_band_is_sand():
assert terrain_for(SAND_Z, moisture=0.9) == "sand"
assert terrain_for(SAND_Z, moisture=0.1) == "sand"
def test_high_elevations_are_stone():
for z in range(STONE_MIN_Z, MAX_HEIGHT):
assert terrain_for(z, moisture=0.1) == "stone"
def test_mid_elevations_are_dirt_or_mud_by_moisture():
mid_z = (SAND_Z + STONE_MIN_Z) // 2
assert terrain_for(mid_z, moisture=0.9) == "mud"
assert terrain_for(mid_z, moisture=0.1) == "dirt"
# --- build_terrain_map: surface + underground fill ----------------------------------------------
def test_surface_tile_has_the_classified_terrain_and_an_empty_roof(registry):
heights = np.array([[3]])
moisture = np.array([[0.1]])
m = build_terrain_map("t", heights, moisture, rng=_FixedRng(1.0))
tile = m.get_tile(0, 0, 3)
assert tile.layer_id("flooring") == "dirt"
assert tile.roof is None
assert tile.is_walkable(registry) is True
def test_underground_is_solid_unwalkable_rock(registry):
heights = np.array([[4]])
moisture = np.array([[0.1]])
m = build_terrain_map("t", heights, moisture, rng=_FixedRng(1.0))
for z in range(4):
tile = m.get_tile(0, 0, z)
assert tile.layer_id("room") == "solid_rock"
assert tile.is_walkable(registry) is False
def test_a_flat_map_has_no_underground_fill_at_the_minimum_height():
heights = np.array([[0, 0]])
moisture = np.array([[0.1, 0.1]])
m = build_terrain_map("t", heights, moisture, rng=_FixedRng(1.0))
# nothing below z=0 to fill - just confirm this doesn't error and the surface is water
assert m.get_tile(0, 0, 0).layer_id("flooring") == "water"
def test_a_water_column_is_swimmable_all_the_way_down_not_solid_rock(registry):
heights = np.array([[1]]) # a height-1 water column has one tile beneath its surface
moisture = np.array([[0.1]])
m = build_terrain_map("t", heights, moisture, rng=_FixedRng(1.0))
assert m.get_tile(0, 0, 0).layer_id("flooring") == "water"
assert m.get_tile(0, 0, 0).is_walkable(registry) is True
assert m.get_tile(0, 0, 1).layer_id("flooring") == "water"
# --- slope connectors: req 3, actually walkable via World.try_move -------------------------------
def test_one_level_boundary_gets_a_walkable_slope_connector_both_ways(registry):
heights = np.array([[2, 3]])
moisture = np.array([[0.1, 0.1]])
m = build_terrain_map("t", heights, moisture, rng=_FixedRng(1.0))
world = World(registry=registry)
world.maps["t"] = m
walker = Entity(position=EntityPosition("t", 0, 0, 2))
assert world.try_move(walker, 1, 0, 0) is True
assert (walker.position.x, walker.position.y, walker.position.z) == (1, 0, 3)
assert world.try_move(walker, -1, 0, 0) is True
assert (walker.position.x, walker.position.y, walker.position.z) == (0, 0, 2)
def test_slope_connector_tiles_are_not_walled(registry):
heights = np.array([[2, 3]])
moisture = np.array([[0.1, 0.1]])
m = build_terrain_map("t", heights, moisture, rng=_FixedRng(1.0))
assert m.get_tile(1, 0, 2).is_walkable(registry) is True # ascend connector
assert m.get_tile(0, 0, 3).is_walkable(registry) is True # descend connector
def test_no_connector_between_columns_at_the_same_height():
heights = np.array([[3, 3]])
moisture = np.array([[0.1, 0.1]])
m = build_terrain_map("t", heights, moisture, rng=_FixedRng(1.0))
# neither column got an extra tile above/below its own surface
assert m.get_tile(0, 0, 4).flooring is None
assert m.get_tile(1, 0, 2).flooring is None
# --- cave pockets: req 4 -------------------------------------------------------------------------
def test_steep_boundary_carves_a_cave_pocket_when_the_roll_succeeds(registry):
heights = np.array([[1, 3]]) # diff == 2, qualifies
moisture = np.array([[0.1, 0.1]])
m = build_terrain_map("t", heights, moisture, rng=_FixedRng(0.0)) # random() always < CAVE_CHANCE
pocket = m.get_tile(1, 0, 1) # higher column (x=1), at the lower column's height
assert pocket.room is None # unwalled
assert pocket.roof is not None # roofed
assert pocket.is_walkable(registry) is True
def test_steep_boundary_stays_solid_rock_when_the_roll_fails(registry):
heights = np.array([[1, 3]])
moisture = np.array([[0.1, 0.1]])
m = build_terrain_map("t", heights, moisture, rng=_FixedRng(1.0)) # random() never < CAVE_CHANCE
pocket = m.get_tile(1, 0, 1)
assert pocket.layer_id("room") == "solid_rock"
assert pocket.is_walkable(registry) is False
def test_cave_min_height_diff_constant_is_2():
assert CAVE_MIN_HEIGHT_DIFF == 2
def test_walking_off_a_steep_uncave_cliff_falls_instead_of_floating(registry):
# diff == 3, no slope connector applies (only diff==1 gets one) and the roll is forced to
# fail (no cave carved) - previously this left bare, "walkable" empty air above the lower
# column (x=0's cells above its own height-1 surface are never touched by generation);
# gravity (engine/world.py::World._apply_gravity) should catch it and drop the walker down
# to the real ground instead of leaving them floating.
heights = np.array([[1, 4]])
moisture = np.array([[0.1, 0.1]])
m = build_terrain_map("t", heights, moisture, rng=_FixedRng(1.0))
world = World(registry=registry)
world.maps["t"] = m
walker = Entity(position=EntityPosition("t", 1, 0, 4)) # start on the high column's surface
assert world.try_move(walker, -1, 0, 0) is True # step toward the low column
assert (walker.position.x, walker.position.y, walker.position.z) == (0, 0, 1) # falls to its real surface
# --- generate_terrain_map: full pipeline smoke test ----------------------------------------------
def test_generate_terrain_map_produces_a_fully_resolvable_map(registry):
m = generate_terrain_map("generated_test", width=12, height=12, seed=1234)
assert m.width == 12
assert m.height == 12
assert m.depth == MAX_HEIGHT + 1
# every tile's layers must resolve against the real registry without raising, and every
# column's own surface must be walkable
for y in range(m.height):
for x in range(m.width):
for z in range(m.depth):
m.get_tile(x, y, z).is_walkable(registry) # no KeyError/crash
def test_generate_terrain_map_is_deterministic_for_the_same_seed():
a = generate_terrain_map("a", 10, 10, seed=55)
b = generate_terrain_map("b", 10, 10, seed=55)
for y in range(10):
for x in range(10):
for z in range(a.depth):
assert a.get_tile(x, y, z).layer_id("flooring") == b.get_tile(x, y, z).layer_id("flooring")