Add world cache to speedup startup
This commit is contained in:
parent
a10f3d4e70
commit
9948a36f5f
3 changed files with 93 additions and 5 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue