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
This commit is contained in:
115
server/server.py
Normal file
115
server/server.py
Normal file
@@ -0,0 +1,115 @@
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user