From e79c523034b40319de453933cf850659a21eae84 Mon Sep 17 00:00:00 2001 From: Vladyslav Doloman Date: Tue, 7 Oct 2025 20:53:24 +0300 Subject: [PATCH] Transport: integrate aioquic QUIC datagram server skeleton (QuicWebTransportServer) and QUIC mode in run.py - New server/quic_transport.py using aioquic to accept QUIC connections and datagrams - run.py: QUIC mode when QUIC_CERT/QUIC_KEY provided; else in-memory - requirements.txt: aioquic + cryptography --- IDEAS.md | 27 ------- requirements.txt | 2 + run.py | 36 +++++++-- server/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 167 bytes server/__pycache__/config.cpython-311.pyc | Bin 0 -> 2091 bytes server/__pycache__/model.cpython-311.pyc | Bin 0 -> 4866 bytes server/__pycache__/protocol.cpython-311.pyc | Bin 0 -> 22691 bytes server/__pycache__/server.cpython-311.pyc | Bin 0 -> 29832 bytes server/__pycache__/transport.cpython-311.pyc | Bin 0 -> 3976 bytes server/quic_transport.py | 75 +++++++++++++++++++ 10 files changed, 108 insertions(+), 32 deletions(-) delete mode 100644 IDEAS.md create mode 100644 requirements.txt create mode 100644 server/__pycache__/__init__.cpython-311.pyc create mode 100644 server/__pycache__/config.cpython-311.pyc create mode 100644 server/__pycache__/model.cpython-311.pyc create mode 100644 server/__pycache__/protocol.cpython-311.pyc create mode 100644 server/__pycache__/server.cpython-311.pyc create mode 100644 server/__pycache__/transport.cpython-311.pyc create mode 100644 server/quic_transport.py diff --git a/IDEAS.md b/IDEAS.md deleted file mode 100644 index 4736cfe..0000000 --- a/IDEAS.md +++ /dev/null @@ -1,27 +0,0 @@ -This project will be a network mutiplayer Snake game. - -python 3 server and a web client that uses webtransport datagrams. - -the logic of the game: When the snake hits something, it doesn't die, the head stays in place, but the tail shortens by 1 every tick the head is turned in the direction of the obstacle. The player can change the direction the head is turned and the snake continues to move in new direction. The snake can not be shorter than its head, so the head always lives, even when the tail is all gone. -consider the possibility of stucking on anothers player snake (head or tail)? when the other player's snake have passed by and no more considered the obstacle, the current player's snake should continue moving. -self-collision is not permanent, because the tail keeps shrinking, there will be a moment, when the segment of a tail that was an obstacle will cease to exist -other snake collision can be auto-cleared when the other snake is moved away or shrinks to the point when it is no longer an obstacle -if a player is disconnected - his snake and score dissapears from the game, so there is no game-over state. the server runs constantly, without "rounds" of gameplay. new players connect directly into the game. There is no end game to detect winner or loser, there is continuous gameplay and in each moment there is a length of each snake. The longest snake is the "winner" at each moment (considering it can shorten or be outrunned by another player and lose its winning position). -keep the snake color the same for the whole duration of client connection -instead of a score based on eaten apples display the snakes current lengths -when only the head is left - it can turn 180 degrees as well as 90 - -make a small input buffer to buffer 3 direction changes for next ticks. if the new direction the user is pressing is directly opposite to the last in the buffer - replace the last in the buffer (do not add it as a new step). -instead of ignoring overflowinf inputs - replace the last one. -do not add to the buffer repeating inputs. before calculating next position when consuming 1 direction from buffer check if it is 180 turn when snake length>1, if so - ignore this input and consume the next one. - -when connected to server - show the current gameplay on the background with the text overlay "press space to join" -when 0 players left - populate the field with 3 apples - -field size 60 by 40 (by default). allow room in the protocol it to be changed between 3x3 up to 255x255 - -when using webtransport datagrams (UDP) for the game protocol, add packet number into the message to ignore late packets. make possible to wrap numbering from the beginnig in a case of integer overflow. use compression for the data in packets, so the full field update can be transmitted as one UDP datagram (typically up to 1500 bytes) -in UDP (webtransport datagrams) Protocol Design - allow room for lost packets before/after wrapping; -make room for up to 32 simultaneous players with different colors; limit player name length to 16 characters - -if UDP packet size exceeds 1280 bytes - split the update in several parts that do not exceed this size by updating different snakes info in different independant packets, so if one of them is lost - some of the information still reaches the recipient. If a snake is so long that its update doesn't fit into one packet by itself - then find a way to split it into several updates, preferably of the similar size (split the snake into the equal size parts) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9728d04 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +aioquic>=1.2.0 +cryptography>=41.0.0 diff --git a/run.py b/run.py index 8e798dc..6b00d39 100644 --- a/run.py +++ b/run.py @@ -1,10 +1,36 @@ -from server.server import main +import asyncio +import os + +from server.server import GameServer +from server.config import ServerConfig +from server.transport import InMemoryTransport + + +async def main(): + from server.server import GameServer + from server.transport import InMemoryTransport + + cfg = ServerConfig() + server = GameServer(transport=InMemoryTransport(lambda d, p: server.on_datagram(d, p)), config=cfg) + await asyncio.gather(server.transport.run(), server.tick_loop()) + if __name__ == "__main__": - import asyncio - try: - asyncio.run(main()) + # Optional QUIC mode if env vars are provided + cert = os.environ.get("QUIC_CERT") + key = os.environ.get("QUIC_KEY") + host = os.environ.get("QUIC_HOST", "0.0.0.0") + port = int(os.environ.get("QUIC_PORT", "4433")) + if cert and key: + from server.quic_transport import QuicWebTransportServer + from server.server import GameServer + cfg = ServerConfig() + async def start_quic(): + server = GameServer(transport=QuicWebTransportServer(host, port, cert, key, lambda d, p: server.on_datagram(d, p)), config=cfg) + await asyncio.gather(server.transport.run(), server.tick_loop()) + asyncio.run(start_quic()) + else: + asyncio.run(main()) except KeyboardInterrupt: pass - diff --git a/server/__pycache__/__init__.cpython-311.pyc b/server/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..85d27a05c2977e48b0a21e93bc2ca125e7bfd1f6 GIT binary patch literal 167 zcmZ3^%ge<81b=OxX7B;&#~=<2FhUuhK}x1Gq%%Y@q%Z_CXfpa~GTvg3k5A0WiH~2& z@EN4+mtl}qOkRFUs&1K)k)Dx(o`G(8W}a?^nMq7CP}C@TZlX-=wL5gX71kZr{vH+*1bWMsU-#nZqJf<-JqApkQwCw+Yzx~a;H@|r^^XAR? z$3!BIpv*4*xiu#v^aoW=ON2LslMkWs2wBJy98?l?L7;8X5lfOT2?$Ff`T$wd0kY&n z5uqpW@p1^&lT!uhvK3iHxoC@8azdz>u3Pp^-!8izs4>g*&7xy^Ua>m_8WLB8lkcGM z2w|iP7ShGfVKKU7iCDIz1?aJ4J;JmI)1rEmX?;xV*ZZs(Fyl;9^nTW-GA*IUn0AF} z1A5#_0&|dQny#?EA*Nlkh7V+2wbHPOOsKA6vV+O{Wp~5gY*9ie*{&Z(c5TbwYTclQ z=oYp&xBM0jaTxRM;Wz>^GTR2mP-|aJr4qCl*$%vWhq;!IOao4Jfmdpg;9e2Dse-31%?&r zB&UVO6+Yo)53F#w!q@1l(>_!b_rz_+CJ@I?EA5H`a#M`i^>i828+0K>{7(8%r%(p_zjm(JcVS&MSs z3;2khuEbJz1TxSQfy8;fg&>cvth8tlhz78q-LSD^Wj%WzXG>Mj&#vR_uI+ExZgx7K zFU-ue=ptL!Dfh*0Hg%~p`TX=`_Wq}_K0j3;^s4S>{>`s$_avJz zCz2E}!afUcIFPyfubH&`b_wr~p1q*?^WAiIPld^9sF@u6{{9bokbDi~nBn7TUUM7Z(bUiK zFG_IhJ&1u&mLP|yk-dIoCp0zfVi_8W$4mat)jaf3cHRfo1! z!+20N9p1x^IUF#IGcUSfuyf~UJwW~S9yqy1ZLa|QgvhJ3{W1l5g&5F=_bWi{%Loeo3zyp#`FFK9`8jm0Uh*acn<*Dk{}38G#2#AQ}j~MEA1!}mA04?=xQ&= TXUzT=!)FrE4>A`AuqFKi7@-F! literal 0 HcmV?d00001 diff --git a/server/__pycache__/model.cpython-311.pyc b/server/__pycache__/model.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f8903c7a2fdbed153308a2942706317c2925be7 GIT binary patch literal 4866 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 bamGs3oAH@?&`+81<@CVo!v~fD1eN|TkKGt4 literal 0 HcmV?d00001 diff --git a/server/__pycache__/server.cpython-311.pyc b/server/__pycache__/server.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc291abf1f0b36d4523cff4f793b157affe1b6b4 GIT binary patch literal 29832 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�>CKA2POXajHN1@CW>9^t!^WR|vG@VCD=Weh zTQJFJ#o2Uka6oBJYr@^EFwW#wJxEE*P?Jf-zPZjFI9G(v)Dohy$-San(T#=}x}l{K zQPpr1tGl?Rgy}jD(!D4w!=pYk*k&CR#_;j*TiHe7hH(O}BY-q0NMj%c2Mu!*Q>Z^T z*%9D{l1E*xpOHol9YMj)w9>79z`oSG!ENrljx0Kc0n*7KDk3UffWx%#BhxK?PG5SQ zU)s+vHH@%Kj3Lbbt%Z0OHV=tbVW{A1AxYuCz^cSj04TJk zKogg?mE78QtHiRh%_}|LKH|B!$F4B5Ojd@+Crsg`q6$B%Dbl<7MBk#yjC3 zkaxvJScU-Z{Q+n*p(x4$Io*g~Pe^`8{tn1&BYqu_HyXcpHFc7oyti?G^`84+>$_+E z^-W{)!hY_Jois4i3{;uQI2f{z9OB$lB!>x*yL}>*G?EGwrTyd(=bm;MW8(_IY(Hr{ dH-CuJPrJOcc?G>Vm^Pk2e<)c`hX_b_{sv<#Xo&y- literal 0 HcmV?d00001 diff --git a/server/quic_transport.py b/server/quic_transport.py new file mode 100644 index 0000000..009427a --- /dev/null +++ b/server/quic_transport.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import asyncio +from dataclasses import dataclass +from typing import Awaitable, Callable, Dict, Optional + +from .transport import DatagramServerTransport, OnDatagram, TransportPeer + + +try: + from aioquic.asyncio import QuicConnectionProtocol, serve + from aioquic.quic.configuration import QuicConfiguration + from aioquic.quic.events import DatagramFrameReceived, ProtocolNegotiated +except Exception: # pragma: no cover - optional dependency not installed in skeleton + QuicConnectionProtocol = object # type: ignore + QuicConfiguration = object # type: ignore + serve = None # type: ignore + DatagramFrameReceived = object # type: ignore + ProtocolNegotiated = object # type: ignore + + +class GameQuicProtocol(QuicConnectionProtocol): # type: ignore[misc] + def __init__(self, *args, on_datagram: OnDatagram, peers: Dict[int, "GameQuicProtocol"], **kwargs): + super().__init__(*args, **kwargs) + self._on_datagram = on_datagram + self._peers = peers + self._peer_id: Optional[int] = None + + def quic_event_received(self, event) -> None: # type: ignore[override] + if isinstance(event, ProtocolNegotiated): + # Register by connection id + self._peer_id = int(self._quic.connection_id) # type: ignore[attr-defined] + self._peers[self._peer_id] = self + elif isinstance(event, DatagramFrameReceived): + # Schedule async callback + if self._peer_id is None: + return + peer = TransportPeer(addr=self) + asyncio.ensure_future(self._on_datagram(bytes(event.data), peer)) + + async def send_datagram(self, data: bytes) -> None: + self._quic.send_datagram_frame(data) # type: ignore[attr-defined] + await self._loop.run_in_executor(None, self.transmit) # type: ignore[attr-defined] + + +class QuicWebTransportServer(DatagramServerTransport): + def __init__(self, host: str, port: int, certfile: str, keyfile: str, on_datagram: OnDatagram): + if serve is None: + raise RuntimeError("aioquic is not installed. Please `pip install aioquic`.") + self.host = host + self.port = port + self.certfile = certfile + self.keyfile = keyfile + self._on_datagram = on_datagram + self._server = None + self._peers: Dict[int, GameQuicProtocol] = {} + + async def send(self, data: bytes, peer: TransportPeer) -> None: + proto = peer.addr # expected GameQuicProtocol + if isinstance(proto, GameQuicProtocol): + await proto.send_datagram(data) + + async def run(self) -> None: + configuration = QuicConfiguration(is_client=False, alpn_protocols=["h3"]) + configuration.load_cert_chain(self.certfile, self.keyfile) + + async def _create_protocol(*args, **kwargs): + return GameQuicProtocol(*args, on_datagram=self._on_datagram, peers=self._peers, **kwargs) + + self._server = await serve(self.host, self.port, configuration=configuration, create_protocol=_create_protocol) + try: + await self._server.wait_closed() + finally: + self._server.close() +