from __future__ import annotations import asyncio from dataclasses import dataclass from typing import Dict, List from .config import ServerConfig from .model import GameState, PlayerSession from .protocol import ( Direction, InputEvent, PacketType, build_config_update, build_input_broadcast, pack_header, ) from .transport import DatagramServerTransport, TransportPeer @dataclass class ServerRuntime: config: ServerConfig state: GameState seq: int = 0 version: int = 1 def next_seq(self) -> int: self.seq = (self.seq + 1) & 0xFFFF return self.seq class GameServer: def __init__(self, transport: DatagramServerTransport, config: ServerConfig): self.transport = transport self.runtime = ServerRuntime(config=config, state=GameState(config.width, config.height)) self.sessions: Dict[int, PlayerSession] = {} self._config_update_interval_ticks = 50 # periodic resend async def on_datagram(self, data: bytes, peer: TransportPeer) -> None: # Skeleton: just ignore inbound for now; real impl would parse and dispatch return async def broadcast_config_update(self) -> None: r = self.runtime payload = build_config_update( version=r.version, seq=r.next_seq(), tick=r.state.tick & 0xFFFF, tick_rate=r.config.tick_rate, wrap_edges=r.config.wrap_edges, apples_per_snake=r.config.apples_per_snake, apples_cap=r.config.apples_cap, ) # Broadcast to all sessions (requires real peer handles) for session in list(self.sessions.values()): await self.transport.send(payload, TransportPeer(session.peer)) async def relay_input_broadcast( self, *, from_player_id: int, input_seq: int, base_tick: int, events: List[InputEvent], apply_at_tick: int | None, ) -> None: r = self.runtime payload = build_input_broadcast( version=r.version, seq=r.next_seq(), tick=r.state.tick & 0xFFFF, player_id=from_player_id, input_seq=input_seq, base_tick=base_tick & 0xFFFF, events=events, apply_at_tick=apply_at_tick, ) for session in list(self.sessions.values()): if session.player_id == from_player_id: continue await self.transport.send(payload, TransportPeer(session.peer)) async def tick_loop(self) -> None: r = self.runtime tick_duration = 1.0 / max(1, r.config.tick_rate) next_cfg_resend = self._config_update_interval_ticks while True: start = asyncio.get_event_loop().time() # TODO: process inputs, update snakes, collisions, apples, deltas r.state.tick = (r.state.tick + 1) & 0xFFFFFFFF next_cfg_resend -= 1 if next_cfg_resend <= 0: await self.broadcast_config_update() next_cfg_resend = self._config_update_interval_ticks elapsed = asyncio.get_event_loop().time() - start await asyncio.sleep(max(0.0, tick_duration - elapsed)) async def main() -> None: from .transport import InMemoryTransport cfg = ServerConfig() server = GameServer(transport=InMemoryTransport(lambda d, p: server.on_datagram(d, p)), config=cfg) # In-memory transport never returns; run tick loop in parallel await asyncio.gather(server.transport.run(), server.tick_loop()) if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: pass