mud/tests/test_terrain.py

243 lines
7.9 KiB
Python

from mudlib.world.terrain import World
def test_world_deterministic_from_seed():
"""Same seed produces identical terrain."""
world1 = World(seed=42, width=100, height=100)
world2 = World(seed=42, width=100, height=100)
for y in range(100):
for x in range(100):
assert world1.get_tile(x, y) == world2.get_tile(x, y)
def test_world_different_seeds_produce_different_terrain():
"""Different seeds produce different terrain."""
world1 = World(seed=42, width=100, height=100)
world2 = World(seed=99, width=100, height=100)
different_tiles = 0
for y in range(100):
for x in range(100):
if world1.get_tile(x, y) != world2.get_tile(x, y):
different_tiles += 1
# at least 10% should be different
assert different_tiles > 1000
def test_world_dimensions():
"""World has correct dimensions."""
world = World(seed=42, width=100, height=50)
# should not raise for valid coordinates
world.get_tile(0, 0)
world.get_tile(99, 49)
# coordinates wrap instead of raising
assert world.get_tile(100, 0) == world.get_tile(0, 0)
assert world.get_tile(0, 50) == world.get_tile(0, 0)
assert world.get_tile(-1, 0) == world.get_tile(99, 0)
assert world.get_tile(0, -1) == world.get_tile(0, 49)
def test_get_tile_returns_valid_terrain():
"""get_tile returns valid terrain characters."""
world = World(seed=42, width=100, height=100)
valid_terrain = {".", "^", "~", "T", ":"}
for y in range(100):
for x in range(100):
tile = world.get_tile(x, y)
assert tile in valid_terrain
def test_is_passable_mountains_impassable():
"""Mountains are impassable."""
world = World(seed=42, width=100, height=100)
for y in range(100):
for x in range(100):
tile = world.get_tile(x, y)
if tile == "^":
assert not world.is_passable(x, y)
def test_is_passable_water_impassable():
"""Water is impassable."""
world = World(seed=42, width=100, height=100)
for y in range(100):
for x in range(100):
tile = world.get_tile(x, y)
if tile == "~":
assert not world.is_passable(x, y)
def test_is_passable_grass_passable():
"""Grass is passable."""
world = World(seed=42, width=100, height=100)
for y in range(100):
for x in range(100):
tile = world.get_tile(x, y)
if tile == ".":
assert world.is_passable(x, y)
def test_is_passable_forest_passable():
"""Forest is passable."""
world = World(seed=42, width=100, height=100)
for y in range(100):
for x in range(100):
tile = world.get_tile(x, y)
if tile == "T":
assert world.is_passable(x, y)
def test_is_passable_sand_passable():
"""Sand is passable."""
world = World(seed=42, width=100, height=100)
for y in range(100):
for x in range(100):
tile = world.get_tile(x, y)
if tile == ":":
assert world.is_passable(x, y)
def test_get_viewport_dimensions():
"""get_viewport returns correct dimensions."""
world = World(seed=42, width=100, height=100)
viewport = world.get_viewport(50, 50, width=10, height=8)
assert len(viewport) == 8
assert len(viewport[0]) == 10
def test_get_viewport_centers_correctly():
"""get_viewport centers on given coordinates."""
world = World(seed=42, width=100, height=100)
viewport = world.get_viewport(50, 50, width=5, height=5)
# center tile should be at position (2, 2) in viewport
center_tile = viewport[2][2]
assert center_tile == world.get_tile(50, 50)
def test_get_viewport_wraps_at_edges():
"""Viewport wraps around world boundaries."""
world = World(seed=42, width=100, height=100)
# near top-left corner - viewport should wrap to bottom/right
viewport = world.get_viewport(0, 0, width=5, height=5)
assert len(viewport) == 5
assert len(viewport[0]) == 5
# top-left of viewport wraps to bottom-right of world
assert viewport[0][0] == world.get_tile(98, 98)
# near bottom-right corner - viewport should wrap to top/left
viewport = world.get_viewport(99, 99, width=5, height=5)
assert len(viewport) == 5
assert len(viewport[0]) == 5
# bottom-right of viewport wraps to top-left of world
assert viewport[4][4] == world.get_tile(1, 1)
def test_terrain_distribution_reasonable():
"""Terrain has reasonable distribution (not all one type)."""
world = World(seed=42, width=100, height=100)
terrain_counts = {".": 0, "^": 0, "~": 0, "T": 0, ":": 0}
for y in range(100):
for x in range(100):
tile = world.get_tile(x, y)
terrain_counts[tile] += 1
# should have at least 3 different terrain types
types_present = sum(1 for count in terrain_counts.values() if count > 0)
assert types_present >= 3
# no single type should dominate completely (> 95%)
total = sum(terrain_counts.values())
for terrain_type, count in terrain_counts.items():
assert count < 0.95 * total, f"{terrain_type} dominates with {count}/{total}"
def test_rivers_exist():
"""Terrain has some water tiles (rivers/lakes)."""
world = World(seed=42, width=100, height=100)
water_count = 0
for y in range(100):
for x in range(100):
if world.get_tile(x, y) == "~":
water_count += 1
# should have at least some water
assert water_count > 0
def test_terrain_tiles_seamlessly():
"""Terrain at opposite edges matches for seamless wrapping."""
world = World(seed=42, width=100, height=100)
# check that a strip along the right edge is continuous with the left edge
# by verifying the elevation-derived terrain doesn't have a hard seam.
# the noise is tileable, so adjacent tiles across the boundary should be
# from the same smooth noise field (not identical, but not a cliff).
# we verify the wrap helper itself works correctly:
for y in range(100):
assert world.get_tile(-1, y) == world.get_tile(99, y)
assert world.get_tile(100, y) == world.get_tile(0, y)
for x in range(100):
assert world.get_tile(x, -1) == world.get_tile(x, 99)
assert world.get_tile(x, 100) == world.get_tile(x, 0)
def test_large_world_generation():
"""Can generate a 1000x1000 world without errors."""
world = World(seed=42, width=1000, height=1000)
# spot check some tiles
assert world.get_tile(0, 0) in {".", "^", "~", "T", ":"}
assert world.get_tile(500, 500) in {".", "^", "~", "T", ":"}
assert world.get_tile(999, 999) in {".", "^", "~", "T", ":"}
# --- terrain caching ---
def test_world_saves_cache(tmp_path):
"""World saves terrain cache when cache_dir is provided."""
world = World(seed=42, width=100, height=100, cache_dir=tmp_path)
cache_file = tmp_path / "terrain_42_100x100.bin"
assert cache_file.exists()
assert cache_file.stat().st_size == 100 * 100 # 1 byte per tile
assert not world.cached # first run generates, doesn't load from cache
def test_world_loads_from_cache(tmp_path):
"""World loads terrain from cache on second creation."""
world1 = World(seed=42, width=100, height=100, cache_dir=tmp_path)
world2 = World(seed=42, width=100, height=100, cache_dir=tmp_path)
for y in range(100):
for x in range(100):
assert world1.get_tile(x, y) == world2.get_tile(x, y)
assert world2.cached
def test_world_cache_different_params(tmp_path):
"""Different seed/size doesn't use wrong cache."""
World(seed=42, width=100, height=100, cache_dir=tmp_path)
world2 = World(seed=99, width=100, height=100, cache_dir=tmp_path)
assert not world2.cached # different seed, must generate
def test_world_no_cache_by_default():
"""World doesn't cache when no cache_dir provided."""
world = World(seed=42, width=50, height=50)
assert not world.cached