From d260c9a1ef31176329b8675b553c75b891d0e6df Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Wed, 26 Nov 2025 15:43:25 +0530 Subject: [PATCH] refactor: abstract WebSocket handling into shared base class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduce WebSocketHandler for reusable socket lifecycle management: • login, connect, close, start_listener • continuous receive loop with on_message callback • background listener task support - Create PlayerWebSocketHandler subclass implementing TicTacToe behavior: • match_create, match_join, send_move helpers • parse match_data opcodes and pretty-print board state - Move shared logic (auth, recv loop, WS decoding) out of gameplay module - Simplifies test scenario execution & enables future bot/test clients - Reduces duplication and improves separation of concerns --- game_flow.py | 73 ++++++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/game_flow.py b/game_flow.py index 06c754c..6c5d2fa 100644 --- a/game_flow.py +++ b/game_flow.py @@ -2,6 +2,7 @@ import random import asyncio import base64 import json +from typing import Awaitable, Callable import requests import websockets @@ -19,16 +20,23 @@ def print_board(board): print("\n" + separator.join(rows) + "\n") -class PlayerWebSocketHandler(object): - def __init__(self, custom_id: str, label: str): +class WebSocketHandler(object): + def __init__( + self, + custom_id: str, + label: str, + ): self.custom_id = custom_id self.label = label + self.token = None self.ws = None self.listener_task = None - # ---------- Auth & Connect ---------- + async def on_message(self, msg: dict): + raise NotImplementedError("Override me!") + # ---------- Auth & Connect ---------- def login(self): """Authenticate via custom ID and store token.""" basic = base64.b64encode(f"{SERVER_KEY}:".encode()).decode() @@ -50,31 +58,13 @@ class PlayerWebSocketHandler(object): self.ws = await websockets.connect(url) # ---------- Listener ---------- - async def listener(self): """Continuously receive events + decode match data.""" try: while True: raw = await self.ws.recv() msg = json.loads(raw) - - if "match_data" not in msg: - print(f"[{self.label}] {msg}") - continue - - md = msg["match_data"] - op = int(md.get("op_code", 0)) - payload = json.loads(base64.b64decode(md["data"]).decode()) - - if op == 1: - print(f"[{self.label}] MOVE -> {payload}") - elif op == 2: - print(f"\n[{self.label}] GAME STATE") - print_board(payload["board"]) - print(f"Turn={payload['turn']} Winner={payload['winner']}\n") - else: - print(f"[{self.label}] UNKNOWN OPCODE {op}: {payload}") - + await self.on_message(msg) # ✅ handoff except websockets.exceptions.ConnectionClosedOK: print(f"[{self.label}] WebSocket closed gracefully") except websockets.exceptions.ConnectionClosedError as e: @@ -85,8 +75,35 @@ class PlayerWebSocketHandler(object): if self.ws: self.listener_task = asyncio.create_task(self.listener()) - # ---------- Match Helpers ---------- + # ---------- Cleanup ---------- + async def close(self): + if self.listener_task: + self.listener_task.cancel() + if self.ws: + await self.ws.close() + print(f"[{self.label}] Connection closed") + +class PlayerWebSocketHandler(WebSocketHandler): + async def on_message(self, msg): + if "match_data" not in msg: + print(f"[{self.label}] {msg}") + return + + md = msg["match_data"] + op = int(md.get("op_code", 0)) + payload = json.loads(base64.b64decode(md["data"]).decode()) + + if op == 1: + print(f"[{self.label}] MOVE -> {payload}") + elif op == 2: + print(f"\n[{self.label}] GAME STATE") + print_board(payload["board"]) + print(f"Turn={payload['turn']} Winner={payload['winner']}\n") + else: + print(f"[{self.label}] UNKNOWN OPCODE {op}: {payload}") + + # ---------- Match Helpers ---------- async def create_match(self) -> str: await self.ws.send(json.dumps({"match_create": {}})) msg = json.loads(await self.ws.recv()) @@ -100,7 +117,6 @@ class PlayerWebSocketHandler(object): print(f"[{self.label}] Joined match: {match_id}") # ---------- Gameplay ---------- - async def send_move(self, match_id: str, row: int, col: int): payload = {"row": row, "col": col} encoded = base64.b64encode(json.dumps(payload).encode()).decode() @@ -114,15 +130,6 @@ class PlayerWebSocketHandler(object): })) print(f"[{self.label}] Sent move: {payload}") - # ---------- Cleanup ---------- - - async def close(self): - if self.listener_task: - self.listener_task.cancel() - if self.ws: - await self.ws.close() - print(f"[{self.label}] Connection closed") - async def happy_path( match_id: str,