Add game loop skeleton for periodic tick processing

This commit is contained in:
Jared Miller 2026-02-07 16:17:20 -05:00
parent d220835f7d
commit 075a6ce303
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 45 additions and 0 deletions

View file

@ -15,12 +15,15 @@ import mudlib.commands.fly
import mudlib.commands.look import mudlib.commands.look
import mudlib.commands.movement import mudlib.commands.movement
import mudlib.commands.quit import mudlib.commands.quit
from mudlib.effects import clear_expired
from mudlib.player import Player, players from mudlib.player import Player, players
from mudlib.world.terrain import World from mudlib.world.terrain import World
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
PORT = 6789 PORT = 6789
TICK_RATE = 10 # ticks per second
TICK_INTERVAL = 1.0 / TICK_RATE
# Module-level world instance, generated once at startup # Module-level world instance, generated once at startup
_world: World | None = None _world: World | None = None
@ -34,6 +37,18 @@ def load_world_config(world_name: str = "earth") -> dict:
return tomllib.load(f) 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]: 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. """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) log.info("listening on 127.0.0.1:%d", PORT)
loop_task = asyncio.create_task(game_loop())
try: try:
while True: while True:
await asyncio.sleep(3600) await asyncio.sleep(3600)
except KeyboardInterrupt: except KeyboardInterrupt:
log.info("shutting down...") log.info("shutting down...")
loop_task.cancel()
server.close() server.close()
await server.wait_closed() await server.wait_closed()

View file

@ -103,3 +103,30 @@ def test_load_world_config_missing():
"""Config loader raises FileNotFoundError for nonexistent world.""" """Config loader raises FileNotFoundError for nonexistent world."""
with pytest.raises(FileNotFoundError): with pytest.raises(FileNotFoundError):
server.load_world_config("nonexistent") 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