Files
codexPySnake/server/server.py
Vladyslav Doloman 9043ba81c0 Server scaffold: protocol + config + transport abstraction + tick loop skeleton
- Protocol: PacketType, TLV Body types, QUIC varint, header, input_broadcast
  and config_update builders, 2-bit body bitpacking helper
- Config/model: live-config ServerConfig, basic GameState/Snake/Session
- Transport: InMemoryTransport placeholder and QUIC server stub
- Server: asyncio tick loop, periodic config_update broadcast, immediate
  input_broadcast relay; main entry and run.py
2025-10-07 20:02:28 +03:00

116 lines
3.6 KiB
Python

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