refactor: abstract WebSocket handling into shared base class

- 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
This commit is contained in:
2025-11-26 15:43:25 +05:30
parent 1b4e7a5ee0
commit d260c9a1ef

View File

@@ -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,