mud/tests/test_terrain.py
Jared Miller 0d0c142993
Add seed-based terrain world with movement and viewport
1000x1000 tile world generated deterministically from a seed using
layered Perlin noise. Terrain derived from elevation: mountains,
forests, grasslands, sand, water, with rivers traced downhill from
peaks. ANSI-colored viewport centered on player.

Command system with registry/dispatch, 8-direction movement (n/s/e/w
+ diagonals), look/l, quit/q. Players see arrival/departure messages.

Set connect_maxwait=0.5 on telnetlib3 to avoid the 4s CHARSET
negotiation timeout — MUD clients reject CHARSET immediately via MTTS.
2026-02-07 13:27:44 -05:00

188 lines
5.5 KiB
Python

import pytest
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)
# should raise for out of bounds
with pytest.raises((IndexError, ValueError)):
world.get_tile(100, 0)
with pytest.raises((IndexError, ValueError)):
world.get_tile(0, 50)
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_handles_edges():
"""Viewport handles world boundaries correctly."""
world = World(seed=42, width=100, height=100)
# near top-left corner
viewport = world.get_viewport(2, 2, width=5, height=5)
assert len(viewport) == 5
assert len(viewport[0]) == 5
# near bottom-right corner
viewport = world.get_viewport(97, 97, width=5, height=5)
assert len(viewport) == 5
assert len(viewport[0]) == 5
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_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", ":"}