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 asyncio
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
from typing import Awaitable, Callable
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import websockets
|
import websockets
|
||||||
@@ -19,16 +20,23 @@ def print_board(board):
|
|||||||
print("\n" + separator.join(rows) + "\n")
|
print("\n" + separator.join(rows) + "\n")
|
||||||
|
|
||||||
|
|
||||||
class PlayerWebSocketHandler(object):
|
class WebSocketHandler(object):
|
||||||
def __init__(self, custom_id: str, label: str):
|
def __init__(
|
||||||
|
self,
|
||||||
|
custom_id: str,
|
||||||
|
label: str,
|
||||||
|
):
|
||||||
self.custom_id = custom_id
|
self.custom_id = custom_id
|
||||||
self.label = label
|
self.label = label
|
||||||
|
|
||||||
self.token = None
|
self.token = None
|
||||||
self.ws = None
|
self.ws = None
|
||||||
self.listener_task = None
|
self.listener_task = None
|
||||||
|
|
||||||
# ---------- Auth & Connect ----------
|
async def on_message(self, msg: dict):
|
||||||
|
raise NotImplementedError("Override me!")
|
||||||
|
|
||||||
|
# ---------- Auth & Connect ----------
|
||||||
def login(self):
|
def login(self):
|
||||||
"""Authenticate via custom ID and store token."""
|
"""Authenticate via custom ID and store token."""
|
||||||
basic = base64.b64encode(f"{SERVER_KEY}:".encode()).decode()
|
basic = base64.b64encode(f"{SERVER_KEY}:".encode()).decode()
|
||||||
@@ -50,31 +58,13 @@ class PlayerWebSocketHandler(object):
|
|||||||
self.ws = await websockets.connect(url)
|
self.ws = await websockets.connect(url)
|
||||||
|
|
||||||
# ---------- Listener ----------
|
# ---------- Listener ----------
|
||||||
|
|
||||||
async def listener(self):
|
async def listener(self):
|
||||||
"""Continuously receive events + decode match data."""
|
"""Continuously receive events + decode match data."""
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
raw = await self.ws.recv()
|
raw = await self.ws.recv()
|
||||||
msg = json.loads(raw)
|
msg = json.loads(raw)
|
||||||
|
await self.on_message(msg) # ✅ handoff
|
||||||
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}")
|
|
||||||
|
|
||||||
except websockets.exceptions.ConnectionClosedOK:
|
except websockets.exceptions.ConnectionClosedOK:
|
||||||
print(f"[{self.label}] WebSocket closed gracefully")
|
print(f"[{self.label}] WebSocket closed gracefully")
|
||||||
except websockets.exceptions.ConnectionClosedError as e:
|
except websockets.exceptions.ConnectionClosedError as e:
|
||||||
@@ -85,8 +75,35 @@ class PlayerWebSocketHandler(object):
|
|||||||
if self.ws:
|
if self.ws:
|
||||||
self.listener_task = asyncio.create_task(self.listener())
|
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:
|
async def create_match(self) -> str:
|
||||||
await self.ws.send(json.dumps({"match_create": {}}))
|
await self.ws.send(json.dumps({"match_create": {}}))
|
||||||
msg = json.loads(await self.ws.recv())
|
msg = json.loads(await self.ws.recv())
|
||||||
@@ -100,7 +117,6 @@ class PlayerWebSocketHandler(object):
|
|||||||
print(f"[{self.label}] Joined match: {match_id}")
|
print(f"[{self.label}] Joined match: {match_id}")
|
||||||
|
|
||||||
# ---------- Gameplay ----------
|
# ---------- Gameplay ----------
|
||||||
|
|
||||||
async def send_move(self, match_id: str, row: int, col: int):
|
async def send_move(self, match_id: str, row: int, col: int):
|
||||||
payload = {"row": row, "col": col}
|
payload = {"row": row, "col": col}
|
||||||
encoded = base64.b64encode(json.dumps(payload).encode()).decode()
|
encoded = base64.b64encode(json.dumps(payload).encode()).decode()
|
||||||
@@ -114,15 +130,6 @@ class PlayerWebSocketHandler(object):
|
|||||||
}))
|
}))
|
||||||
print(f"[{self.label}] Sent move: {payload}")
|
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(
|
async def happy_path(
|
||||||
match_id: str,
|
match_id: str,
|
||||||
|
|||||||
Reference in New Issue
Block a user