From 075a6ce303524becfd944ff9e0df572a9fb83a65 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Sat, 7 Feb 2026 16:17:20 -0500 Subject: [PATCH] Add game loop skeleton for periodic tick processing --- src/mudlib/server.py | 18 ++++++++++++++++++ tests/test_server.py | 27 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/mudlib/server.py b/src/mudlib/server.py index 641047f..27a3c24 100644 --- a/src/mudlib/server.py +++ b/src/mudlib/server.py @@ -15,12 +15,15 @@ import mudlib.commands.fly import mudlib.commands.look import mudlib.commands.movement import mudlib.commands.quit +from mudlib.effects import clear_expired from mudlib.player import Player, players from mudlib.world.terrain import World log = logging.getLogger(__name__) PORT = 6789 +TICK_RATE = 10 # ticks per second +TICK_INTERVAL = 1.0 / TICK_RATE # Module-level world instance, generated once at startup _world: World | None = None @@ -34,6 +37,18 @@ def load_world_config(world_name: str = "earth") -> dict: return tomllib.load(f) +async def game_loop() -> None: + """Run periodic game tasks at TICK_RATE ticks per second.""" + log.info("game loop started (%d ticks/sec)", TICK_RATE) + while True: + t0 = asyncio.get_event_loop().time() + clear_expired() + elapsed = asyncio.get_event_loop().time() - t0 + sleep_time = TICK_INTERVAL - elapsed + if sleep_time > 0: + await asyncio.sleep(sleep_time) + + def find_passable_start(world: World, start_x: int, start_y: int) -> tuple[int, int]: """Find a passable tile starting from (start_x, start_y) and searching outward. @@ -183,10 +198,13 @@ async def run_server() -> None: ) log.info("listening on 127.0.0.1:%d", PORT) + loop_task = asyncio.create_task(game_loop()) + try: while True: await asyncio.sleep(3600) except KeyboardInterrupt: log.info("shutting down...") + loop_task.cancel() server.close() await server.wait_closed() diff --git a/tests/test_server.py b/tests/test_server.py index cd1d0d1..fdcb79e 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -103,3 +103,30 @@ def test_load_world_config_missing(): """Config loader raises FileNotFoundError for nonexistent world.""" with pytest.raises(FileNotFoundError): server.load_world_config("nonexistent") + + +def test_tick_constants(): + """Tick rate and interval are configured correctly.""" + assert server.TICK_RATE == 10 + assert server.TICK_INTERVAL == pytest.approx(0.1) + + +def test_game_loop_exists(): + """Game loop is an async callable.""" + assert callable(server.game_loop) + assert asyncio.iscoroutinefunction(server.game_loop) + + +@pytest.mark.asyncio +async def test_game_loop_calls_clear_expired(): + """Game loop calls clear_expired each tick.""" + with patch("mudlib.server.clear_expired") as mock_clear: + task = asyncio.create_task(server.game_loop()) + await asyncio.sleep(0.25) + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + assert mock_clear.call_count >= 1