206 lines
7.5 KiB
Python
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")
|