"""Terrain generation for MUD worlds using deterministic noise.""" import math import random def _make_perm(seed: int) -> list[int]: """Generate permutation table from seed.""" rng = random.Random(seed) p = list(range(256)) rng.shuffle(p) return p + p def _noise(perm: list[int], x: float, y: float, px: int, py: int) -> float: """2D tileable Perlin noise. Wraps seamlessly with period px along x and py along y. """ # unit grid cell fx = math.floor(x) fy = math.floor(y) xi = int(fx) % px yi = int(fy) % py xi1 = (xi + 1) % px yi1 = (yi + 1) % py # relative coordinates within cell xf = x - fx yf = y - fy # fade curves (inlined) u = xf * xf * xf * (xf * (xf * 6 - 15) + 10) v = yf * yf * yf * (yf * (yf * 6 - 15) + 10) # hash coordinates of 4 corners (wrapping) aa = perm[perm[xi] + yi] ab = perm[perm[xi] + yi1] ba = perm[perm[xi1] + yi] bb = perm[perm[xi1] + yi1] # grad + lerp inlined for all 4 corners xf1 = xf - 1 yf1 = yf - 1 def _g(h: int, gx: float, gy: float) -> float: h &= 7 a = gx if h < 4 else gy b = gy if h < 4 else gx return (a if (h & 1) == 0 else -a) + (b if (h & 2) == 0 else -b) g_aa = _g(aa, xf, yf) g_ba = _g(ba, xf1, yf) g_ab = _g(ab, xf, yf1) g_bb = _g(bb, xf1, yf1) x1 = g_aa + u * (g_ba - g_aa) x2 = g_ab + u * (g_bb - g_ab) return x1 + v * (x2 - x1) class World: """Procedurally generated terrain world.""" TERRAIN_CHARS = { "mountain": "^", "forest": "T", "grass": ".", "sand": ":", "water": "~", } def __init__(self, seed: int = 42, width: int = 1000, height: int = 1000): self.seed = seed self.width = width self.height = height # generate terrain grid self.terrain = self._generate_terrain() def _generate_elevation(self) -> list[list[float]]: """Generate tileable elevation map using fractal noise. Computes at 1/4 resolution and interpolates up for speed. Tiles seamlessly in both axes so the world can wrap. """ w = self.width h = self.height perm = _make_perm(self.seed) noise = _noise # generate at 1/4 resolution (no boundary padding, we wrap instead) step = 4 cw = w // step ch = h // step coarse = [[0.0] * cw for _ in range(ch)] min_val = float("inf") max_val = float("-inf") for cy in range(ch): row = coarse[cy] for cx in range(cw): # map coarse coords back to world space [0, 1) wx = cx * step / w wy = cy * step / h # each octave tiles at its own frequency value = ( noise(perm, wx * 4, wy * 4, 4, 4) + noise(perm, wx * 8, wy * 8, 8, 8) * 0.5 + noise(perm, wx * 16, wy * 16, 16, 16) * 0.25 ) row[cx] = value if value < min_val: min_val = value if value > max_val: max_val = value # normalize coarse grid to [0, 1] val_range = max_val - min_val if val_range > 0: inv_range = 1.0 / val_range for cy in range(ch): row = coarse[cy] for cx in range(cw): row[cx] = (row[cx] - min_val) * inv_range # bilinear interpolation to full resolution (wrapping at edges) inv_step = 1.0 / step elevation = [[0.0] * w for _ in range(h)] for y in range(h): cy_f = y * inv_step cy_floor = int(cy_f) cy0 = cy_floor % ch cy1 = (cy0 + 1) % ch fy = cy_f - cy_floor fy_inv = 1.0 - fy row_0 = coarse[cy0] row_1 = coarse[cy1] out_row = elevation[y] for x in range(w): cx_f = x * inv_step cx_floor = int(cx_f) cx0 = cx_floor % cw cx1 = (cx0 + 1) % cw fx = cx_f - cx_floor # bilinear interpolation out_row[x] = ( row_0[cx0] * (1.0 - fx) * fy_inv + row_0[cx1] * fx * fy_inv + row_1[cx0] * (1.0 - fx) * fy + row_1[cx1] * fx * fy ) return elevation def _derive_terrain_from_elevation( self, elevation: list[list[float]] ) -> list[list[str]]: """Convert elevation map to terrain types.""" w = self.width h = self.height # local lookups mountain = self.TERRAIN_CHARS["mountain"] forest = self.TERRAIN_CHARS["forest"] grass = self.TERRAIN_CHARS["grass"] sand = self.TERRAIN_CHARS["sand"] water = self.TERRAIN_CHARS["water"] terrain = [[""] * w for _ in range(h)] for y in range(h): elev_row = elevation[y] terr_row = terrain[y] for x in range(w): e = elev_row[x] if e > 0.75: terr_row[x] = mountain elif e > 0.55: terr_row[x] = forest elif e > 0.25: terr_row[x] = grass elif e > 0.15: terr_row[x] = sand else: terr_row[x] = water return terrain def _generate_rivers( self, terrain: list[list[str]], elevation: list[list[float]] ) -> None: """Generate rivers from mountains to water bodies.""" rng = random.Random(self.seed) w = self.width h = self.height # find some high-elevation starting points num_rivers = max(5, (w * h) // 50000) for _ in range(num_rivers): # pick random high elevation point x = rng.randint(0, w - 1) y = rng.randint(0, h - 1) if elevation[y][x] < 0.6: continue # trace downhill (wrapping at edges) visited = set() while True: if (x, y) in visited or len(visited) > 200: break visited.add((x, y)) # if we hit water, stop if terrain[y][x] == self.TERRAIN_CHARS["water"]: break # carve river if elevation[y][x] < 0.75: # don't carve through mountains terrain[y][x] = self.TERRAIN_CHARS["water"] # find steepest descent to neighbor (wrapping) current_elevation = elevation[y][x] best_x, best_y = x, y best_elevation = current_elevation for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: nx = (x + dx) % w ny = (y + dy) % h if elevation[ny][nx] < best_elevation: best_elevation = elevation[ny][nx] best_x, best_y = nx, ny if (best_x, best_y) == (x, y): break x, y = best_x, best_y def _generate_terrain(self) -> list[list[str]]: """Generate complete terrain map.""" elevation = self._generate_elevation() terrain = self._derive_terrain_from_elevation(elevation) self._generate_rivers(terrain, elevation) return terrain def wrap(self, x: int, y: int) -> tuple[int, int]: """Wrap coordinates to stay within world bounds.""" return x % self.width, y % self.height def get_tile(self, x: int, y: int) -> str: """Return terrain character at position (wrapping).""" wx, wy = self.wrap(x, y) return self.terrain[wy][wx] def is_passable(self, x: int, y: int) -> bool: """Check if position is passable (wrapping).""" tile = self.get_tile(x, y) return tile not in {self.TERRAIN_CHARS["mountain"], self.TERRAIN_CHARS["water"]} def get_viewport( self, cx: int, cy: int, width: int, height: int ) -> list[list[str]]: """Return 2D slice of terrain centered on (cx, cy), wrapping at edges.""" viewport = [] half_width = width // 2 half_height = height // 2 start_y = cy - half_height start_x = cx - half_width for dy in range(height): row = [] for dx in range(width): wx, wy = self.wrap(start_x + dx, start_y + dy) row.append(self.terrain[wy][wx]) viewport.append(row) return viewport