Files
tic-tac-toe/game_flow.py

159 lines
4.2 KiB
Python

import asyncio
import base64
import json
import requests
import websockets
HOST = "http://127.0.0.1:7350"
WS = "ws://127.0.0.1:7350"
SERVER_KEY = "defaultkey"
def print_board(board):
# board = [["X","",""], ["","O",""], ["","",""]]
symbols = lambda c: c if c else " "
rows = [" | ".join(symbols(c) for c in row) for row in board]
separator = "\n---------\n"
print("\n" + separator.join(rows) + "\n")
# ---------- Auth ----------
def login(custom_id: str) -> str:
"""Authenticate via custom ID and return Nakama session token (JWT)."""
basic = base64.b64encode(f"{SERVER_KEY}:".encode()).decode()
r = requests.post(
f"{HOST}/v2/account/authenticate/custom?create=true",
headers={"Authorization": f"Basic {basic}"},
json={"id": custom_id},
)
r.raise_for_status()
body = r.json()
return body["token"]
# ---------- WebSocket helpers ----------
async def connect(token: str) -> websockets.ClientConnection:
url = f"{WS}/ws?token={token}"
ws = await websockets.connect(url)
return ws
async def listener(ws, label: str):
"""Log all messages for a given socket."""
try:
while True:
raw = await ws.recv()
data = json.loads(raw)
if "match_data" not in data:
print(f"[{label}] {data}")
continue
md = data["match_data"]
parse_match_data(md, label)
except websockets.exceptions.ConnectionClosedOK:
print(f"[{label}] WebSocket closed cleanly")
return
except websockets.exceptions.ConnectionClosedError as e:
print(f"[{label}] WebSocket closed with error: {e}")
return
def parse_match_data(md, label: str):
op = int(md.get("op_code", 0))
# decode base64 payload
payload_json = base64.b64decode(md["data"]).decode()
payload = json.loads(payload_json)
# OpMove → just log move
if op == 1:
print(f"[{label}] MOVE -> {payload}")
return
# OpState → pretty-print board
if op == 2:
print(f"\n[{label}] GAME STATE")
print_board(payload["board"])
print(f"Turn={payload['turn']} Winner={payload['winner']}\n")
return
# other opcodes
raise RuntimeError(f"[{label}] UNKNOWN OPCODE {op}: {payload}")
async def send_move(ws, match_id: str, row: int, col: int):
"""Send a TicTacToe move using OpMove = 1 with base64-encoded data."""
payload = {"row": row, "col": col}
# Nakama expects `data` as bytes -> base64 string in JSON
data_bytes = json.dumps(payload).encode("utf-8")
data_b64 = base64.b64encode(data_bytes).decode("ascii")
msg = {
"match_data_send": {
"match_id": match_id,
"op_code": 1, # OpMove
"data": data_b64,
}
}
await ws.send(json.dumps(msg))
# ---------- Main flow ----------
async def main():
# 1) Login 2 players
token1 = login("player_one_123456")
token2 = login("player_two_123456")
# 2) Connect sockets
ws1 = await connect(token1)
ws2 = await connect(token2)
# 3) Create a match from P1
await ws1.send(json.dumps({"match_create": {}}))
raw = await ws1.recv()
msg = json.loads(raw)
match_id = msg["match"]["match_id"]
print("Match:", match_id)
# 4) Only P2 explicitly joins (creator is auto-joined)
await ws2.send(json.dumps({"match_join": {"match_id": match_id}}))
# 5) Start listeners
asyncio.create_task(listener(ws1, "P1"))
asyncio.create_task(listener(ws2, "P2"))
# Give server time to process joins and initial state
await asyncio.sleep(1)
# 6) Play a quick winning game for P1 (X)
# P1: (0,0)
await send_move(ws1, match_id, 0, 0)
await asyncio.sleep(1.5)
# P2: (1,1)
await send_move(ws2, match_id, 1, 1)
await asyncio.sleep(1.5)
# P1: (0,1)
await send_move(ws1, match_id, 0, 1)
await asyncio.sleep(1.5)
# P2: (2,2)
await send_move(ws2, match_id, 2, 2)
await asyncio.sleep(1.5)
# P1: (0,2) -> X wins by top row
await send_move(ws1, match_id, 0, 2)
# Wait to receive final state broadcast (OpState = 2)
await asyncio.sleep(2)
await ws1.close()
await ws2.close()
if __name__ == "__main__":
asyncio.run(main())