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")