diff --git a/game_flow.py b/game_flow.py new file mode 100644 index 0000000..7a28dee --- /dev/null +++ b/game_flow.py @@ -0,0 +1,124 @@ +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" + + +# ---------- 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() + # print("LOGIN:", body) + 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) + print(f"[{label}] {data}") + except websockets.exceptions.ConnectionClosedOK: + print(f"[{label}] WebSocket closed cleanly") + except websockets.exceptions.ConnectionClosedError as e: + print(f"[{label}] WebSocket closed with error: {e}") + + +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 match from P1 + await ws1.send(json.dumps({"match_create": {}})) + raw = await ws1.recv() + print("RAW:", raw) + msg = json.loads(raw) + match_id = msg["match"]["match_id"] + print("Match:", match_id) # keep the dot; it's part of the 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(0.3) + + # P2: (1,1) + await send_move(ws2, match_id, 1, 1) + await asyncio.sleep(0.3) + + # P1: (0,1) + await send_move(ws1, match_id, 0, 1) + await asyncio.sleep(0.3) + + # P2: (2,2) + await send_move(ws2, match_id, 2, 2) + await asyncio.sleep(0.3) + + # 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())