Add world cache to speedup startup

This commit is contained in:
Jared Miller 2026-02-07 15:00:07 -05:00
parent a10f3d4e70
commit 9948a36f5f
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
3 changed files with 93 additions and 5 deletions

View file

@ -2,6 +2,7 @@
import asyncio
import logging
import pathlib
import time
from typing import cast
@ -136,12 +137,16 @@ async def run_server() -> None:
"""Start the MUD telnet server."""
global _world
# Generate world once at startup
log.info("generating world (seed=42, 1000x1000)...")
# Generate world once at startup (cached to build/ after first run)
cache_dir = pathlib.Path(__file__).resolve().parents[2] / "build"
log.info("loading world (seed=42, 1000x1000)...")
t0 = time.monotonic()
_world = World(seed=42, width=1000, height=1000)
_world = World(seed=42, width=1000, height=1000, cache_dir=cache_dir)
elapsed = time.monotonic() - t0
log.info("world generated in %.2fs", elapsed)
if _world.cached:
log.info("world loaded from cache in %.2fs", elapsed)
else:
log.info("world generated in %.2fs (cached for next run)", elapsed)
# Inject world into command modules
mudlib.commands.fly.world = _world

View file

@ -1,6 +1,7 @@
"""Terrain generation for MUD worlds using deterministic noise."""
import math
import pathlib
import random
@ -70,14 +71,33 @@ class World:
"water": "~",
}
def __init__(self, seed: int = 42, width: int = 1000, height: int = 1000):
def __init__(
self,
seed: int = 42,
width: int = 1000,
height: int = 1000,
cache_dir: pathlib.Path | None = None,
):
self.seed = seed
self.width = width
self.height = height
self.cached = False
# try loading from cache
if cache_dir is not None:
terrain = self._load_cache(cache_dir)
if terrain is not None:
self.terrain = terrain
self.cached = True
return
# generate terrain grid
self.terrain = self._generate_terrain()
# save to cache for next time
if cache_dir is not None:
self._save_cache(cache_dir)
def _generate_elevation(self) -> list[list[float]]:
"""Generate tileable elevation map using fractal noise.
@ -249,6 +269,32 @@ class World:
self._generate_rivers(terrain, elevation)
return terrain
def _cache_path(self, cache_dir: pathlib.Path) -> pathlib.Path:
return cache_dir / f"terrain_{self.seed}_{self.width}x{self.height}.bin"
def _load_cache(self, cache_dir: pathlib.Path) -> list[list[str]] | None:
"""Load terrain from a cached binary file. Returns None on miss."""
path = self._cache_path(cache_dir)
if not path.exists():
return None
data = path.read_bytes()
if len(data) != self.width * self.height:
return None
terrain = []
for y in range(self.height):
offset = y * self.width
row = [chr(b) for b in data[offset : offset + self.width]]
terrain.append(row)
return terrain
def _save_cache(self, cache_dir: pathlib.Path) -> None:
"""Save terrain to a binary file (1 byte per tile)."""
cache_dir.mkdir(parents=True, exist_ok=True)
data = bytearray()
for row in self.terrain:
data.extend(ord(c) for c in row)
self._cache_path(cache_dir).write_bytes(bytes(data))
def wrap(self, x: int, y: int) -> tuple[int, int]:
"""Wrap coordinates to stay within world bounds."""
return x % self.width, y % self.height

View file

@ -204,3 +204,40 @@ def test_large_world_generation():
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