Create overworld Zone at startup, set player.location
This commit is contained in:
parent
6f58ae0501
commit
66c6e1ebd4
2 changed files with 47 additions and 14 deletions
|
|
@ -43,6 +43,7 @@ from mudlib.store import (
|
||||||
update_last_login,
|
update_last_login,
|
||||||
)
|
)
|
||||||
from mudlib.world.terrain import World
|
from mudlib.world.terrain import World
|
||||||
|
from mudlib.zone import Zone
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -54,6 +55,7 @@ AUTOSAVE_INTERVAL = 60.0 # seconds between auto-saves
|
||||||
|
|
||||||
# Module-level world instance, generated once at startup
|
# Module-level world instance, generated once at startup
|
||||||
_world: World | None = None
|
_world: World | None = None
|
||||||
|
_overworld: Zone | None = None
|
||||||
|
|
||||||
|
|
||||||
def load_world_config(world_name: str = "earth") -> dict:
|
def load_world_config(world_name: str = "earth") -> dict:
|
||||||
|
|
@ -92,11 +94,11 @@ async def game_loop() -> None:
|
||||||
await asyncio.sleep(sleep_time)
|
await asyncio.sleep(sleep_time)
|
||||||
|
|
||||||
|
|
||||||
def find_passable_start(world: World, start_x: int, start_y: int) -> tuple[int, int]:
|
def find_passable_start(zone: Zone, start_x: int, start_y: int) -> tuple[int, int]:
|
||||||
"""Find a passable tile starting from (start_x, start_y) and searching outward.
|
"""Find a passable tile starting from (start_x, start_y) and searching outward.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
world: The world to search in
|
zone: The zone to search in
|
||||||
start_x: Starting X coordinate
|
start_x: Starting X coordinate
|
||||||
start_y: Starting Y coordinate
|
start_y: Starting Y coordinate
|
||||||
|
|
||||||
|
|
@ -104,7 +106,7 @@ def find_passable_start(world: World, start_x: int, start_y: int) -> tuple[int,
|
||||||
Tuple of (x, y) for the first passable tile found
|
Tuple of (x, y) for the first passable tile found
|
||||||
"""
|
"""
|
||||||
# Try the starting position first
|
# Try the starting position first
|
||||||
if world.is_passable(start_x, start_y):
|
if zone.is_passable(start_x, start_y):
|
||||||
return start_x, start_y
|
return start_x, start_y
|
||||||
|
|
||||||
# Spiral outward from the starting position (wrapping)
|
# Spiral outward from the starting position (wrapping)
|
||||||
|
|
@ -115,9 +117,9 @@ def find_passable_start(world: World, start_x: int, start_y: int) -> tuple[int,
|
||||||
if abs(dx) != radius and abs(dy) != radius:
|
if abs(dx) != radius and abs(dy) != radius:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
x, y = world.wrap(start_x + dx, start_y + dy)
|
x, y = zone.wrap(start_x + dx, start_y + dy)
|
||||||
|
|
||||||
if world.is_passable(x, y):
|
if zone.is_passable(x, y):
|
||||||
return x, y
|
return x, y
|
||||||
|
|
||||||
# Fallback to starting position if nothing found
|
# Fallback to starting position if nothing found
|
||||||
|
|
@ -210,6 +212,9 @@ async def shell(
|
||||||
_writer = cast(telnetlib3.TelnetWriterUnicode, writer)
|
_writer = cast(telnetlib3.TelnetWriterUnicode, writer)
|
||||||
|
|
||||||
assert _world is not None, "World must be initialized before accepting connections"
|
assert _world is not None, "World must be initialized before accepting connections"
|
||||||
|
assert _overworld is not None, (
|
||||||
|
"Overworld zone must be initialized before accepting connections"
|
||||||
|
)
|
||||||
|
|
||||||
log.debug("new connection from %s", _writer.get_extra_info("peername"))
|
log.debug("new connection from %s", _writer.get_extra_info("peername"))
|
||||||
|
|
||||||
|
|
@ -248,9 +253,9 @@ async def shell(
|
||||||
player_data: PlayerData | None = login_result["player_data"]
|
player_data: PlayerData | None = login_result["player_data"]
|
||||||
if player_data is None:
|
if player_data is None:
|
||||||
# New player - find a passable starting position
|
# New player - find a passable starting position
|
||||||
center_x = _world.width // 2
|
center_x = _overworld.width // 2
|
||||||
center_y = _world.height // 2
|
center_y = _overworld.height // 2
|
||||||
start_x, start_y = find_passable_start(_world, center_x, center_y)
|
start_x, start_y = find_passable_start(_overworld, center_x, center_y)
|
||||||
player_data = {
|
player_data = {
|
||||||
"x": start_x,
|
"x": start_x,
|
||||||
"y": start_y,
|
"y": start_y,
|
||||||
|
|
@ -261,10 +266,10 @@ async def shell(
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# Existing player - verify spawn position is still passable
|
# Existing player - verify spawn position is still passable
|
||||||
if not _world.is_passable(player_data["x"], player_data["y"]):
|
if not _overworld.is_passable(player_data["x"], player_data["y"]):
|
||||||
# Saved position is no longer passable, find a new one
|
# Saved position is no longer passable, find a new one
|
||||||
start_x, start_y = find_passable_start(
|
start_x, start_y = find_passable_start(
|
||||||
_world, player_data["x"], player_data["y"]
|
_overworld, player_data["x"], player_data["y"]
|
||||||
)
|
)
|
||||||
player_data["x"] = start_x
|
player_data["x"] = start_x
|
||||||
player_data["y"] = start_y
|
player_data["y"] = start_y
|
||||||
|
|
@ -272,6 +277,7 @@ async def shell(
|
||||||
# Create player instance
|
# Create player instance
|
||||||
player = Player(
|
player = Player(
|
||||||
name=player_name,
|
name=player_name,
|
||||||
|
location=_overworld,
|
||||||
x=player_data["x"],
|
x=player_data["x"],
|
||||||
y=player_data["y"],
|
y=player_data["y"],
|
||||||
pl=player_data["pl"],
|
pl=player_data["pl"],
|
||||||
|
|
@ -381,7 +387,7 @@ async def shell(
|
||||||
|
|
||||||
async def run_server() -> None:
|
async def run_server() -> None:
|
||||||
"""Start the MUD telnet server."""
|
"""Start the MUD telnet server."""
|
||||||
global _world
|
global _world, _overworld
|
||||||
|
|
||||||
# Initialize database
|
# Initialize database
|
||||||
data_dir = pathlib.Path(__file__).resolve().parents[2] / "data"
|
data_dir = pathlib.Path(__file__).resolve().parents[2] / "data"
|
||||||
|
|
@ -412,6 +418,16 @@ async def run_server() -> None:
|
||||||
else:
|
else:
|
||||||
log.info("world generated in %.2fs (cached for next run)", elapsed)
|
log.info("world generated in %.2fs (cached for next run)", elapsed)
|
||||||
|
|
||||||
|
# Create overworld zone from generated terrain
|
||||||
|
_overworld = Zone(
|
||||||
|
name="overworld",
|
||||||
|
width=_world.width,
|
||||||
|
height=_world.height,
|
||||||
|
terrain=_world.terrain,
|
||||||
|
toroidal=True,
|
||||||
|
)
|
||||||
|
log.info("created overworld zone (%dx%d, toroidal)", _world.width, _world.height)
|
||||||
|
|
||||||
# Inject world into command modules
|
# Inject world into command modules
|
||||||
mudlib.commands.fly.world = _world
|
mudlib.commands.fly.world = _world
|
||||||
mudlib.commands.look.world = _world
|
mudlib.commands.look.world = _world
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import pytest
|
||||||
from mudlib import server
|
from mudlib import server
|
||||||
from mudlib.store import init_db
|
from mudlib.store import init_db
|
||||||
from mudlib.world.terrain import World
|
from mudlib.world.terrain import World
|
||||||
|
from mudlib.zone import Zone
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -44,16 +45,23 @@ def test_run_server_exists():
|
||||||
|
|
||||||
def test_find_passable_start():
|
def test_find_passable_start():
|
||||||
world = World(seed=42, width=100, height=100)
|
world = World(seed=42, width=100, height=100)
|
||||||
x, y = server.find_passable_start(world, 50, 50)
|
zone = Zone(
|
||||||
|
name="test", width=100, height=100, terrain=world.terrain, toroidal=True
|
||||||
|
)
|
||||||
|
x, y = server.find_passable_start(zone, 50, 50)
|
||||||
assert isinstance(x, int)
|
assert isinstance(x, int)
|
||||||
assert isinstance(y, int)
|
assert isinstance(y, int)
|
||||||
assert world.is_passable(x, y)
|
assert zone.is_passable(x, y)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_shell_greets_and_accepts_commands(temp_db):
|
async def test_shell_greets_and_accepts_commands(temp_db):
|
||||||
world = World(seed=42, width=100, height=100)
|
world = World(seed=42, width=100, height=100)
|
||||||
|
zone = Zone(
|
||||||
|
name="overworld", width=100, height=100, terrain=world.terrain, toroidal=True
|
||||||
|
)
|
||||||
server._world = world
|
server._world = world
|
||||||
|
server._overworld = zone
|
||||||
|
|
||||||
reader = AsyncMock()
|
reader = AsyncMock()
|
||||||
writer = MagicMock()
|
writer = MagicMock()
|
||||||
|
|
@ -91,7 +99,12 @@ async def test_shell_greets_and_accepts_commands(temp_db):
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_shell_handles_eof():
|
async def test_shell_handles_eof():
|
||||||
server._world = World(seed=42, width=100, height=100)
|
world = World(seed=42, width=100, height=100)
|
||||||
|
zone = Zone(
|
||||||
|
name="overworld", width=100, height=100, terrain=world.terrain, toroidal=True
|
||||||
|
)
|
||||||
|
server._world = world
|
||||||
|
server._overworld = zone
|
||||||
|
|
||||||
reader = AsyncMock()
|
reader = AsyncMock()
|
||||||
writer = MagicMock()
|
writer = MagicMock()
|
||||||
|
|
@ -110,7 +123,11 @@ async def test_shell_handles_eof():
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_shell_handles_quit(temp_db):
|
async def test_shell_handles_quit(temp_db):
|
||||||
world = World(seed=42, width=100, height=100)
|
world = World(seed=42, width=100, height=100)
|
||||||
|
zone = Zone(
|
||||||
|
name="overworld", width=100, height=100, terrain=world.terrain, toroidal=True
|
||||||
|
)
|
||||||
server._world = world
|
server._world = world
|
||||||
|
server._overworld = zone
|
||||||
|
|
||||||
reader = AsyncMock()
|
reader = AsyncMock()
|
||||||
writer = MagicMock()
|
writer = MagicMock()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue