Add game loop skeleton for periodic tick processing
This commit is contained in:
parent
d220835f7d
commit
075a6ce303
2 changed files with 45 additions and 0 deletions
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue