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:
73
game_flow.py
73
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,
|
||||
|
||||
Reference in New Issue
Block a user