From c7edb83f4e24e91b1e275c2d0931d9b265cb135f Mon Sep 17 00:00:00 2001 From: Eteimz <8teims@gmail.com> Date: Mon, 21 Jul 2025 06:44:21 +0100 Subject: [PATCH 1/6] Fixed whot example --- examples/whot-web/game.html | 47 ++++ examples/whot-web/index.html | 55 ++--- examples/whot-web/main.py | 37 +++- examples/whot-web/requirements.txt | 3 + examples/whot-web/script.js | 101 ++++++--- examples/whot-web/server.py | 330 ++++++++++++++++++++++++----- 6 files changed, 450 insertions(+), 123 deletions(-) create mode 100644 examples/whot-web/game.html diff --git a/examples/whot-web/game.html b/examples/whot-web/game.html new file mode 100644 index 0000000..4f3dbca --- /dev/null +++ b/examples/whot-web/game.html @@ -0,0 +1,47 @@ + + + + + + + + Whot Game + + + +

Welcome to the Whot Game

+ +

Current Player:

+

Player ID:

+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ + + + + + + + \ No newline at end of file diff --git a/examples/whot-web/index.html b/examples/whot-web/index.html index bcdc5c1..4efc11d 100644 --- a/examples/whot-web/index.html +++ b/examples/whot-web/index.html @@ -9,41 +9,32 @@ -

Welcome to the Whot Game

+

Create Whot Game

-

Current Player:

-

Player ID:

- -
-
- -
-
- -
-
- -
-
- -
+
+ New Game + + + +
- -
- New - Join -
- - - - + + + \ No newline at end of file diff --git a/examples/whot-web/main.py b/examples/whot-web/main.py index 64f72ad..e667dbc 100644 --- a/examples/whot-web/main.py +++ b/examples/whot-web/main.py @@ -1,32 +1,51 @@ -from aiohttp import web import asyncio +import os + +from aiohttp import web +import aiohttp_jinja2 +from dotenv import load_dotenv +import jinja2 from websockets.asyncio.server import serve + from server import handler -PORT = 8080 -WEBSOCKET_PORT = 8765 +load_dotenv() + +ADDRESS = os.environ.get("ADDRESS", "127.0.0.1") +PORT = int(os.environ.get("PORT", 8080)) +WEBSOCKET_PORT = int(os.environ.get("WEBSOCKET_PORT", 8765)) + +# Create an aiohttp web app +app = web.Application() + +aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('.')) # Serve index.html async def handle_index(request): return web.FileResponse("index.html") +async def handle_game(request): + return aiohttp_jinja2.render_template("game.html", request, context={ + "websocket_url": f"ws://{ADDRESS}:{WEBSOCKET_PORT}" + }) + # WebSocket server function async def websocket_server(): - async with serve(handler, "0.0.0.0", WEBSOCKET_PORT): - print(f"WebSocket server running at ws://0.0.0.0:{WEBSOCKET_PORT}") + async with serve(handler, ADDRESS, WEBSOCKET_PORT): + print(f"WebSocket server running at ws://{ADDRESS}:{WEBSOCKET_PORT}") await asyncio.Future() # Keep the WebSocket server running -# Create an aiohttp web app -app = web.Application() +# Define routes app.router.add_get("/", handle_index) +app.router.add_get("/game", handle_game) app.router.add_static("/", path=".", name="static") # Function to run the aiohttp server async def run_server(): runner = web.AppRunner(app) await runner.setup() - site = web.TCPSite(runner, "0.0.0.0", PORT) - print(f"HTTP server running at http://0.0.0.0:{PORT}") + site = web.TCPSite(runner, ADDRESS, PORT) + print(f"HTTP server running at http://{ADDRESS}:{PORT}") await site.start() await asyncio.Future() # Keep running diff --git a/examples/whot-web/requirements.txt b/examples/whot-web/requirements.txt index 396afaf..bbc23d3 100644 --- a/examples/whot-web/requirements.txt +++ b/examples/whot-web/requirements.txt @@ -1,2 +1,5 @@ websockets==13.0.1 aiohttp +whot +python-dotenv +aiohttp-jinja2 \ No newline at end of file diff --git a/examples/whot-web/script.js b/examples/whot-web/script.js index 38064da..b0f3c07 100644 --- a/examples/whot-web/script.js +++ b/examples/whot-web/script.js @@ -18,13 +18,13 @@ function initGame(websocket) { function addMiddleCardImage(text) { const cardImg = document.getElementById("card_top"); - - const card = text.toLowerCase().split(" "); + const face = text["face"] + const suit = text["suit"].toLowerCase(); let path; - if (card[1] != 'whot') { - path = `assets/images/${card[1]}/${card[0]}_${card[1]}.png` + if (suit != 'whot') { + path = `assets/images/${suit}/${face}_${suit}.png` } else { path = `assets/images/20_whot.png` } @@ -61,18 +61,20 @@ function addPlayerCardImages(cards, websocket, player_id) { // Create a new image element const newCard = document.createElement('img'); - const card = cards[i].toLowerCase().split(" "); + // const card = cards[i].toLowerCase().split(" "); + const face = cards[i]["face"] + const suit = cards[i]["suit"].toLowerCase(); // Set attributes for the image - if (card[1] != 'whot') { - newCard.src = `assets/images/${card[1]}/${card[0]}_${card[1]}.png`; // Path to the image + if (suit != 'whot') { + newCard.src = `assets/images/${suit}/${face}_${suit}.png`; // Path to the image } else { newCard.src = `assets/images/20_whot.png` } newCard.alt = 'Player Card'; // Alternative text newCard.width = 100; // Set width newCard.height = 120; // Set height - console.log('Card: ', card[0]) + console.log(player_id) newCard.onclick = () => { const event = { @@ -92,38 +94,73 @@ function receiveMoves(websocket, player_id) { const current_player = document.getElementById("current_player"); websocket.addEventListener("message", ({ data }) => { - console.log(data) + const event = JSON.parse(data); switch (event.type) { + case "init": // Create links for inviting the second player and spectators. - document.querySelector(".join").href = "?join=" + event.join; + const link = document.createElement("p"); + link.innerText = window.location.href + "?join=" + event.join; + document.body.appendChild(link); + //document.querySelector(".watch").href = "?watch=" + event.watch; break; - + case "play": - current_player.textContent = `Current Player: ${event.game_state["current_player"]}` - - const opponent = Object.keys(event.game_state).filter(key => key.startsWith('player_')).filter(key => key !== `player_${event.player_id}`) + playCard(event, websocket); + break; - addMiddleCardImage(event.game_state["pile_top"]) - addOponnentCardImages(event.game_state[opponent]) - addPlayerCardImages(event.game_state[`player_${event.player_id}`], websocket, player_id) + case "normal": + playCard(event, websocket); break; + case "hold_on": + playCard(event, websocket); + + // showMessage("Hold on!") + + break; + + case "general_market": + playCard(event, websocket); + + // showMessage("Go Gen!") + + break; + + case "pick_2": + playCard(event, websocket); + + // showMessage("Pick two!") + + break; + + case "pick_3": + playCard(event, websocket); + + // showMessage("Pick three!") + + break; + + case "suspension": + playCard(event, websocket); + + // showMessage("Suspension!") + + break; + case "request": current_player.textContent = `Current Player: ${event.game_state["current_player"]}` - const opponent2 = Object.keys(event.game_state).filter(key => key.startsWith('player_')).filter(key => key !== `player_${event.player_id}`) + const opponent2 = Object.keys(event.game_state["players"]).filter(key => key !== `player_${event.player_id}`) let div = document.getElementById("i_need"); addMiddleCardImage(event.game_state["pile_top"]) - addOponnentCardImages(event.game_state[opponent2]) - console.log(player_id) - console.log(event.player_id) - addPlayerCardImages(event.game_state[`player_${event.player_id}`], websocket, player_id) + addOponnentCardImages(event.game_state["players"][opponent2[0]]) + addPlayerCardImages(event.game_state["players"][`player_${event.player_id}`], websocket, player_id) div.style.visibility = "visible"; @@ -147,7 +184,7 @@ function receiveMoves(websocket, player_id) { break; case "message": - console.log(event.message) + showMessage(event.message) break; case "failed": @@ -168,6 +205,16 @@ function showMessage(message) { window.setTimeout(() => window.alert(message), 50); } +function playCard(event, websocket){ + current_player.textContent = `Current Player: ${event.game_state["current_player"]}` + + const opponent = Object.keys(event.game_state["players"]).filter(key => key !== `player_${event.player_id}`) + + addMiddleCardImage(event.game_state["pile_top"]) + addOponnentCardImages(event.game_state["players"][opponent[0]]) + addPlayerCardImages(event.game_state["players"][`player_${event.player_id}`], websocket, player_id) +} + function sendMoves(websocket, card, playBtn) { // Don't send moves for a spectator watching a game. const params = new URLSearchParams(window.location.search); @@ -195,7 +242,9 @@ window.addEventListener("DOMContentLoaded", () => { const player_id = document.getElementById("player_id"); const i_need = document.getElementById("i_need"); - const websocket = new WebSocket("ws://127.0.0.1:8765/"); + console.log("Connecting to WebSocket..."); + console.log(WEBSOCKET_URL) + const websocket = new WebSocket(WEBSOCKET_URL); initGame(websocket); receiveMoves(websocket, player_id); @@ -221,4 +270,8 @@ window.addEventListener("DOMContentLoaded", () => { websocket.send(JSON.stringify(event)); } } + + websocket.onclose = function(event) { + console.log("WebSocket closed:", event); + }; }); diff --git a/examples/whot-web/server.py b/examples/whot-web/server.py index db0e768..5bfd7fa 100644 --- a/examples/whot-web/server.py +++ b/examples/whot-web/server.py @@ -2,10 +2,26 @@ import websockets from websockets.asyncio.client import ClientConnection from whot import Whot +from whot.utils import serialize_game_view +from whot.exceptions import ( + GameNotStartedError, + GameOverError, + InvalidMoveError, + InvalidCardError, + InvalidSuitError, +) import json import secrets +# Todo: +# Consider adding everything to a class +# Work on individual messages in the play event. Done +# Work on disconnections so users can that get disconnected can always reconnect +# The game ends when all players have left, Add option for restart +# Refactor code + +# JOIN = {} @@ -22,13 +38,43 @@ def __init__(self, game: Whot): self.connections: dict[str, ClientConnection] = {} self.num_of_connections = 0 - def add_connection(self, connection: ClientConnection): - if self.game.num_of_players != self.num_of_connections: + def add_connection(self, connection: ClientConnection) -> str: + if len(self.game.players) != self.num_of_connections: self.num_of_connections += 1 self.connections[f"player_{self.num_of_connections}"] = connection return f"player_{self.num_of_connections}" else: return False + + async def send(self, socket_id, event): + """ + sends message to a particular websocket client + """ + + await self.connections[socket_id].send(json.dumps(event)) + + async def broadcast(self, event, exceptions = None): + """ + sends message to all websocket client + """ + + websockets.broadcast(self.connections.values(), json.dumps(event)) # type: ignore + + +class WhotServer: + pass + +async def send_event(socket_id, type, game: Whot, gameConnections: GameConnection): + for i, socket_id in enumerate(gameConnections.connections, start=1): + event = { + "type": type, + "player_id": i, + "game_state": serialize_game_view(game.view(f"player_{i}")) + } + + await gameConnections.send(socket_id, event) + + async def play(websocket: ClientConnection, game: Whot, player_id: str, gameConnections: GameConnection): event = { @@ -38,31 +84,30 @@ async def play(websocket: ClientConnection, game: Whot, player_id: str, gameConn await websocket.send(json.dumps(event)) - while gameConnections.num_of_connections < 2: + while gameConnections.num_of_connections < len(game.players): await asyncio.sleep(1) game.start_game() - for i, socket in enumerate(gameConnections.connections, start=1): + for i, socket_id in enumerate(gameConnections.connections, start=1): event = { "type": "play", "player_id": i, - "game_state": game.view(f"player_{i}") + "game_state": serialize_game_view(game.view(f"player_{i}")) } - await gameConnections.connections[socket].send(json.dumps(event)) + await gameConnections.send(socket_id, event) if game.request_mode == True: - current_player = game.current_player.player_id + socket_id = game.current_player.player_id event = { "type": "request", "player_id": 1, - "game_state": game.view(f"player_1") + "game_state": serialize_game_view(game.view(f"player_1")) } - await gameConnections.connections[current_player].send(json.dumps(event)) - + await gameConnections.send(socket_id, event) async for message in websocket: @@ -72,60 +117,204 @@ async def play(websocket: ClientConnection, game: Whot, player_id: str, gameConn if event["player_id"] == game.game_state()["current_player"]: card_index = int(event["card"]) - result = game.play(card_index) - - if result["status"] == "Success": - for i, socket in enumerate(gameConnections.connections, start=1): - event = { - "type": "play", - "player_id": i, - "game_state": game.view(f"player_{i}") - } + try: + result = game.play(card_index) + + if result["status"] == True and result["type"] != "request": + + if result["type"] == "pick_2": + + for i, socket_id in enumerate(gameConnections.connections, start=1): + # Notify all players of the result + event = { + "type": result["type"], + "player_id": i, + "game_state": serialize_game_view(game.view(f"player_{i}")) + } + await gameConnections.send(socket_id, event) + + if socket_id == game.current_player.player_id: + # Notify the player to pick two cards + + event = { + "type": "message", + "message": "Pick two! You have been asked to pick two cards." + } + + await gameConnections.send(socket_id, event) + + elif result["type"] == "general_market": + + for i, socket_id in enumerate(gameConnections.connections, start=1): + event = { + "type": result["type"], + "player_id": i, + "game_state": serialize_game_view(game.view(f"player_{i}")) + } + await gameConnections.send(socket_id, event) + + if socket_id != game.current_player.player_id: + # Notify the player to pick two cards + + event = { + "type": "message", + "message": "Everyone Go gen." + } + + await gameConnections.send(socket_id, event) + + elif result["type"] == "suspension": + try: + current_player_index = game.players.index(game.current_player) - 1 + suspended_player_id = game.players[current_player_index].player_id + except IndexError: + current_player_index = len(game.players) - 1 + suspended_player_id = game.players[current_player_index].player_id + + for i, socket_id in enumerate(gameConnections.connections, start=1): + event = { + "type": result["type"], + "player_id": i, + "game_state": serialize_game_view(game.view(f"player_{i}")) + } + await gameConnections.send(socket_id, event) + + if socket_id == suspended_player_id: + # Notify the player that they are suspended + event = { + "type": "message", + "message": "You have been suspended." + } + + await gameConnections.send(suspended_player_id, event) + + elif result["type"] == "hold_on": + try: + current_player_index = game.players.index(game.current_player) + 1 + on_hold_player_id = game.players[current_player_index].player_id + except IndexError: + current_player_index = 0 + on_hold_player_id = game.players[current_player_index].player_id + + for i, socket_id in enumerate(gameConnections.connections, start=1): + event = { + "type": result["type"], + "player_id": i, + "game_state": serialize_game_view(game.view(f"player_{i}")) + } + await gameConnections.send(socket_id, event) + + if socket_id == on_hold_player_id: + # Notify the player that they are suspended + event = { + "type": "message", + "message": "You have been placed on hold." + } + + await gameConnections.send(on_hold_player_id, event) + else: - await gameConnections.connections[socket].send(json.dumps(event)) - - elif result["status"] == "Failed": + for i, socket_id in enumerate(gameConnections.connections, start=1): + event = { + "type": result["type"], + "player_id": i, + "game_state": serialize_game_view(game.view(f"player_{i}")) + } + await gameConnections.send(socket_id, event) + + if socket_id == game.current_player.player_id: + # Notify the player that they are suspended + event = { + "type": "message", + "message": "Your turn to play." + } + + await gameConnections.send(socket_id, event) - for i, socket in enumerate(gameConnections.connections, start=1): - event = { - "type": "failed", - "current_player": game.game_state()["current_player"] - } + elif result['type'] == "request": - await gameConnections.connections[socket].send(json.dumps(event)) + current_player = game.current_player.player_id - elif result['status'] == "Request": + for i, socket_id in enumerate(gameConnections.connections, start=1): - current_player = game.current_player.player_id + if socket_id == current_player: + + event = { + "type": "request", + "player_id": i, + "game_state": serialize_game_view(game.view(f"player_{i}")) + } - for i, socket in enumerate(gameConnections.connections, start=1): + else: - if socket == current_player: + event = { + "type": "play", + "player_id": i, + "game_state": serialize_game_view(game.view(f"player_{i}")) + } - event = { - "type": "request", - "player_id": i, - "game_state": game.view(f"player_{i}") - } + await gameConnections.send(socket_id, event) - else: + elif result['status'] == False: + for i, socket_id in enumerate(gameConnections.connections, start=1): event = { - "type": "play", + "type": result["type"], "player_id": i, - "game_state": game.view(f"player_{i}") + "game_state": serialize_game_view(game.view(f"player_{i}")) } + await gameConnections.send(socket_id, event) + + event = { + "type": "win", + "winner": result['player_id'], + } + game.save("game.json") # Todo: This could be improved + + await gameConnections.broadcast(event) + + except GameNotStartedError: + event = { + "type": "message", + "message": "Game has not started yet" + } + await websocket.send(json.dumps(event)) + + except GameOverError: + event = { + "type": "message", + "message": "Game is over" + } + await websocket.send(json.dumps(event)) + + except InvalidMoveError: + event = { + "type": "message", + "message": "Invalid move" + } + await websocket.send(json.dumps(event)) - await gameConnections.connections[socket].send(json.dumps(event)) + except InvalidCardError: + event = { + "type": "message", + "message": "Invalid card" + } + await websocket.send(json.dumps(event)) + except InvalidSuitError: + event = { + "type": "message", + "message": "Invalid suit" + } + await websocket.send(json.dumps(event)) - elif result['status'] == "GameOver": + except Exception as e: event = { - "type": "win", - "winner": result['winner'], + "type": "message", + "message": f"An error occurred: {str(e)}" } - game.save("game.json") - websockets.broadcast(gameConnections.connections.values(), json.dumps(event)) + await websocket.send(json.dumps(event)) + else: event = { "type": "message", @@ -140,14 +329,24 @@ async def play(websocket: ClientConnection, game: Whot, player_id: str, gameConn game.market() - for i, socket in enumerate(gameConnections.connections, start=1): + for i, socket_id in enumerate(gameConnections.connections, start=1): event = { "type": "play", "player_id": i, - "game_state": game.view(f"player_{i}") + "game_state": serialize_game_view(game.view(f"player_{i}")) } - await gameConnections.connections[socket].send(json.dumps(event)) + await gameConnections.send(socket_id, event) + + if socket_id == game.current_player.player_id: + # Notify the player that they are suspended + event = { + "type": "message", + "message": "Your turn to play." + } + + await gameConnections.send(socket_id, event) + elif event["type"] == "request": suit = event["suit"] @@ -156,31 +355,47 @@ async def play(websocket: ClientConnection, game: Whot, player_id: str, gameConn card = str(game.request(suit)['requested_suit']) - for i, socket in enumerate(gameConnections.connections, start=1): + for i, socket_id in enumerate(gameConnections.connections, start=1): - if socket != requester: + if socket_id != requester: event = { "type": "request_card", "message": f"{requester} requested for {card}", - "game_state": game.view(f"player_{i}") + "game_state": serialize_game_view(game.view(f"player_{i}")) } - await gameConnections.connections[socket].send(json.dumps(event)) + await gameConnections.send(socket_id, event) async def join(websocket: ClientConnection, join_key): try: gameConnection: GameConnection = JOIN[join_key] - print("Joined") + + event = { + "type": "message", + "message": "New Player Joined" + } + + await gameConnection.broadcast(event) + except KeyError: - print("Game doesn't exist") - return + await websocket.send(json.dumps({ + "type": "message", + "message": "Invalid join key" + })) + await websocket.close() player_id = gameConnection.add_connection(websocket) await play(websocket, gameConnection.game, player_id, gameConnection) async def start(websocket: ClientConnection): - game = Whot(2, number_of_cards=4) + + # These values would defined by the client later + num_of_player = 2 + num_of_cards = 4 + + game = Whot(num_of_player, num_of_cards) + gameConnection = GameConnection(game) player_id = gameConnection.add_connection(websocket) @@ -199,7 +414,6 @@ async def start(websocket: ClientConnection): del JOIN[join_key] async def handler(websocket: ClientConnection): - message = await websocket.recv() event = json.loads(message) assert event["type"] == "init" From 00ac62472a146409ba5e9f38ce8efa4e99928971 Mon Sep 17 00:00:00 2001 From: Eteimz <8teims@gmail.com> Date: Fri, 25 Jul 2025 02:53:53 +0100 Subject: [PATCH 2/6] Updated example --- examples/whot-web/script.js | 28 +---- examples/whot-web/server.py | 245 +++++++++++++++--------------------- 2 files changed, 108 insertions(+), 165 deletions(-) diff --git a/examples/whot-web/script.js b/examples/whot-web/script.js index b0f3c07..72b7d97 100644 --- a/examples/whot-web/script.js +++ b/examples/whot-web/script.js @@ -104,8 +104,6 @@ function receiveMoves(websocket, player_id) { const link = document.createElement("p"); link.innerText = window.location.href + "?join=" + event.join; document.body.appendChild(link); - - //document.querySelector(".watch").href = "?watch=" + event.watch; break; case "play": @@ -118,49 +116,34 @@ function receiveMoves(websocket, player_id) { case "hold_on": playCard(event, websocket); - - // showMessage("Hold on!") - break; case "general_market": playCard(event, websocket); - - // showMessage("Go Gen!") - break; case "pick_2": playCard(event, websocket); - - // showMessage("Pick two!") - break; case "pick_3": playCard(event, websocket); - - // showMessage("Pick three!") - break; case "suspension": playCard(event, websocket); - - // showMessage("Suspension!") - break; case "request": current_player.textContent = `Current Player: ${event.game_state["current_player"]}` - const opponent2 = Object.keys(event.game_state["players"]).filter(key => key !== `player_${event.player_id}`) + const opponent2 = Object.keys(event.game_state["players"]).filter(key => key !== event.player_id) let div = document.getElementById("i_need"); addMiddleCardImage(event.game_state["pile_top"]) addOponnentCardImages(event.game_state["players"][opponent2[0]]) - addPlayerCardImages(event.game_state["players"][`player_${event.player_id}`], websocket, player_id) + addPlayerCardImages(event.game_state["players"][event.player_id], websocket, player_id) div.style.visibility = "visible"; @@ -208,11 +191,11 @@ function showMessage(message) { function playCard(event, websocket){ current_player.textContent = `Current Player: ${event.game_state["current_player"]}` - const opponent = Object.keys(event.game_state["players"]).filter(key => key !== `player_${event.player_id}`) + const opponent = Object.keys(event.game_state["players"]).filter(key => key !== event.player_id) addMiddleCardImage(event.game_state["pile_top"]) addOponnentCardImages(event.game_state["players"][opponent[0]]) - addPlayerCardImages(event.game_state["players"][`player_${event.player_id}`], websocket, player_id) + addPlayerCardImages(event.game_state["players"][event.player_id], websocket, player_id) } function sendMoves(websocket, card, playBtn) { @@ -255,11 +238,12 @@ window.addEventListener("DOMContentLoaded", () => { type: "market", player_id: player[1] }; + console.log(event) console.log("Market!!") websocket.send(JSON.stringify(event)); } - for ( const button of i_need.children){ + for (const button of i_need.children){ button.onclick = () => { const event = { type: "request", diff --git a/examples/whot-web/server.py b/examples/whot-web/server.py index 5bfd7fa..e900b73 100644 --- a/examples/whot-web/server.py +++ b/examples/whot-web/server.py @@ -14,12 +14,14 @@ import json import secrets +from dataclasses import dataclass + # Todo: # Consider adding everything to a class # Work on individual messages in the play event. Done # Work on disconnections so users can that get disconnected can always reconnect # The game ends when all players have left, Add option for restart -# Refactor code +# Refactor code Done # @@ -60,19 +62,59 @@ async def broadcast(self, event, exceptions = None): websockets.broadcast(self.connections.values(), json.dumps(event)) # type: ignore +@dataclass +class Message: + receiver_ids: list[str] + content: str class WhotServer: - pass + async def play(self): + pass + + async def handler(self): + pass + + async def start(self): + pass + + async def join(self): + pass + + async def send_event_to_all(self): + pass -async def send_event(socket_id, type, game: Whot, gameConnections: GameConnection): - for i, socket_id in enumerate(gameConnections.connections, start=1): + async def send_event_to_one(self): + pass + +async def send_event_to_all( type, game: Whot, gameConnections: GameConnection, message: Message | None = None): + for socket_id in gameConnections.connections: event = { "type": type, - "player_id": i, - "game_state": serialize_game_view(game.view(f"player_{i}")) + "player_id": socket_id, + "game_state": serialize_game_view(game.view(socket_id)) } - await gameConnections.send(socket_id, event) + await gameConnections.send(socket_id, event) + + if message != None: + if socket_id in message.receiver_ids: + # Notify the player to pick two cards + + event = { + "type": "message", + "message": message.content + } + + await gameConnections.send(socket_id, event) + +async def send_event_to_one(socket_id, type, game: Whot, gameConnections: GameConnection): + event = { + "type": type, + "player_id": socket_id, + "game_state": serialize_game_view(game.view(socket_id)) + } + + await gameConnections.send(socket_id, event) @@ -89,30 +131,16 @@ async def play(websocket: ClientConnection, game: Whot, player_id: str, gameConn game.start_game() - for i, socket_id in enumerate(gameConnections.connections, start=1): - event = { - "type": "play", - "player_id": i, - "game_state": serialize_game_view(game.view(f"player_{i}")) - } - - await gameConnections.send(socket_id, event) + await send_event_to_all("play", game, gameConnections) if game.request_mode == True: socket_id = game.current_player.player_id - - event = { - "type": "request", - "player_id": 1, - "game_state": serialize_game_view(game.view(f"player_1")) - } - - await gameConnections.send(socket_id, event) + await send_event_to_one(socket_id, "request", game, gameConnections) async for message in websocket: event = json.loads(message) - + if event["type"] == "play": if event["player_id"] == game.game_state()["current_player"]: @@ -125,44 +153,22 @@ async def play(websocket: ClientConnection, game: Whot, player_id: str, gameConn if result["type"] == "pick_2": - for i, socket_id in enumerate(gameConnections.connections, start=1): - # Notify all players of the result - event = { - "type": result["type"], - "player_id": i, - "game_state": serialize_game_view(game.view(f"player_{i}")) - } - await gameConnections.send(socket_id, event) - - if socket_id == game.current_player.player_id: - # Notify the player to pick two cards - - event = { - "type": "message", - "message": "Pick two! You have been asked to pick two cards." - } - - await gameConnections.send(socket_id, event) + message = Message( + receiver_ids=[game.current_player.player_id], + content="Pick two! You have been asked to pick two cards.") + + await send_event_to_all(result["type"], game, gameConnections, message) elif result["type"] == "general_market": - for i, socket_id in enumerate(gameConnections.connections, start=1): - event = { - "type": result["type"], - "player_id": i, - "game_state": serialize_game_view(game.view(f"player_{i}")) - } - await gameConnections.send(socket_id, event) - - if socket_id != game.current_player.player_id: - # Notify the player to pick two cards - - event = { - "type": "message", - "message": "Everyone Go gen." - } - - await gameConnections.send(socket_id, event) + other_players = list(gameConnections.connections.keys()) + other_players.remove(game.current_player.player_id) + + message = Message( + receiver_ids=other_players, + content="Everyone Go gen.") + + await send_event_to_all(result["type"], game, gameConnections, message) elif result["type"] == "suspension": try: @@ -171,23 +177,12 @@ async def play(websocket: ClientConnection, game: Whot, player_id: str, gameConn except IndexError: current_player_index = len(game.players) - 1 suspended_player_id = game.players[current_player_index].player_id - - for i, socket_id in enumerate(gameConnections.connections, start=1): - event = { - "type": result["type"], - "player_id": i, - "game_state": serialize_game_view(game.view(f"player_{i}")) - } - await gameConnections.send(socket_id, event) - - if socket_id == suspended_player_id: - # Notify the player that they are suspended - event = { - "type": "message", - "message": "You have been suspended." - } - - await gameConnections.send(suspended_player_id, event) + + message = Message( + receiver_ids=[suspended_player_id], + content="You have been suspended.") + + await send_event_to_all(result["type"], game, gameConnections, message) elif result["type"] == "hold_on": try: @@ -196,75 +191,48 @@ async def play(websocket: ClientConnection, game: Whot, player_id: str, gameConn except IndexError: current_player_index = 0 on_hold_player_id = game.players[current_player_index].player_id + + message = Message( + receiver_ids=[on_hold_player_id], + content="You have been placed on hold.") + + await send_event_to_all(result["type"], game, gameConnections, message) - for i, socket_id in enumerate(gameConnections.connections, start=1): - event = { - "type": result["type"], - "player_id": i, - "game_state": serialize_game_view(game.view(f"player_{i}")) - } - await gameConnections.send(socket_id, event) - - if socket_id == on_hold_player_id: - # Notify the player that they are suspended - event = { - "type": "message", - "message": "You have been placed on hold." - } - - await gameConnections.send(on_hold_player_id, event) else: - for i, socket_id in enumerate(gameConnections.connections, start=1): - event = { - "type": result["type"], - "player_id": i, - "game_state": serialize_game_view(game.view(f"player_{i}")) - } - await gameConnections.send(socket_id, event) - - if socket_id == game.current_player.player_id: - # Notify the player that they are suspended - event = { - "type": "message", - "message": "Your turn to play." - } - - await gameConnections.send(socket_id, event) + message = Message( + receiver_ids=[game.current_player.player_id], + content="Your turn to play.") + + await send_event_to_all(result["type"], game, gameConnections, message) elif result['type'] == "request": current_player = game.current_player.player_id - for i, socket_id in enumerate(gameConnections.connections, start=1): + for socket_id in gameConnections.connections: if socket_id == current_player: event = { "type": "request", - "player_id": i, - "game_state": serialize_game_view(game.view(f"player_{i}")) + "player_id": socket_id, + "game_state": serialize_game_view(game.view(socket_id)) } else: event = { "type": "play", - "player_id": i, - "game_state": serialize_game_view(game.view(f"player_{i}")) + "player_id": socket_id, + "game_state": serialize_game_view(game.view(socket_id)) } await gameConnections.send(socket_id, event) elif result['status'] == False: - for i, socket_id in enumerate(gameConnections.connections, start=1): - event = { - "type": result["type"], - "player_id": i, - "game_state": serialize_game_view(game.view(f"player_{i}")) - } - await gameConnections.send(socket_id, event) + await send_event_to_all(result["type"], game, gameConnections) event = { "type": "win", @@ -301,6 +269,7 @@ async def play(websocket: ClientConnection, game: Whot, player_id: str, gameConn "message": "Invalid card" } await websocket.send(json.dumps(event)) + except InvalidSuitError: event = { "type": "message", @@ -324,28 +293,17 @@ async def play(websocket: ClientConnection, game: Whot, player_id: str, gameConn await websocket.send(json.dumps(event)) elif event["type"] == "market": - + if event["player_id"] == game.game_state()["current_player"]: game.market() - for i, socket_id in enumerate(gameConnections.connections, start=1): - event = { - "type": "play", - "player_id": i, - "game_state": serialize_game_view(game.view(f"player_{i}")) - } - - await gameConnections.send(socket_id, event) - - if socket_id == game.current_player.player_id: - # Notify the player that they are suspended - event = { - "type": "message", - "message": "Your turn to play." - } - - await gameConnections.send(socket_id, event) + message = Message( + receiver_ids=[game.current_player.player_id], + content="Your turn to play." + ) + + await send_event_to_all("play", game, gameConnections, message) elif event["type"] == "request": @@ -355,13 +313,13 @@ async def play(websocket: ClientConnection, game: Whot, player_id: str, gameConn card = str(game.request(suit)['requested_suit']) - for i, socket_id in enumerate(gameConnections.connections, start=1): + for socket_id in gameConnections.connections: if socket_id != requester: event = { "type": "request_card", "message": f"{requester} requested for {card}", - "game_state": serialize_game_view(game.view(f"player_{i}")) + "game_state": serialize_game_view(game.view(socket_id)) } await gameConnections.send(socket_id, event) @@ -377,6 +335,10 @@ async def join(websocket: ClientConnection, join_key): await gameConnection.broadcast(event) + player_id = gameConnection.add_connection(websocket) + + await play(websocket, gameConnection.game, player_id, gameConnection) + except KeyError: await websocket.send(json.dumps({ "type": "message", @@ -384,9 +346,6 @@ async def join(websocket: ClientConnection, join_key): })) await websocket.close() - player_id = gameConnection.add_connection(websocket) - - await play(websocket, gameConnection.game, player_id, gameConnection) async def start(websocket: ClientConnection): From 99e0ada199afc6371e4582a34b86e03382daf22c Mon Sep 17 00:00:00 2001 From: Eteimz <8teims@gmail.com> Date: Fri, 25 Jul 2025 04:25:12 +0100 Subject: [PATCH 3/6] Made server code class based --- examples/whot-web/main.py | 6 +- examples/whot-web/server.py | 506 +++++++++++++++++------------------- 2 files changed, 243 insertions(+), 269 deletions(-) diff --git a/examples/whot-web/main.py b/examples/whot-web/main.py index e667dbc..f179168 100644 --- a/examples/whot-web/main.py +++ b/examples/whot-web/main.py @@ -7,7 +7,7 @@ import jinja2 from websockets.asyncio.server import serve -from server import handler +from server import WhotServer load_dotenv() @@ -17,6 +17,7 @@ # Create an aiohttp web app app = web.Application() +whot_server = WhotServer() aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('.')) @@ -29,6 +30,9 @@ async def handle_game(request): "websocket_url": f"ws://{ADDRESS}:{WEBSOCKET_PORT}" }) +async def handler(websocket): + await whot_server.handle(websocket) + # WebSocket server function async def websocket_server(): async with serve(handler, ADDRESS, WEBSOCKET_PORT): diff --git a/examples/whot-web/server.py b/examples/whot-web/server.py index e900b73..616afe7 100644 --- a/examples/whot-web/server.py +++ b/examples/whot-web/server.py @@ -16,17 +16,6 @@ from dataclasses import dataclass -# Todo: -# Consider adding everything to a class -# Work on individual messages in the play event. Done -# Work on disconnections so users can that get disconnected can always reconnect -# The game ends when all players have left, Add option for restart -# Refactor code Done - -# - -JOIN = {} - class GameConnection: """ @@ -48,19 +37,21 @@ def add_connection(self, connection: ClientConnection) -> str: else: return False - async def send(self, socket_id, event): + async def send(self, socket_id, server_event): """ sends message to a particular websocket client """ - await self.connections[socket_id].send(json.dumps(event)) + await self.connections[socket_id].send(json.dumps(server_event)) - async def broadcast(self, event, exceptions = None): + async def broadcast(self, server_event, exceptions = None): """ sends message to all websocket client """ - websockets.broadcast(self.connections.values(), json.dumps(event)) # type: ignore + # Todo: add exceptions in broadcast + + websockets.broadcast(self.connections.values(), json.dumps(server_event)) @dataclass class Message: @@ -68,315 +59,294 @@ class Message: content: str class WhotServer: - async def play(self): - pass - - async def handler(self): - pass - - async def start(self): - pass - - async def join(self): - pass - - async def send_event_to_all(self): - pass - - async def send_event_to_one(self): - pass - -async def send_event_to_all( type, game: Whot, gameConnections: GameConnection, message: Message | None = None): - for socket_id in gameConnections.connections: - event = { - "type": type, - "player_id": socket_id, - "game_state": serialize_game_view(game.view(socket_id)) - } - - await gameConnections.send(socket_id, event) - if message != None: - if socket_id in message.receiver_ids: - # Notify the player to pick two cards + def __init__(self): + self.game = Whot() + self.gameConnection = GameConnection(self.game) + self.JOIN = {} + + async def start(self, websocket: ClientConnection): - event = { - "type": "message", - "message": message.content - } - - await gameConnections.send(socket_id, event) + player_id = self.gameConnection.add_connection(websocket) -async def send_event_to_one(socket_id, type, game: Whot, gameConnections: GameConnection): - event = { - "type": type, - "player_id": socket_id, - "game_state": serialize_game_view(game.view(socket_id)) - } + join_key = secrets.token_urlsafe(4) + self.JOIN[join_key] = self.gameConnection - await gameConnections.send(socket_id, event) + try: + server_event = { + "type": "init", + "join": join_key + } + await websocket.send(json.dumps(server_event)) + await self.play(websocket, self.game, player_id, self.gameConnection) + finally: + del self.JOIN[join_key] + async def join(self, websocket: ClientConnection, join_key): + try: + self.gameConnection: GameConnection = self.JOIN[join_key] + + server_event = { + "type": "message", + "message": "New Player Joined" + } + + player_id = self.gameConnection.add_connection(websocket) -async def play(websocket: ClientConnection, game: Whot, player_id: str, gameConnections: GameConnection): - event = { - "type": "player_id", - "player_id": player_id, - } + await self.gameConnection.broadcast(server_event) + + await self.play(websocket, self.gameConnection.game, player_id, self.gameConnection) + + except KeyError: + await websocket.send(json.dumps({ + "type": "message", + "message": "Invalid join key" + })) + await websocket.close() + + async def play(self, websocket: ClientConnection, game: Whot, player_id: str, gameConnections: GameConnection): + server_event = { + "type": "player_id", + "player_id": player_id, + } - await websocket.send(json.dumps(event)) + await websocket.send(json.dumps(server_event)) - while gameConnections.num_of_connections < len(game.players): - await asyncio.sleep(1) + while gameConnections.num_of_connections < len(game.players): + await asyncio.sleep(1) - game.start_game() + game.start_game() - await send_event_to_all("play", game, gameConnections) + await self.send_event_to_all("play") - if game.request_mode == True: - socket_id = game.current_player.player_id - await send_event_to_one(socket_id, "request", game, gameConnections) - - async for message in websocket: + if game.request_mode == True: + socket_id = game.current_player.player_id + await self.send_event_to_one(socket_id, "request") - event = json.loads(message) + async for message in websocket: + + client_event = json.loads(message) - if event["type"] == "play": - if event["player_id"] == game.game_state()["current_player"]: + if client_event["type"] == "play": + if client_event["player_id"] == game.game_state()["current_player"]: - card_index = int(event["card"]) + card_index = int(client_event["card"]) - try: - result = game.play(card_index) - - if result["status"] == True and result["type"] != "request": + try: + result = game.play(card_index) + + if result["status"] == True and result["type"] != "request": - if result["type"] == "pick_2": + if result["type"] == "pick_2": - message = Message( - receiver_ids=[game.current_player.player_id], - content="Pick two! You have been asked to pick two cards.") - - await send_event_to_all(result["type"], game, gameConnections, message) + message = Message( + receiver_ids=[game.current_player.player_id], + content="Pick two! You have been asked to pick two cards.") + + await self.send_event_to_all(result["type"], message) - elif result["type"] == "general_market": + elif result["type"] == "general_market": - other_players = list(gameConnections.connections.keys()) - other_players.remove(game.current_player.player_id) - - message = Message( - receiver_ids=other_players, - content="Everyone Go gen.") + other_players = list(gameConnections.connections.keys()) + other_players.remove(game.current_player.player_id) - await send_event_to_all(result["type"], game, gameConnections, message) - - elif result["type"] == "suspension": - try: - current_player_index = game.players.index(game.current_player) - 1 - suspended_player_id = game.players[current_player_index].player_id - except IndexError: - current_player_index = len(game.players) - 1 - suspended_player_id = game.players[current_player_index].player_id - - message = Message( - receiver_ids=[suspended_player_id], - content="You have been suspended.") - - await send_event_to_all(result["type"], game, gameConnections, message) - - elif result["type"] == "hold_on": - try: - current_player_index = game.players.index(game.current_player) + 1 - on_hold_player_id = game.players[current_player_index].player_id - except IndexError: - current_player_index = 0 - on_hold_player_id = game.players[current_player_index].player_id - - message = Message( - receiver_ids=[on_hold_player_id], - content="You have been placed on hold.") + message = Message( + receiver_ids=other_players, + content="Everyone Go gen.") + + await self.send_event_to_all(result["type"], message) - await send_event_to_all(result["type"], game, gameConnections, message) + elif result["type"] == "suspension": + try: + current_player_index = game.players.index(game.current_player) - 1 + suspended_player_id = game.players[current_player_index].player_id + except IndexError: + current_player_index = len(game.players) - 1 + suspended_player_id = game.players[current_player_index].player_id + + message = Message( + receiver_ids=[suspended_player_id], + content="You have been suspended.") + + await self.send_event_to_all(result["type"], message) + + elif result["type"] == "hold_on": + try: + current_player_index = game.players.index(game.current_player) + 1 + on_hold_player_id = game.players[current_player_index].player_id + except IndexError: + current_player_index = 0 + on_hold_player_id = game.players[current_player_index].player_id + + message = Message( + receiver_ids=[on_hold_player_id], + content="You have been placed on hold.") + + await self.send_event_to_all(result["type"], message) - else: + else: - message = Message( - receiver_ids=[game.current_player.player_id], - content="Your turn to play.") - - await send_event_to_all(result["type"], game, gameConnections, message) - - elif result['type'] == "request": + message = Message( + receiver_ids=[game.current_player.player_id], + content="Your turn to play.") + + await self.send_event_to_all(result["type"], message) + + elif result['type'] == "request": - current_player = game.current_player.player_id + current_player = game.current_player.player_id - for socket_id in gameConnections.connections: + for socket_id in gameConnections.connections: - if socket_id == current_player: + if socket_id == current_player: + + server_event = { + "type": "request", + "player_id": socket_id, + "game_state": serialize_game_view(game.view(socket_id)) + } + + else: + + server_event = { + "type": "play", + "player_id": socket_id, + "game_state": serialize_game_view(game.view(socket_id)) + } - event = { - "type": "request", - "player_id": socket_id, - "game_state": serialize_game_view(game.view(socket_id)) - } + await gameConnections.send(socket_id, server_event) - else: + elif result['status'] == False: - event = { - "type": "play", - "player_id": socket_id, - "game_state": serialize_game_view(game.view(socket_id)) - } + await self.send_event_to_all(result["type"]) - await gameConnections.send(socket_id, event) - - elif result['status'] == False: - - await send_event_to_all(result["type"], game, gameConnections) + server_event = { + "type": "win", + "winner": result['player_id'], + } + game.save("game.json") # Todo: This could be improved + + await gameConnections.broadcast(server_event) - event = { - "type": "win", - "winner": result['player_id'], + except GameNotStartedError: + server_event = { + "type": "message", + "message": "Game has not started yet" } - game.save("game.json") # Todo: This could be improved - - await gameConnections.broadcast(event) - - except GameNotStartedError: - event = { - "type": "message", - "message": "Game has not started yet" - } - await websocket.send(json.dumps(event)) + await websocket.send(json.dumps(server_event)) - except GameOverError: - event = { - "type": "message", - "message": "Game is over" - } - await websocket.send(json.dumps(event)) + except GameOverError: + server_event = { + "type": "message", + "message": "Game is over" + } + await websocket.send(json.dumps(server_event)) - except InvalidMoveError: - event = { - "type": "message", - "message": "Invalid move" - } - await websocket.send(json.dumps(event)) + except InvalidMoveError: + server_event = { + "type": "message", + "message": "Invalid move" + } + await websocket.send(json.dumps(server_event)) - except InvalidCardError: - event = { - "type": "message", - "message": "Invalid card" - } - await websocket.send(json.dumps(event)) + except InvalidCardError: + server_event = { + "type": "message", + "message": "Invalid card" + } + await websocket.send(json.dumps(server_event)) - except InvalidSuitError: - event = { - "type": "message", - "message": "Invalid suit" - } - await websocket.send(json.dumps(event)) + except InvalidSuitError: + server_event = { + "type": "message", + "message": "Invalid suit" + } + await websocket.send(json.dumps(server_event)) + + except Exception as e: + server_event = { + "type": "message", + "message": f"An error occurred: {str(e)}" + } + await websocket.send(json.dumps(server_event)) - except Exception as e: - event = { + else: + server_event = { "type": "message", - "message": f"An error occurred: {str(e)}" + "message":"It is not your turn" } - await websocket.send(json.dumps(event)) + + await websocket.send(json.dumps(server_event)) - else: - event = { - "type": "message", - "message":"It is not your turn" - } + elif client_event["type"] == "market": - await websocket.send(json.dumps(event)) + if client_event["player_id"] == game.game_state()["current_player"]: - elif event["type"] == "market": - - if event["player_id"] == game.game_state()["current_player"]: + game.market() - game.market() + message = Message( + receiver_ids=[game.current_player.player_id], + content="Your turn to play." + ) + + await self.send_event_to_all("play", message) - message = Message( - receiver_ids=[game.current_player.player_id], - content="Your turn to play." - ) - - await send_event_to_all("play", game, gameConnections, message) - + elif client_event["type"] == "request": + suit = client_event["suit"] - elif event["type"] == "request": - suit = event["suit"] + requester = game.game_state()['current_player'] - requester = game.game_state()['current_player'] + card = str(game.request(suit)['requested_suit']) - card = str(game.request(suit)['requested_suit']) + for socket_id in gameConnections.connections: - for socket_id in gameConnections.connections: + if socket_id != requester: + server_event = { + "type": "request_card", + "message": f"{requester} requested for {card}", + "game_state": serialize_game_view(game.view(socket_id)) + } - if socket_id != requester: - event = { - "type": "request_card", - "message": f"{requester} requested for {card}", - "game_state": serialize_game_view(game.view(socket_id)) - } + await gameConnections.send(socket_id, server_event) - await gameConnections.send(socket_id, event) + async def send_event_to_all(self, type, message: Message | None = None): + for socket_id in self.gameConnection.connections: + server_event = { + "type": type, + "player_id": socket_id, + "game_state": serialize_game_view(self.game.view(socket_id)) + } -async def join(websocket: ClientConnection, join_key): - try: - gameConnection: GameConnection = JOIN[join_key] - - event = { - "type": "message", - "message": "New Player Joined" - } - - await gameConnection.broadcast(event) - - player_id = gameConnection.add_connection(websocket) - - await play(websocket, gameConnection.game, player_id, gameConnection) - - except KeyError: - await websocket.send(json.dumps({ - "type": "message", - "message": "Invalid join key" - })) - await websocket.close() + await self.gameConnection.send(socket_id, server_event) + if message != None: + if socket_id in message.receiver_ids: + # Notify the player to pick two cards -async def start(websocket: ClientConnection): + server_event = { + "type": "message", + "message": message.content + } + + await self.gameConnection.send(socket_id, server_event) - # These values would defined by the client later - num_of_player = 2 - num_of_cards = 4 + async def send_event_to_one(self, socket_id, type): + server_event = { + "type": type, + "player_id": socket_id, + "game_state": serialize_game_view(self.game.view(socket_id)) + } - game = Whot(num_of_player, num_of_cards) - - gameConnection = GameConnection(game) - player_id = gameConnection.add_connection(websocket) + await self.gameConnection.send(socket_id, server_event) - join_key = secrets.token_urlsafe(4) - JOIN[join_key] = gameConnection + async def handle(self, websocket: ClientConnection): + message = await websocket.recv() + client_event = json.loads(message) - try: - event = { - "type": "init", - "join": join_key - } + assert client_event["type"] == "init" - await websocket.send(json.dumps(event)) - await play(websocket, game, player_id, gameConnection) - finally: - del JOIN[join_key] - -async def handler(websocket: ClientConnection): - message = await websocket.recv() - event = json.loads(message) - assert event["type"] == "init" - if "join" in event: - await join(websocket, event["join"]) - else: - await start(websocket) \ No newline at end of file + if "join" in client_event: + await self.join(websocket, client_event["join"]) + else: + await self.start(websocket) \ No newline at end of file From 0a034058adb5cbee48e2963c004d66e2c5a8ede1 Mon Sep 17 00:00:00 2001 From: Eteimz <8teims@gmail.com> Date: Fri, 25 Jul 2025 05:06:29 +0100 Subject: [PATCH 4/6] Added updates to whot server --- .github/workflows/python-tests.yml | 2 +- examples/whot-web/main.py | 1 + examples/whot-web/server.py | 140 +++++++++++++++-------------- whot/agent.py | 9 ++ 4 files changed, 86 insertions(+), 66 deletions(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index a88874d..5e652d1 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -1,4 +1,4 @@ -name: Python package +name: WHOT Tests on: push: diff --git a/examples/whot-web/main.py b/examples/whot-web/main.py index f179168..4e9d542 100644 --- a/examples/whot-web/main.py +++ b/examples/whot-web/main.py @@ -19,6 +19,7 @@ app = web.Application() whot_server = WhotServer() + aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('.')) # Serve index.html diff --git a/examples/whot-web/server.py b/examples/whot-web/server.py index 616afe7..2fd9fec 100644 --- a/examples/whot-web/server.py +++ b/examples/whot-web/server.py @@ -60,12 +60,11 @@ class Message: class WhotServer: - def __init__(self): + async def start(self, websocket: ClientConnection): + self.game = Whot() self.gameConnection = GameConnection(self.game) self.JOIN = {} - - async def start(self, websocket: ClientConnection): player_id = self.gameConnection.add_connection(websocket) @@ -139,54 +138,18 @@ async def play(self, websocket: ClientConnection, game: Whot, player_id: str, ga if result["status"] == True and result["type"] != "request": if result["type"] == "pick_2": - - message = Message( - receiver_ids=[game.current_player.player_id], - content="Pick two! You have been asked to pick two cards.") - - await self.send_event_to_all(result["type"], message) + await self.handle_pick_two() elif result["type"] == "general_market": - - other_players = list(gameConnections.connections.keys()) - other_players.remove(game.current_player.player_id) - - message = Message( - receiver_ids=other_players, - content="Everyone Go gen.") - - await self.send_event_to_all(result["type"], message) + await self.handle_general_market() elif result["type"] == "suspension": - try: - current_player_index = game.players.index(game.current_player) - 1 - suspended_player_id = game.players[current_player_index].player_id - except IndexError: - current_player_index = len(game.players) - 1 - suspended_player_id = game.players[current_player_index].player_id - - message = Message( - receiver_ids=[suspended_player_id], - content="You have been suspended.") - - await self.send_event_to_all(result["type"], message) + await self.handle_suspension() elif result["type"] == "hold_on": - try: - current_player_index = game.players.index(game.current_player) + 1 - on_hold_player_id = game.players[current_player_index].player_id - except IndexError: - current_player_index = 0 - on_hold_player_id = game.players[current_player_index].player_id - - message = Message( - receiver_ids=[on_hold_player_id], - content="You have been placed on hold.") - - await self.send_event_to_all(result["type"], message) + await self.handle_hold_on() else: - message = Message( receiver_ids=[game.current_player.player_id], content="Your turn to play.") @@ -194,28 +157,7 @@ async def play(self, websocket: ClientConnection, game: Whot, player_id: str, ga await self.send_event_to_all(result["type"], message) elif result['type'] == "request": - - current_player = game.current_player.player_id - - for socket_id in gameConnections.connections: - - if socket_id == current_player: - - server_event = { - "type": "request", - "player_id": socket_id, - "game_state": serialize_game_view(game.view(socket_id)) - } - - else: - - server_event = { - "type": "play", - "player_id": socket_id, - "game_state": serialize_game_view(game.view(socket_id)) - } - - await gameConnections.send(socket_id, server_event) + await self.handle_request() elif result['status'] == False: @@ -310,6 +252,74 @@ async def play(self, websocket: ClientConnection, game: Whot, player_id: str, ga await gameConnections.send(socket_id, server_event) + async def handle_pick_two(self): + message = Message( + receiver_ids=[self.game.current_player.player_id], + content="Pick two! You have been asked to pick two cards.") + + await self.send_event_to_all("pick_2", message) + + async def handle_general_market(self): + other_players = list(self.gameConnection.connections.keys()) + other_players.remove(self.game.current_player.player_id) + + message = Message( + receiver_ids=other_players, + content="Everyone Go gen.") + + await self.send_event_to_all("general_market", message) + + async def handle_suspension(self): + try: + current_player_index = self.game.players.index(self.game.current_player) - 1 + suspended_player_id = self.game.players[current_player_index].player_id + except IndexError: + current_player_index = len(self.game.players) - 1 + suspended_player_id = self.game.players[current_player_index].player_id + + message = Message( + receiver_ids=[suspended_player_id], + content="You have been suspended.") + + await self.send_event_to_all("suspension", message) + + async def handle_hold_on(self): + try: + current_player_index = self.game.players.index(self.game.current_player) + 1 + on_hold_player_id = self.game.players[current_player_index].player_id + except IndexError: + current_player_index = 0 + on_hold_player_id = self.game.players[current_player_index].player_id + + message = Message( + receiver_ids=[on_hold_player_id], + content="You have been placed on hold.") + + await self.send_event_to_all("hold_on", message) + + async def handle_request(self): + current_player = self.game.current_player.player_id + + for socket_id in self.gameConnection.connections: + + if socket_id == current_player: + + server_event = { + "type": "request", + "player_id": socket_id, + "game_state": serialize_game_view(self.game.view(socket_id)) + } + + else: + + server_event = { + "type": "play", + "player_id": socket_id, + "game_state": serialize_game_view(self.game.view(socket_id)) + } + + await self.gameConnection.send(socket_id, server_event) + async def send_event_to_all(self, type, message: Message | None = None): for socket_id in self.gameConnection.connections: server_event = { diff --git a/whot/agent.py b/whot/agent.py index a2d0cd9..9231a4b 100644 --- a/whot/agent.py +++ b/whot/agent.py @@ -4,6 +4,15 @@ from .game import Engine from .deck import Suit +""" +I want don't want the agents to be using the game engine internally. +I want them to use the game engine externally. +So A game starts +the agent calls the engine.play() method +It would would use the game state to determine what to do next. +If it see it is its turn, it will play a card. +""" + class BaseAgent(ABC): def __init__(self, agent_id, engine): From 42fe70daa343de70fc2e8a37d00893f182cf5536 Mon Sep 17 00:00:00 2001 From: Eteimz <8teims@gmail.com> Date: Fri, 25 Jul 2025 05:39:02 +0100 Subject: [PATCH 5/6] Fixed connection error --- examples/whot-web/server.py | 116 ++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 57 deletions(-) diff --git a/examples/whot-web/server.py b/examples/whot-web/server.py index 2fd9fec..46bc9b4 100644 --- a/examples/whot-web/server.py +++ b/examples/whot-web/server.py @@ -60,16 +60,18 @@ class Message: class WhotServer: + def __init__(self): + self.JOIN = {} + async def start(self, websocket: ClientConnection): - self.game = Whot() - self.gameConnection = GameConnection(self.game) - self.JOIN = {} + game = Whot() + gameConnection = GameConnection(game) - player_id = self.gameConnection.add_connection(websocket) + player_id = gameConnection.add_connection(websocket) join_key = secrets.token_urlsafe(4) - self.JOIN[join_key] = self.gameConnection + self.JOIN[join_key] = gameConnection try: server_event = { @@ -78,24 +80,24 @@ async def start(self, websocket: ClientConnection): } await websocket.send(json.dumps(server_event)) - await self.play(websocket, self.game, player_id, self.gameConnection) + await self.play(websocket, game, player_id, gameConnection) finally: del self.JOIN[join_key] async def join(self, websocket: ClientConnection, join_key): try: - self.gameConnection: GameConnection = self.JOIN[join_key] + gameConnection: GameConnection = self.JOIN[join_key] server_event = { "type": "message", "message": "New Player Joined" } - player_id = self.gameConnection.add_connection(websocket) + player_id = gameConnection.add_connection(websocket) - await self.gameConnection.broadcast(server_event) + await gameConnection.broadcast(server_event) - await self.play(websocket, self.gameConnection.game, player_id, self.gameConnection) + await self.play(websocket, gameConnection.game, player_id, gameConnection) except KeyError: await websocket.send(json.dumps({ @@ -104,7 +106,7 @@ async def join(self, websocket: ClientConnection, join_key): })) await websocket.close() - async def play(self, websocket: ClientConnection, game: Whot, player_id: str, gameConnections: GameConnection): + async def play(self, websocket: ClientConnection, game: Whot, player_id: str, gameConnection: GameConnection): server_event = { "type": "player_id", "player_id": player_id, @@ -112,16 +114,16 @@ async def play(self, websocket: ClientConnection, game: Whot, player_id: str, ga await websocket.send(json.dumps(server_event)) - while gameConnections.num_of_connections < len(game.players): + while gameConnection.num_of_connections < len(game.players): await asyncio.sleep(1) game.start_game() - await self.send_event_to_all("play") + await self.send_event_to_all("play", game, gameConnection) if game.request_mode == True: socket_id = game.current_player.player_id - await self.send_event_to_one(socket_id, "request") + await self.send_event_to_one(socket_id, "request", game, gameConnection) async for message in websocket: @@ -138,30 +140,30 @@ async def play(self, websocket: ClientConnection, game: Whot, player_id: str, ga if result["status"] == True and result["type"] != "request": if result["type"] == "pick_2": - await self.handle_pick_two() + await self.handle_pick_two(game, gameConnection) elif result["type"] == "general_market": - await self.handle_general_market() + await self.handle_general_market(game, gameConnection) elif result["type"] == "suspension": - await self.handle_suspension() + await self.handle_suspension(game, gameConnection) elif result["type"] == "hold_on": - await self.handle_hold_on() + await self.handle_hold_on(game, gameConnection) else: message = Message( receiver_ids=[game.current_player.player_id], content="Your turn to play.") - await self.send_event_to_all(result["type"], message) + await self.send_event_to_all(result["type"], game, gameConnection, message) elif result['type'] == "request": - await self.handle_request() + await self.handle_request(game, gameConnection) elif result['status'] == False: - await self.send_event_to_all(result["type"]) + await self.send_event_to_all(result["type"], game, gameConnection) server_event = { "type": "win", @@ -169,7 +171,7 @@ async def play(self, websocket: ClientConnection, game: Whot, player_id: str, ga } game.save("game.json") # Todo: This could be improved - await gameConnections.broadcast(server_event) + await gameConnection.broadcast(server_event) except GameNotStartedError: server_event = { @@ -232,7 +234,7 @@ async def play(self, websocket: ClientConnection, game: Whot, player_id: str, ga content="Your turn to play." ) - await self.send_event_to_all("play", message) + await self.send_event_to_all("play", game, gameConnection, message) elif client_event["type"] == "request": suit = client_event["suit"] @@ -241,7 +243,7 @@ async def play(self, websocket: ClientConnection, game: Whot, player_id: str, ga card = str(game.request(suit)['requested_suit']) - for socket_id in gameConnections.connections: + for socket_id in gameConnection.connections: if socket_id != requester: server_event = { @@ -250,64 +252,64 @@ async def play(self, websocket: ClientConnection, game: Whot, player_id: str, ga "game_state": serialize_game_view(game.view(socket_id)) } - await gameConnections.send(socket_id, server_event) + await gameConnection.send(socket_id, server_event) - async def handle_pick_two(self): + async def handle_pick_two(self, game: Whot, gameConnection: GameConnection): message = Message( - receiver_ids=[self.game.current_player.player_id], + receiver_ids=[game.current_player.player_id], content="Pick two! You have been asked to pick two cards.") - await self.send_event_to_all("pick_2", message) + await self.send_event_to_all("pick_2", game, gameConnection, message) - async def handle_general_market(self): - other_players = list(self.gameConnection.connections.keys()) - other_players.remove(self.game.current_player.player_id) + async def handle_general_market(self, game: Whot, gameConnection: GameConnection): + other_players = list(gameConnection.connections.keys()) + other_players.remove(game.current_player.player_id) message = Message( receiver_ids=other_players, content="Everyone Go gen.") - await self.send_event_to_all("general_market", message) + await self.send_event_to_all("general_market", game, gameConnection, message) - async def handle_suspension(self): + async def handle_suspension(self, game: Whot, gameConnection: GameConnection): try: - current_player_index = self.game.players.index(self.game.current_player) - 1 - suspended_player_id = self.game.players[current_player_index].player_id + current_player_index = game.players.index(game.current_player) - 1 + suspended_player_id = game.players[current_player_index].player_id except IndexError: - current_player_index = len(self.game.players) - 1 - suspended_player_id = self.game.players[current_player_index].player_id + current_player_index = len(game.players) - 1 + suspended_player_id = game.players[current_player_index].player_id message = Message( receiver_ids=[suspended_player_id], content="You have been suspended.") - await self.send_event_to_all("suspension", message) + await self.send_event_to_all("suspension", game, gameConnection, message) - async def handle_hold_on(self): + async def handle_hold_on(self, game: Whot, gameConnection: GameConnection): try: - current_player_index = self.game.players.index(self.game.current_player) + 1 - on_hold_player_id = self.game.players[current_player_index].player_id + current_player_index = game.players.index(game.current_player) + 1 + on_hold_player_id = game.players[current_player_index].player_id except IndexError: current_player_index = 0 - on_hold_player_id = self.game.players[current_player_index].player_id + on_hold_player_id = game.players[current_player_index].player_id message = Message( receiver_ids=[on_hold_player_id], content="You have been placed on hold.") - await self.send_event_to_all("hold_on", message) + await self.send_event_to_all("hold_on", game, gameConnection, message) - async def handle_request(self): - current_player = self.game.current_player.player_id + async def handle_request(self, game: Whot, gameConnection: GameConnection): + current_player = game.current_player.player_id - for socket_id in self.gameConnection.connections: + for socket_id in gameConnection.connections: if socket_id == current_player: server_event = { "type": "request", "player_id": socket_id, - "game_state": serialize_game_view(self.game.view(socket_id)) + "game_state": serialize_game_view(game.view(socket_id)) } else: @@ -315,20 +317,20 @@ async def handle_request(self): server_event = { "type": "play", "player_id": socket_id, - "game_state": serialize_game_view(self.game.view(socket_id)) + "game_state": serialize_game_view(game.view(socket_id)) } - await self.gameConnection.send(socket_id, server_event) + await gameConnection.send(socket_id, server_event) - async def send_event_to_all(self, type, message: Message | None = None): - for socket_id in self.gameConnection.connections: + async def send_event_to_all(self, type, game: Whot, gameConnection: GameConnection, message: Message | None = None): + for socket_id in gameConnection.connections: server_event = { "type": type, "player_id": socket_id, - "game_state": serialize_game_view(self.game.view(socket_id)) + "game_state": serialize_game_view(game.view(socket_id)) } - await self.gameConnection.send(socket_id, server_event) + await gameConnection.send(socket_id, server_event) if message != None: if socket_id in message.receiver_ids: @@ -339,16 +341,16 @@ async def send_event_to_all(self, type, message: Message | None = None): "message": message.content } - await self.gameConnection.send(socket_id, server_event) + await gameConnection.send(socket_id, server_event) - async def send_event_to_one(self, socket_id, type): + async def send_event_to_one(self, socket_id, type, game: Whot, gameConnection: GameConnection): server_event = { "type": type, "player_id": socket_id, - "game_state": serialize_game_view(self.game.view(socket_id)) + "game_state": serialize_game_view(game.view(socket_id)) } - await self.gameConnection.send(socket_id, server_event) + await gameConnection.send(socket_id, server_event) async def handle(self, websocket: ClientConnection): message = await websocket.recv() From da8769ef610f3f7b6d0875ffb282c0bad8aaf0bb Mon Sep 17 00:00:00 2001 From: Eteimz <8teims@gmail.com> Date: Thu, 31 Jul 2025 05:40:57 +0100 Subject: [PATCH 6/6] Final Improvements --- examples/whot-web/.env-example | 3 + examples/whot-web/README.md | 78 ++++++ examples/whot-web/game.html | 47 ---- examples/whot-web/html/game.html | 61 +++++ examples/whot-web/html/index.html | 48 ++++ examples/whot-web/index.html | 40 --- examples/whot-web/main.py | 9 +- examples/whot-web/server.py | 45 ++-- .../whot-web/{ => static}/assets/cards.png | Bin .../whot-web/{ => static}/assets/gameplay.png | Bin .../{ => static}/assets/images/20_whot.png | Bin .../assets/images/circle/10_circle.png | Bin .../assets/images/circle/11_circle.png | Bin .../assets/images/circle/12_circle.png | Bin .../assets/images/circle/13_circle.png | Bin .../assets/images/circle/14_circle.png | Bin .../assets/images/circle/1_circle.png | Bin .../assets/images/circle/2_circle.png | Bin .../assets/images/circle/3_circle.png | Bin .../assets/images/circle/4_circle.png | Bin .../assets/images/circle/5_circle.png | Bin .../assets/images/circle/7_circle.png | Bin .../assets/images/circle/8_circle.png | Bin .../assets/images/cross/10_cross.png | Bin .../assets/images/cross/11_cross.png | Bin .../assets/images/cross/13_cross.png | Bin .../assets/images/cross/14_cross.png | Bin .../assets/images/cross/1_cross.png | Bin .../assets/images/cross/2_cross.png | Bin .../assets/images/cross/3_cross.png | Bin .../assets/images/cross/5_cross.png | Bin .../assets/images/cross/7_cross.png | Bin .../assets/images/square/10_square.png | Bin .../assets/images/square/11_square.png | Bin .../assets/images/square/13_square.png | Bin .../assets/images/square/14_square.png | Bin .../assets/images/square/1_square.png | Bin .../assets/images/square/2_square.png | Bin .../assets/images/square/3_square.png | Bin .../assets/images/square/5_square.png | Bin .../assets/images/square/7_square.png | Bin .../assets/images/star/1_star.png | Bin .../assets/images/star/2_star.png | Bin .../assets/images/star/3_star.png | Bin .../assets/images/star/4_star.png | Bin .../assets/images/star/5_star.png | Bin .../assets/images/star/7_star.png | Bin .../assets/images/star/8_star.png | Bin .../assets/images/triangle/10_triangle.png | Bin .../assets/images/triangle/11_triangle.png | Bin .../assets/images/triangle/12_triangle.png | Bin .../assets/images/triangle/13_triangle.png | Bin .../assets/images/triangle/14_triangle.png | Bin .../assets/images/triangle/1_triangle.png | Bin .../assets/images/triangle/2_triangle.png | Bin .../assets/images/triangle/3_triangle.png | Bin .../assets/images/triangle/4_triangle.png | Bin .../assets/images/triangle/5_triangle.png | Bin .../assets/images/triangle/7_triangle.png | Bin .../assets/images/triangle/8_triangle.png | Bin .../{ => static}/assets/images/whot_back.png | Bin .../{ => static}/assets/svg/20_whot.svg | 0 .../assets/svg/circle/10_circle.svg | 0 .../assets/svg/circle/11_circle.svg | 0 .../assets/svg/circle/12_circle.svg | 0 .../assets/svg/circle/13_circle.svg | 0 .../assets/svg/circle/14_circle.svg | 0 .../assets/svg/circle/1_circle.svg | 0 .../assets/svg/circle/2_circle.svg | 0 .../assets/svg/circle/3_circle.svg | 0 .../assets/svg/circle/4_circle.svg | 0 .../assets/svg/circle/5_circle.svg | 0 .../assets/svg/circle/7_circle.svg | 0 .../assets/svg/circle/8_circle.svg | 0 .../assets/svg/cross/10_cross.svg | 0 .../assets/svg/cross/11_cross.svg | 0 .../assets/svg/cross/13_cross.svg | 0 .../assets/svg/cross/14_cross.svg | 0 .../{ => static}/assets/svg/cross/1_cross.svg | 0 .../{ => static}/assets/svg/cross/2_cross.svg | 0 .../{ => static}/assets/svg/cross/3_cross.svg | 0 .../{ => static}/assets/svg/cross/5_cross.svg | 0 .../{ => static}/assets/svg/cross/7_cross.svg | 0 .../assets/svg/square/10_square.svg | 0 .../assets/svg/square/11_square.svg | 0 .../assets/svg/square/13_square.svg | 0 .../assets/svg/square/14_square.svg | 0 .../assets/svg/square/1_square.svg | 0 .../assets/svg/square/2_square.svg | 0 .../assets/svg/square/3_square.svg | 0 .../assets/svg/square/5_square.svg | 0 .../assets/svg/square/7_square.svg | 0 .../{ => static}/assets/svg/star/1_star.svg | 0 .../{ => static}/assets/svg/star/2_star.svg | 0 .../{ => static}/assets/svg/star/3_star.svg | 0 .../{ => static}/assets/svg/star/4_star.svg | 0 .../{ => static}/assets/svg/star/5_star.svg | 0 .../{ => static}/assets/svg/star/7_star.svg | 0 .../{ => static}/assets/svg/star/8_star.svg | 0 .../assets/svg/triangle/10_triangle.svg | 0 .../assets/svg/triangle/11_triangle.svg | 0 .../assets/svg/triangle/12_triangle.svg | 0 .../assets/svg/triangle/13_triangle.svg | 0 .../assets/svg/triangle/14_triangle.svg | 0 .../assets/svg/triangle/1_triangle.svg | 0 .../assets/svg/triangle/2_triangle.svg | 0 .../assets/svg/triangle/3_triangle.svg | 0 .../assets/svg/triangle/4_triangle.svg | 0 .../assets/svg/triangle/5_triangle.svg | 0 .../assets/svg/triangle/7_triangle.svg | 0 .../assets/svg/triangle/8_triangle.svg | 0 .../{ => static}/assets/svg/whot_back.svg | 0 .../{ => static/assets}/table_top.jpeg | Bin examples/whot-web/static/css/game.css | 229 ++++++++++++++++++ examples/whot-web/static/css/index.css | 85 +++++++ examples/whot-web/{ => static/js}/script.js | 182 ++++++-------- examples/whot-web/styles.css | 72 ------ 117 files changed, 607 insertions(+), 292 deletions(-) create mode 100644 examples/whot-web/.env-example create mode 100644 examples/whot-web/README.md delete mode 100644 examples/whot-web/game.html create mode 100644 examples/whot-web/html/game.html create mode 100644 examples/whot-web/html/index.html delete mode 100644 examples/whot-web/index.html rename examples/whot-web/{ => static}/assets/cards.png (100%) rename examples/whot-web/{ => static}/assets/gameplay.png (100%) rename examples/whot-web/{ => static}/assets/images/20_whot.png (100%) rename examples/whot-web/{ => static}/assets/images/circle/10_circle.png (100%) rename examples/whot-web/{ => static}/assets/images/circle/11_circle.png (100%) rename examples/whot-web/{ => static}/assets/images/circle/12_circle.png (100%) rename examples/whot-web/{ => static}/assets/images/circle/13_circle.png (100%) rename examples/whot-web/{ => static}/assets/images/circle/14_circle.png (100%) rename examples/whot-web/{ => static}/assets/images/circle/1_circle.png (100%) rename examples/whot-web/{ => static}/assets/images/circle/2_circle.png (100%) rename examples/whot-web/{ => static}/assets/images/circle/3_circle.png (100%) rename examples/whot-web/{ => static}/assets/images/circle/4_circle.png (100%) rename examples/whot-web/{ => static}/assets/images/circle/5_circle.png (100%) rename examples/whot-web/{ => static}/assets/images/circle/7_circle.png (100%) rename examples/whot-web/{ => static}/assets/images/circle/8_circle.png (100%) rename examples/whot-web/{ => static}/assets/images/cross/10_cross.png (100%) rename examples/whot-web/{ => static}/assets/images/cross/11_cross.png (100%) rename examples/whot-web/{ => static}/assets/images/cross/13_cross.png (100%) rename examples/whot-web/{ => static}/assets/images/cross/14_cross.png (100%) rename examples/whot-web/{ => static}/assets/images/cross/1_cross.png (100%) rename examples/whot-web/{ => static}/assets/images/cross/2_cross.png (100%) rename examples/whot-web/{ => static}/assets/images/cross/3_cross.png (100%) rename examples/whot-web/{ => static}/assets/images/cross/5_cross.png (100%) rename examples/whot-web/{ => static}/assets/images/cross/7_cross.png (100%) rename examples/whot-web/{ => static}/assets/images/square/10_square.png (100%) rename examples/whot-web/{ => static}/assets/images/square/11_square.png (100%) rename examples/whot-web/{ => static}/assets/images/square/13_square.png (100%) rename examples/whot-web/{ => static}/assets/images/square/14_square.png (100%) rename examples/whot-web/{ => static}/assets/images/square/1_square.png (100%) rename examples/whot-web/{ => static}/assets/images/square/2_square.png (100%) rename examples/whot-web/{ => static}/assets/images/square/3_square.png (100%) rename examples/whot-web/{ => static}/assets/images/square/5_square.png (100%) rename examples/whot-web/{ => static}/assets/images/square/7_square.png (100%) rename examples/whot-web/{ => static}/assets/images/star/1_star.png (100%) rename examples/whot-web/{ => static}/assets/images/star/2_star.png (100%) rename examples/whot-web/{ => static}/assets/images/star/3_star.png (100%) rename examples/whot-web/{ => static}/assets/images/star/4_star.png (100%) rename examples/whot-web/{ => static}/assets/images/star/5_star.png (100%) rename examples/whot-web/{ => static}/assets/images/star/7_star.png (100%) rename examples/whot-web/{ => static}/assets/images/star/8_star.png (100%) rename examples/whot-web/{ => static}/assets/images/triangle/10_triangle.png (100%) rename examples/whot-web/{ => static}/assets/images/triangle/11_triangle.png (100%) rename examples/whot-web/{ => static}/assets/images/triangle/12_triangle.png (100%) rename examples/whot-web/{ => static}/assets/images/triangle/13_triangle.png (100%) rename examples/whot-web/{ => static}/assets/images/triangle/14_triangle.png (100%) rename examples/whot-web/{ => static}/assets/images/triangle/1_triangle.png (100%) rename examples/whot-web/{ => static}/assets/images/triangle/2_triangle.png (100%) rename examples/whot-web/{ => static}/assets/images/triangle/3_triangle.png (100%) rename examples/whot-web/{ => static}/assets/images/triangle/4_triangle.png (100%) rename examples/whot-web/{ => static}/assets/images/triangle/5_triangle.png (100%) rename examples/whot-web/{ => static}/assets/images/triangle/7_triangle.png (100%) rename examples/whot-web/{ => static}/assets/images/triangle/8_triangle.png (100%) rename examples/whot-web/{ => static}/assets/images/whot_back.png (100%) rename examples/whot-web/{ => static}/assets/svg/20_whot.svg (100%) rename examples/whot-web/{ => static}/assets/svg/circle/10_circle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/circle/11_circle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/circle/12_circle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/circle/13_circle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/circle/14_circle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/circle/1_circle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/circle/2_circle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/circle/3_circle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/circle/4_circle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/circle/5_circle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/circle/7_circle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/circle/8_circle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/cross/10_cross.svg (100%) rename examples/whot-web/{ => static}/assets/svg/cross/11_cross.svg (100%) rename examples/whot-web/{ => static}/assets/svg/cross/13_cross.svg (100%) rename examples/whot-web/{ => static}/assets/svg/cross/14_cross.svg (100%) rename examples/whot-web/{ => static}/assets/svg/cross/1_cross.svg (100%) rename examples/whot-web/{ => static}/assets/svg/cross/2_cross.svg (100%) rename examples/whot-web/{ => static}/assets/svg/cross/3_cross.svg (100%) rename examples/whot-web/{ => static}/assets/svg/cross/5_cross.svg (100%) rename examples/whot-web/{ => static}/assets/svg/cross/7_cross.svg (100%) rename examples/whot-web/{ => static}/assets/svg/square/10_square.svg (100%) rename examples/whot-web/{ => static}/assets/svg/square/11_square.svg (100%) rename examples/whot-web/{ => static}/assets/svg/square/13_square.svg (100%) rename examples/whot-web/{ => static}/assets/svg/square/14_square.svg (100%) rename examples/whot-web/{ => static}/assets/svg/square/1_square.svg (100%) rename examples/whot-web/{ => static}/assets/svg/square/2_square.svg (100%) rename examples/whot-web/{ => static}/assets/svg/square/3_square.svg (100%) rename examples/whot-web/{ => static}/assets/svg/square/5_square.svg (100%) rename examples/whot-web/{ => static}/assets/svg/square/7_square.svg (100%) rename examples/whot-web/{ => static}/assets/svg/star/1_star.svg (100%) rename examples/whot-web/{ => static}/assets/svg/star/2_star.svg (100%) rename examples/whot-web/{ => static}/assets/svg/star/3_star.svg (100%) rename examples/whot-web/{ => static}/assets/svg/star/4_star.svg (100%) rename examples/whot-web/{ => static}/assets/svg/star/5_star.svg (100%) rename examples/whot-web/{ => static}/assets/svg/star/7_star.svg (100%) rename examples/whot-web/{ => static}/assets/svg/star/8_star.svg (100%) rename examples/whot-web/{ => static}/assets/svg/triangle/10_triangle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/triangle/11_triangle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/triangle/12_triangle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/triangle/13_triangle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/triangle/14_triangle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/triangle/1_triangle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/triangle/2_triangle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/triangle/3_triangle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/triangle/4_triangle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/triangle/5_triangle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/triangle/7_triangle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/triangle/8_triangle.svg (100%) rename examples/whot-web/{ => static}/assets/svg/whot_back.svg (100%) rename examples/whot-web/{ => static/assets}/table_top.jpeg (100%) create mode 100644 examples/whot-web/static/css/game.css create mode 100644 examples/whot-web/static/css/index.css rename examples/whot-web/{ => static/js}/script.js (54%) delete mode 100644 examples/whot-web/styles.css diff --git a/examples/whot-web/.env-example b/examples/whot-web/.env-example new file mode 100644 index 0000000..5d37aa5 --- /dev/null +++ b/examples/whot-web/.env-example @@ -0,0 +1,3 @@ +ADDRESS=127.0.0.1 +PORT=8080 +WEBSOCKET_PORT=8765 diff --git a/examples/whot-web/README.md b/examples/whot-web/README.md new file mode 100644 index 0000000..d2a28e5 --- /dev/null +++ b/examples/whot-web/README.md @@ -0,0 +1,78 @@ +# Web Whot + +A web-based implementation of **Whot**. With Web Whot, you can play the game over any network. Just create a game and share the link with another player. + +> Currently supports **two players per game**. + +## Technologies Used + +This project is built using simple, lightweight technologies: + +**Client:** + +* HTML +* Vanilla JavaScript +* CSS + +**Communication:** + +* HTTP +* WebSocket (for real-time updates) + +**Server:** + +* [aiohttp](https://github.com/aio-libs/aiohttp) – HTTP framework for Python +* [whot](https://github.com/EteimZ/whot) – Whot engine that powers the logic + +## Getting Started + +### 1. Clone the Game Engine + +Start by cloning the [`whot`](https://github.com/EteimZ/whot) repository: + +```bash +git clone https://github.com/EteimZ/whot.git +cd whot/examples/whot-web +``` + +### 2. Set Up Your Environment + +Make sure you have **Python 3.11+** installed. + +(Optional) Create and activate a virtual environment: + +```bash +python -m venv env +source env/bin/activate +``` + +### 3. Install Dependencies + +```bash +pip install -r requirements.txt +``` + +### 4. Configure Environment Variables + +Create a `.env` file in the root of the project and define the following variables: + +```dotenv +ADDRESS=127.0.0.1 +PORT=8080 +WEBSOCKET_PORT=8765 +``` + +`ADDRESS` should be your local or network IP address. +`PORT` is for the HTTP server. +`WEBSOCKET_PORT` is for WebSocket communication. + +## Running the App + +Start the application with: + +```bash +python main.py +``` + +Visit `http://
:` in your browser and start playing. + diff --git a/examples/whot-web/game.html b/examples/whot-web/game.html deleted file mode 100644 index 4f3dbca..0000000 --- a/examples/whot-web/game.html +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - Whot Game - - - -

Welcome to the Whot Game

- -

Current Player:

-

Player ID:

- -
-
- -
-
- -
-
- -
-
- -
- -
- - - - - - - - \ No newline at end of file diff --git a/examples/whot-web/html/game.html b/examples/whot-web/html/game.html new file mode 100644 index 0000000..b1ae5d0 --- /dev/null +++ b/examples/whot-web/html/game.html @@ -0,0 +1,61 @@ + + + + + + + + Whot Game + + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+ + + + + + + + + + diff --git a/examples/whot-web/html/index.html b/examples/whot-web/html/index.html new file mode 100644 index 0000000..cc1d945 --- /dev/null +++ b/examples/whot-web/html/index.html @@ -0,0 +1,48 @@ + + + + + + + Whot Game + + +

Create Whot Game

+

Play Whot with your friends by starting or joining a game using a link

+
+ New Game + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/examples/whot-web/index.html b/examples/whot-web/index.html deleted file mode 100644 index 4efc11d..0000000 --- a/examples/whot-web/index.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - Whot Game - - - -

Create Whot Game

- -
- New Game - - - - - -
- - - - - - \ No newline at end of file diff --git a/examples/whot-web/main.py b/examples/whot-web/main.py index 4e9d542..37f1a3b 100644 --- a/examples/whot-web/main.py +++ b/examples/whot-web/main.py @@ -17,17 +17,18 @@ # Create an aiohttp web app app = web.Application() -whot_server = WhotServer() +# Create an instance of the whot server +whot_server = WhotServer() aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('.')) # Serve index.html async def handle_index(request): - return web.FileResponse("index.html") + return web.FileResponse("html/index.html") async def handle_game(request): - return aiohttp_jinja2.render_template("game.html", request, context={ + return aiohttp_jinja2.render_template("html/game.html", request, context={ "websocket_url": f"ws://{ADDRESS}:{WEBSOCKET_PORT}" }) @@ -43,7 +44,7 @@ async def websocket_server(): # Define routes app.router.add_get("/", handle_index) app.router.add_get("/game", handle_game) -app.router.add_static("/", path=".", name="static") +app.router.add_static("/", path="static", name="static") # Function to run the aiohttp server async def run_server(): diff --git a/examples/whot-web/server.py b/examples/whot-web/server.py index 46bc9b4..647c514 100644 --- a/examples/whot-web/server.py +++ b/examples/whot-web/server.py @@ -108,7 +108,7 @@ async def join(self, websocket: ClientConnection, join_key): async def play(self, websocket: ClientConnection, game: Whot, player_id: str, gameConnection: GameConnection): server_event = { - "type": "player_id", + "type": "start", "player_id": player_id, } @@ -156,14 +156,14 @@ async def play(self, websocket: ClientConnection, game: Whot, player_id: str, ga receiver_ids=[game.current_player.player_id], content="Your turn to play.") - await self.send_event_to_all(result["type"], game, gameConnection, message) + await self.send_event_to_all("play", game, gameConnection, message) elif result['type'] == "request": await self.handle_request(game, gameConnection) elif result['status'] == False: - await self.send_event_to_all(result["type"], game, gameConnection) + await self.send_event_to_all("play", game, gameConnection) server_event = { "type": "win", @@ -245,11 +245,18 @@ async def play(self, websocket: ClientConnection, game: Whot, player_id: str, ga for socket_id in gameConnection.connections: + server_event = { + "type": "play", + "player_id": socket_id, + "game_state": serialize_game_view(game.view(socket_id)) + } + + await gameConnection.send(socket_id, server_event) + if socket_id != requester: server_event = { - "type": "request_card", - "message": f"{requester} requested for {card}", - "game_state": serialize_game_view(game.view(socket_id)) + "type": "message", + "message": f"{requester} needs {card}", } await gameConnection.send(socket_id, server_event) @@ -259,7 +266,7 @@ async def handle_pick_two(self, game: Whot, gameConnection: GameConnection): receiver_ids=[game.current_player.player_id], content="Pick two! You have been asked to pick two cards.") - await self.send_event_to_all("pick_2", game, gameConnection, message) + await self.send_event_to_all("play", game, gameConnection, message) async def handle_general_market(self, game: Whot, gameConnection: GameConnection): other_players = list(gameConnection.connections.keys()) @@ -269,7 +276,7 @@ async def handle_general_market(self, game: Whot, gameConnection: GameConnection receiver_ids=other_players, content="Everyone Go gen.") - await self.send_event_to_all("general_market", game, gameConnection, message) + await self.send_event_to_all("play", game, gameConnection, message) async def handle_suspension(self, game: Whot, gameConnection: GameConnection): try: @@ -283,7 +290,7 @@ async def handle_suspension(self, game: Whot, gameConnection: GameConnection): receiver_ids=[suspended_player_id], content="You have been suspended.") - await self.send_event_to_all("suspension", game, gameConnection, message) + await self.send_event_to_all("play", game, gameConnection, message) async def handle_hold_on(self, game: Whot, gameConnection: GameConnection): try: @@ -297,13 +304,21 @@ async def handle_hold_on(self, game: Whot, gameConnection: GameConnection): receiver_ids=[on_hold_player_id], content="You have been placed on hold.") - await self.send_event_to_all("hold_on", game, gameConnection, message) + await self.send_event_to_all("play", game, gameConnection, message) async def handle_request(self, game: Whot, gameConnection: GameConnection): current_player = game.current_player.player_id for socket_id in gameConnection.connections: + server_event = { + "type": "play", + "player_id": socket_id, + "game_state": serialize_game_view(game.view(socket_id)) + } + + await gameConnection.send(socket_id, server_event) + if socket_id == current_player: server_event = { @@ -311,16 +326,8 @@ async def handle_request(self, game: Whot, gameConnection: GameConnection): "player_id": socket_id, "game_state": serialize_game_view(game.view(socket_id)) } - - else: - - server_event = { - "type": "play", - "player_id": socket_id, - "game_state": serialize_game_view(game.view(socket_id)) - } - await gameConnection.send(socket_id, server_event) + await gameConnection.send(socket_id, server_event) async def send_event_to_all(self, type, game: Whot, gameConnection: GameConnection, message: Message | None = None): for socket_id in gameConnection.connections: diff --git a/examples/whot-web/assets/cards.png b/examples/whot-web/static/assets/cards.png similarity index 100% rename from examples/whot-web/assets/cards.png rename to examples/whot-web/static/assets/cards.png diff --git a/examples/whot-web/assets/gameplay.png b/examples/whot-web/static/assets/gameplay.png similarity index 100% rename from examples/whot-web/assets/gameplay.png rename to examples/whot-web/static/assets/gameplay.png diff --git a/examples/whot-web/assets/images/20_whot.png b/examples/whot-web/static/assets/images/20_whot.png similarity index 100% rename from examples/whot-web/assets/images/20_whot.png rename to examples/whot-web/static/assets/images/20_whot.png diff --git a/examples/whot-web/assets/images/circle/10_circle.png b/examples/whot-web/static/assets/images/circle/10_circle.png similarity index 100% rename from examples/whot-web/assets/images/circle/10_circle.png rename to examples/whot-web/static/assets/images/circle/10_circle.png diff --git a/examples/whot-web/assets/images/circle/11_circle.png b/examples/whot-web/static/assets/images/circle/11_circle.png similarity index 100% rename from examples/whot-web/assets/images/circle/11_circle.png rename to examples/whot-web/static/assets/images/circle/11_circle.png diff --git a/examples/whot-web/assets/images/circle/12_circle.png b/examples/whot-web/static/assets/images/circle/12_circle.png similarity index 100% rename from examples/whot-web/assets/images/circle/12_circle.png rename to examples/whot-web/static/assets/images/circle/12_circle.png diff --git a/examples/whot-web/assets/images/circle/13_circle.png b/examples/whot-web/static/assets/images/circle/13_circle.png similarity index 100% rename from examples/whot-web/assets/images/circle/13_circle.png rename to examples/whot-web/static/assets/images/circle/13_circle.png diff --git a/examples/whot-web/assets/images/circle/14_circle.png b/examples/whot-web/static/assets/images/circle/14_circle.png similarity index 100% rename from examples/whot-web/assets/images/circle/14_circle.png rename to examples/whot-web/static/assets/images/circle/14_circle.png diff --git a/examples/whot-web/assets/images/circle/1_circle.png b/examples/whot-web/static/assets/images/circle/1_circle.png similarity index 100% rename from examples/whot-web/assets/images/circle/1_circle.png rename to examples/whot-web/static/assets/images/circle/1_circle.png diff --git a/examples/whot-web/assets/images/circle/2_circle.png b/examples/whot-web/static/assets/images/circle/2_circle.png similarity index 100% rename from examples/whot-web/assets/images/circle/2_circle.png rename to examples/whot-web/static/assets/images/circle/2_circle.png diff --git a/examples/whot-web/assets/images/circle/3_circle.png b/examples/whot-web/static/assets/images/circle/3_circle.png similarity index 100% rename from examples/whot-web/assets/images/circle/3_circle.png rename to examples/whot-web/static/assets/images/circle/3_circle.png diff --git a/examples/whot-web/assets/images/circle/4_circle.png b/examples/whot-web/static/assets/images/circle/4_circle.png similarity index 100% rename from examples/whot-web/assets/images/circle/4_circle.png rename to examples/whot-web/static/assets/images/circle/4_circle.png diff --git a/examples/whot-web/assets/images/circle/5_circle.png b/examples/whot-web/static/assets/images/circle/5_circle.png similarity index 100% rename from examples/whot-web/assets/images/circle/5_circle.png rename to examples/whot-web/static/assets/images/circle/5_circle.png diff --git a/examples/whot-web/assets/images/circle/7_circle.png b/examples/whot-web/static/assets/images/circle/7_circle.png similarity index 100% rename from examples/whot-web/assets/images/circle/7_circle.png rename to examples/whot-web/static/assets/images/circle/7_circle.png diff --git a/examples/whot-web/assets/images/circle/8_circle.png b/examples/whot-web/static/assets/images/circle/8_circle.png similarity index 100% rename from examples/whot-web/assets/images/circle/8_circle.png rename to examples/whot-web/static/assets/images/circle/8_circle.png diff --git a/examples/whot-web/assets/images/cross/10_cross.png b/examples/whot-web/static/assets/images/cross/10_cross.png similarity index 100% rename from examples/whot-web/assets/images/cross/10_cross.png rename to examples/whot-web/static/assets/images/cross/10_cross.png diff --git a/examples/whot-web/assets/images/cross/11_cross.png b/examples/whot-web/static/assets/images/cross/11_cross.png similarity index 100% rename from examples/whot-web/assets/images/cross/11_cross.png rename to examples/whot-web/static/assets/images/cross/11_cross.png diff --git a/examples/whot-web/assets/images/cross/13_cross.png b/examples/whot-web/static/assets/images/cross/13_cross.png similarity index 100% rename from examples/whot-web/assets/images/cross/13_cross.png rename to examples/whot-web/static/assets/images/cross/13_cross.png diff --git a/examples/whot-web/assets/images/cross/14_cross.png b/examples/whot-web/static/assets/images/cross/14_cross.png similarity index 100% rename from examples/whot-web/assets/images/cross/14_cross.png rename to examples/whot-web/static/assets/images/cross/14_cross.png diff --git a/examples/whot-web/assets/images/cross/1_cross.png b/examples/whot-web/static/assets/images/cross/1_cross.png similarity index 100% rename from examples/whot-web/assets/images/cross/1_cross.png rename to examples/whot-web/static/assets/images/cross/1_cross.png diff --git a/examples/whot-web/assets/images/cross/2_cross.png b/examples/whot-web/static/assets/images/cross/2_cross.png similarity index 100% rename from examples/whot-web/assets/images/cross/2_cross.png rename to examples/whot-web/static/assets/images/cross/2_cross.png diff --git a/examples/whot-web/assets/images/cross/3_cross.png b/examples/whot-web/static/assets/images/cross/3_cross.png similarity index 100% rename from examples/whot-web/assets/images/cross/3_cross.png rename to examples/whot-web/static/assets/images/cross/3_cross.png diff --git a/examples/whot-web/assets/images/cross/5_cross.png b/examples/whot-web/static/assets/images/cross/5_cross.png similarity index 100% rename from examples/whot-web/assets/images/cross/5_cross.png rename to examples/whot-web/static/assets/images/cross/5_cross.png diff --git a/examples/whot-web/assets/images/cross/7_cross.png b/examples/whot-web/static/assets/images/cross/7_cross.png similarity index 100% rename from examples/whot-web/assets/images/cross/7_cross.png rename to examples/whot-web/static/assets/images/cross/7_cross.png diff --git a/examples/whot-web/assets/images/square/10_square.png b/examples/whot-web/static/assets/images/square/10_square.png similarity index 100% rename from examples/whot-web/assets/images/square/10_square.png rename to examples/whot-web/static/assets/images/square/10_square.png diff --git a/examples/whot-web/assets/images/square/11_square.png b/examples/whot-web/static/assets/images/square/11_square.png similarity index 100% rename from examples/whot-web/assets/images/square/11_square.png rename to examples/whot-web/static/assets/images/square/11_square.png diff --git a/examples/whot-web/assets/images/square/13_square.png b/examples/whot-web/static/assets/images/square/13_square.png similarity index 100% rename from examples/whot-web/assets/images/square/13_square.png rename to examples/whot-web/static/assets/images/square/13_square.png diff --git a/examples/whot-web/assets/images/square/14_square.png b/examples/whot-web/static/assets/images/square/14_square.png similarity index 100% rename from examples/whot-web/assets/images/square/14_square.png rename to examples/whot-web/static/assets/images/square/14_square.png diff --git a/examples/whot-web/assets/images/square/1_square.png b/examples/whot-web/static/assets/images/square/1_square.png similarity index 100% rename from examples/whot-web/assets/images/square/1_square.png rename to examples/whot-web/static/assets/images/square/1_square.png diff --git a/examples/whot-web/assets/images/square/2_square.png b/examples/whot-web/static/assets/images/square/2_square.png similarity index 100% rename from examples/whot-web/assets/images/square/2_square.png rename to examples/whot-web/static/assets/images/square/2_square.png diff --git a/examples/whot-web/assets/images/square/3_square.png b/examples/whot-web/static/assets/images/square/3_square.png similarity index 100% rename from examples/whot-web/assets/images/square/3_square.png rename to examples/whot-web/static/assets/images/square/3_square.png diff --git a/examples/whot-web/assets/images/square/5_square.png b/examples/whot-web/static/assets/images/square/5_square.png similarity index 100% rename from examples/whot-web/assets/images/square/5_square.png rename to examples/whot-web/static/assets/images/square/5_square.png diff --git a/examples/whot-web/assets/images/square/7_square.png b/examples/whot-web/static/assets/images/square/7_square.png similarity index 100% rename from examples/whot-web/assets/images/square/7_square.png rename to examples/whot-web/static/assets/images/square/7_square.png diff --git a/examples/whot-web/assets/images/star/1_star.png b/examples/whot-web/static/assets/images/star/1_star.png similarity index 100% rename from examples/whot-web/assets/images/star/1_star.png rename to examples/whot-web/static/assets/images/star/1_star.png diff --git a/examples/whot-web/assets/images/star/2_star.png b/examples/whot-web/static/assets/images/star/2_star.png similarity index 100% rename from examples/whot-web/assets/images/star/2_star.png rename to examples/whot-web/static/assets/images/star/2_star.png diff --git a/examples/whot-web/assets/images/star/3_star.png b/examples/whot-web/static/assets/images/star/3_star.png similarity index 100% rename from examples/whot-web/assets/images/star/3_star.png rename to examples/whot-web/static/assets/images/star/3_star.png diff --git a/examples/whot-web/assets/images/star/4_star.png b/examples/whot-web/static/assets/images/star/4_star.png similarity index 100% rename from examples/whot-web/assets/images/star/4_star.png rename to examples/whot-web/static/assets/images/star/4_star.png diff --git a/examples/whot-web/assets/images/star/5_star.png b/examples/whot-web/static/assets/images/star/5_star.png similarity index 100% rename from examples/whot-web/assets/images/star/5_star.png rename to examples/whot-web/static/assets/images/star/5_star.png diff --git a/examples/whot-web/assets/images/star/7_star.png b/examples/whot-web/static/assets/images/star/7_star.png similarity index 100% rename from examples/whot-web/assets/images/star/7_star.png rename to examples/whot-web/static/assets/images/star/7_star.png diff --git a/examples/whot-web/assets/images/star/8_star.png b/examples/whot-web/static/assets/images/star/8_star.png similarity index 100% rename from examples/whot-web/assets/images/star/8_star.png rename to examples/whot-web/static/assets/images/star/8_star.png diff --git a/examples/whot-web/assets/images/triangle/10_triangle.png b/examples/whot-web/static/assets/images/triangle/10_triangle.png similarity index 100% rename from examples/whot-web/assets/images/triangle/10_triangle.png rename to examples/whot-web/static/assets/images/triangle/10_triangle.png diff --git a/examples/whot-web/assets/images/triangle/11_triangle.png b/examples/whot-web/static/assets/images/triangle/11_triangle.png similarity index 100% rename from examples/whot-web/assets/images/triangle/11_triangle.png rename to examples/whot-web/static/assets/images/triangle/11_triangle.png diff --git a/examples/whot-web/assets/images/triangle/12_triangle.png b/examples/whot-web/static/assets/images/triangle/12_triangle.png similarity index 100% rename from examples/whot-web/assets/images/triangle/12_triangle.png rename to examples/whot-web/static/assets/images/triangle/12_triangle.png diff --git a/examples/whot-web/assets/images/triangle/13_triangle.png b/examples/whot-web/static/assets/images/triangle/13_triangle.png similarity index 100% rename from examples/whot-web/assets/images/triangle/13_triangle.png rename to examples/whot-web/static/assets/images/triangle/13_triangle.png diff --git a/examples/whot-web/assets/images/triangle/14_triangle.png b/examples/whot-web/static/assets/images/triangle/14_triangle.png similarity index 100% rename from examples/whot-web/assets/images/triangle/14_triangle.png rename to examples/whot-web/static/assets/images/triangle/14_triangle.png diff --git a/examples/whot-web/assets/images/triangle/1_triangle.png b/examples/whot-web/static/assets/images/triangle/1_triangle.png similarity index 100% rename from examples/whot-web/assets/images/triangle/1_triangle.png rename to examples/whot-web/static/assets/images/triangle/1_triangle.png diff --git a/examples/whot-web/assets/images/triangle/2_triangle.png b/examples/whot-web/static/assets/images/triangle/2_triangle.png similarity index 100% rename from examples/whot-web/assets/images/triangle/2_triangle.png rename to examples/whot-web/static/assets/images/triangle/2_triangle.png diff --git a/examples/whot-web/assets/images/triangle/3_triangle.png b/examples/whot-web/static/assets/images/triangle/3_triangle.png similarity index 100% rename from examples/whot-web/assets/images/triangle/3_triangle.png rename to examples/whot-web/static/assets/images/triangle/3_triangle.png diff --git a/examples/whot-web/assets/images/triangle/4_triangle.png b/examples/whot-web/static/assets/images/triangle/4_triangle.png similarity index 100% rename from examples/whot-web/assets/images/triangle/4_triangle.png rename to examples/whot-web/static/assets/images/triangle/4_triangle.png diff --git a/examples/whot-web/assets/images/triangle/5_triangle.png b/examples/whot-web/static/assets/images/triangle/5_triangle.png similarity index 100% rename from examples/whot-web/assets/images/triangle/5_triangle.png rename to examples/whot-web/static/assets/images/triangle/5_triangle.png diff --git a/examples/whot-web/assets/images/triangle/7_triangle.png b/examples/whot-web/static/assets/images/triangle/7_triangle.png similarity index 100% rename from examples/whot-web/assets/images/triangle/7_triangle.png rename to examples/whot-web/static/assets/images/triangle/7_triangle.png diff --git a/examples/whot-web/assets/images/triangle/8_triangle.png b/examples/whot-web/static/assets/images/triangle/8_triangle.png similarity index 100% rename from examples/whot-web/assets/images/triangle/8_triangle.png rename to examples/whot-web/static/assets/images/triangle/8_triangle.png diff --git a/examples/whot-web/assets/images/whot_back.png b/examples/whot-web/static/assets/images/whot_back.png similarity index 100% rename from examples/whot-web/assets/images/whot_back.png rename to examples/whot-web/static/assets/images/whot_back.png diff --git a/examples/whot-web/assets/svg/20_whot.svg b/examples/whot-web/static/assets/svg/20_whot.svg similarity index 100% rename from examples/whot-web/assets/svg/20_whot.svg rename to examples/whot-web/static/assets/svg/20_whot.svg diff --git a/examples/whot-web/assets/svg/circle/10_circle.svg b/examples/whot-web/static/assets/svg/circle/10_circle.svg similarity index 100% rename from examples/whot-web/assets/svg/circle/10_circle.svg rename to examples/whot-web/static/assets/svg/circle/10_circle.svg diff --git a/examples/whot-web/assets/svg/circle/11_circle.svg b/examples/whot-web/static/assets/svg/circle/11_circle.svg similarity index 100% rename from examples/whot-web/assets/svg/circle/11_circle.svg rename to examples/whot-web/static/assets/svg/circle/11_circle.svg diff --git a/examples/whot-web/assets/svg/circle/12_circle.svg b/examples/whot-web/static/assets/svg/circle/12_circle.svg similarity index 100% rename from examples/whot-web/assets/svg/circle/12_circle.svg rename to examples/whot-web/static/assets/svg/circle/12_circle.svg diff --git a/examples/whot-web/assets/svg/circle/13_circle.svg b/examples/whot-web/static/assets/svg/circle/13_circle.svg similarity index 100% rename from examples/whot-web/assets/svg/circle/13_circle.svg rename to examples/whot-web/static/assets/svg/circle/13_circle.svg diff --git a/examples/whot-web/assets/svg/circle/14_circle.svg b/examples/whot-web/static/assets/svg/circle/14_circle.svg similarity index 100% rename from examples/whot-web/assets/svg/circle/14_circle.svg rename to examples/whot-web/static/assets/svg/circle/14_circle.svg diff --git a/examples/whot-web/assets/svg/circle/1_circle.svg b/examples/whot-web/static/assets/svg/circle/1_circle.svg similarity index 100% rename from examples/whot-web/assets/svg/circle/1_circle.svg rename to examples/whot-web/static/assets/svg/circle/1_circle.svg diff --git a/examples/whot-web/assets/svg/circle/2_circle.svg b/examples/whot-web/static/assets/svg/circle/2_circle.svg similarity index 100% rename from examples/whot-web/assets/svg/circle/2_circle.svg rename to examples/whot-web/static/assets/svg/circle/2_circle.svg diff --git a/examples/whot-web/assets/svg/circle/3_circle.svg b/examples/whot-web/static/assets/svg/circle/3_circle.svg similarity index 100% rename from examples/whot-web/assets/svg/circle/3_circle.svg rename to examples/whot-web/static/assets/svg/circle/3_circle.svg diff --git a/examples/whot-web/assets/svg/circle/4_circle.svg b/examples/whot-web/static/assets/svg/circle/4_circle.svg similarity index 100% rename from examples/whot-web/assets/svg/circle/4_circle.svg rename to examples/whot-web/static/assets/svg/circle/4_circle.svg diff --git a/examples/whot-web/assets/svg/circle/5_circle.svg b/examples/whot-web/static/assets/svg/circle/5_circle.svg similarity index 100% rename from examples/whot-web/assets/svg/circle/5_circle.svg rename to examples/whot-web/static/assets/svg/circle/5_circle.svg diff --git a/examples/whot-web/assets/svg/circle/7_circle.svg b/examples/whot-web/static/assets/svg/circle/7_circle.svg similarity index 100% rename from examples/whot-web/assets/svg/circle/7_circle.svg rename to examples/whot-web/static/assets/svg/circle/7_circle.svg diff --git a/examples/whot-web/assets/svg/circle/8_circle.svg b/examples/whot-web/static/assets/svg/circle/8_circle.svg similarity index 100% rename from examples/whot-web/assets/svg/circle/8_circle.svg rename to examples/whot-web/static/assets/svg/circle/8_circle.svg diff --git a/examples/whot-web/assets/svg/cross/10_cross.svg b/examples/whot-web/static/assets/svg/cross/10_cross.svg similarity index 100% rename from examples/whot-web/assets/svg/cross/10_cross.svg rename to examples/whot-web/static/assets/svg/cross/10_cross.svg diff --git a/examples/whot-web/assets/svg/cross/11_cross.svg b/examples/whot-web/static/assets/svg/cross/11_cross.svg similarity index 100% rename from examples/whot-web/assets/svg/cross/11_cross.svg rename to examples/whot-web/static/assets/svg/cross/11_cross.svg diff --git a/examples/whot-web/assets/svg/cross/13_cross.svg b/examples/whot-web/static/assets/svg/cross/13_cross.svg similarity index 100% rename from examples/whot-web/assets/svg/cross/13_cross.svg rename to examples/whot-web/static/assets/svg/cross/13_cross.svg diff --git a/examples/whot-web/assets/svg/cross/14_cross.svg b/examples/whot-web/static/assets/svg/cross/14_cross.svg similarity index 100% rename from examples/whot-web/assets/svg/cross/14_cross.svg rename to examples/whot-web/static/assets/svg/cross/14_cross.svg diff --git a/examples/whot-web/assets/svg/cross/1_cross.svg b/examples/whot-web/static/assets/svg/cross/1_cross.svg similarity index 100% rename from examples/whot-web/assets/svg/cross/1_cross.svg rename to examples/whot-web/static/assets/svg/cross/1_cross.svg diff --git a/examples/whot-web/assets/svg/cross/2_cross.svg b/examples/whot-web/static/assets/svg/cross/2_cross.svg similarity index 100% rename from examples/whot-web/assets/svg/cross/2_cross.svg rename to examples/whot-web/static/assets/svg/cross/2_cross.svg diff --git a/examples/whot-web/assets/svg/cross/3_cross.svg b/examples/whot-web/static/assets/svg/cross/3_cross.svg similarity index 100% rename from examples/whot-web/assets/svg/cross/3_cross.svg rename to examples/whot-web/static/assets/svg/cross/3_cross.svg diff --git a/examples/whot-web/assets/svg/cross/5_cross.svg b/examples/whot-web/static/assets/svg/cross/5_cross.svg similarity index 100% rename from examples/whot-web/assets/svg/cross/5_cross.svg rename to examples/whot-web/static/assets/svg/cross/5_cross.svg diff --git a/examples/whot-web/assets/svg/cross/7_cross.svg b/examples/whot-web/static/assets/svg/cross/7_cross.svg similarity index 100% rename from examples/whot-web/assets/svg/cross/7_cross.svg rename to examples/whot-web/static/assets/svg/cross/7_cross.svg diff --git a/examples/whot-web/assets/svg/square/10_square.svg b/examples/whot-web/static/assets/svg/square/10_square.svg similarity index 100% rename from examples/whot-web/assets/svg/square/10_square.svg rename to examples/whot-web/static/assets/svg/square/10_square.svg diff --git a/examples/whot-web/assets/svg/square/11_square.svg b/examples/whot-web/static/assets/svg/square/11_square.svg similarity index 100% rename from examples/whot-web/assets/svg/square/11_square.svg rename to examples/whot-web/static/assets/svg/square/11_square.svg diff --git a/examples/whot-web/assets/svg/square/13_square.svg b/examples/whot-web/static/assets/svg/square/13_square.svg similarity index 100% rename from examples/whot-web/assets/svg/square/13_square.svg rename to examples/whot-web/static/assets/svg/square/13_square.svg diff --git a/examples/whot-web/assets/svg/square/14_square.svg b/examples/whot-web/static/assets/svg/square/14_square.svg similarity index 100% rename from examples/whot-web/assets/svg/square/14_square.svg rename to examples/whot-web/static/assets/svg/square/14_square.svg diff --git a/examples/whot-web/assets/svg/square/1_square.svg b/examples/whot-web/static/assets/svg/square/1_square.svg similarity index 100% rename from examples/whot-web/assets/svg/square/1_square.svg rename to examples/whot-web/static/assets/svg/square/1_square.svg diff --git a/examples/whot-web/assets/svg/square/2_square.svg b/examples/whot-web/static/assets/svg/square/2_square.svg similarity index 100% rename from examples/whot-web/assets/svg/square/2_square.svg rename to examples/whot-web/static/assets/svg/square/2_square.svg diff --git a/examples/whot-web/assets/svg/square/3_square.svg b/examples/whot-web/static/assets/svg/square/3_square.svg similarity index 100% rename from examples/whot-web/assets/svg/square/3_square.svg rename to examples/whot-web/static/assets/svg/square/3_square.svg diff --git a/examples/whot-web/assets/svg/square/5_square.svg b/examples/whot-web/static/assets/svg/square/5_square.svg similarity index 100% rename from examples/whot-web/assets/svg/square/5_square.svg rename to examples/whot-web/static/assets/svg/square/5_square.svg diff --git a/examples/whot-web/assets/svg/square/7_square.svg b/examples/whot-web/static/assets/svg/square/7_square.svg similarity index 100% rename from examples/whot-web/assets/svg/square/7_square.svg rename to examples/whot-web/static/assets/svg/square/7_square.svg diff --git a/examples/whot-web/assets/svg/star/1_star.svg b/examples/whot-web/static/assets/svg/star/1_star.svg similarity index 100% rename from examples/whot-web/assets/svg/star/1_star.svg rename to examples/whot-web/static/assets/svg/star/1_star.svg diff --git a/examples/whot-web/assets/svg/star/2_star.svg b/examples/whot-web/static/assets/svg/star/2_star.svg similarity index 100% rename from examples/whot-web/assets/svg/star/2_star.svg rename to examples/whot-web/static/assets/svg/star/2_star.svg diff --git a/examples/whot-web/assets/svg/star/3_star.svg b/examples/whot-web/static/assets/svg/star/3_star.svg similarity index 100% rename from examples/whot-web/assets/svg/star/3_star.svg rename to examples/whot-web/static/assets/svg/star/3_star.svg diff --git a/examples/whot-web/assets/svg/star/4_star.svg b/examples/whot-web/static/assets/svg/star/4_star.svg similarity index 100% rename from examples/whot-web/assets/svg/star/4_star.svg rename to examples/whot-web/static/assets/svg/star/4_star.svg diff --git a/examples/whot-web/assets/svg/star/5_star.svg b/examples/whot-web/static/assets/svg/star/5_star.svg similarity index 100% rename from examples/whot-web/assets/svg/star/5_star.svg rename to examples/whot-web/static/assets/svg/star/5_star.svg diff --git a/examples/whot-web/assets/svg/star/7_star.svg b/examples/whot-web/static/assets/svg/star/7_star.svg similarity index 100% rename from examples/whot-web/assets/svg/star/7_star.svg rename to examples/whot-web/static/assets/svg/star/7_star.svg diff --git a/examples/whot-web/assets/svg/star/8_star.svg b/examples/whot-web/static/assets/svg/star/8_star.svg similarity index 100% rename from examples/whot-web/assets/svg/star/8_star.svg rename to examples/whot-web/static/assets/svg/star/8_star.svg diff --git a/examples/whot-web/assets/svg/triangle/10_triangle.svg b/examples/whot-web/static/assets/svg/triangle/10_triangle.svg similarity index 100% rename from examples/whot-web/assets/svg/triangle/10_triangle.svg rename to examples/whot-web/static/assets/svg/triangle/10_triangle.svg diff --git a/examples/whot-web/assets/svg/triangle/11_triangle.svg b/examples/whot-web/static/assets/svg/triangle/11_triangle.svg similarity index 100% rename from examples/whot-web/assets/svg/triangle/11_triangle.svg rename to examples/whot-web/static/assets/svg/triangle/11_triangle.svg diff --git a/examples/whot-web/assets/svg/triangle/12_triangle.svg b/examples/whot-web/static/assets/svg/triangle/12_triangle.svg similarity index 100% rename from examples/whot-web/assets/svg/triangle/12_triangle.svg rename to examples/whot-web/static/assets/svg/triangle/12_triangle.svg diff --git a/examples/whot-web/assets/svg/triangle/13_triangle.svg b/examples/whot-web/static/assets/svg/triangle/13_triangle.svg similarity index 100% rename from examples/whot-web/assets/svg/triangle/13_triangle.svg rename to examples/whot-web/static/assets/svg/triangle/13_triangle.svg diff --git a/examples/whot-web/assets/svg/triangle/14_triangle.svg b/examples/whot-web/static/assets/svg/triangle/14_triangle.svg similarity index 100% rename from examples/whot-web/assets/svg/triangle/14_triangle.svg rename to examples/whot-web/static/assets/svg/triangle/14_triangle.svg diff --git a/examples/whot-web/assets/svg/triangle/1_triangle.svg b/examples/whot-web/static/assets/svg/triangle/1_triangle.svg similarity index 100% rename from examples/whot-web/assets/svg/triangle/1_triangle.svg rename to examples/whot-web/static/assets/svg/triangle/1_triangle.svg diff --git a/examples/whot-web/assets/svg/triangle/2_triangle.svg b/examples/whot-web/static/assets/svg/triangle/2_triangle.svg similarity index 100% rename from examples/whot-web/assets/svg/triangle/2_triangle.svg rename to examples/whot-web/static/assets/svg/triangle/2_triangle.svg diff --git a/examples/whot-web/assets/svg/triangle/3_triangle.svg b/examples/whot-web/static/assets/svg/triangle/3_triangle.svg similarity index 100% rename from examples/whot-web/assets/svg/triangle/3_triangle.svg rename to examples/whot-web/static/assets/svg/triangle/3_triangle.svg diff --git a/examples/whot-web/assets/svg/triangle/4_triangle.svg b/examples/whot-web/static/assets/svg/triangle/4_triangle.svg similarity index 100% rename from examples/whot-web/assets/svg/triangle/4_triangle.svg rename to examples/whot-web/static/assets/svg/triangle/4_triangle.svg diff --git a/examples/whot-web/assets/svg/triangle/5_triangle.svg b/examples/whot-web/static/assets/svg/triangle/5_triangle.svg similarity index 100% rename from examples/whot-web/assets/svg/triangle/5_triangle.svg rename to examples/whot-web/static/assets/svg/triangle/5_triangle.svg diff --git a/examples/whot-web/assets/svg/triangle/7_triangle.svg b/examples/whot-web/static/assets/svg/triangle/7_triangle.svg similarity index 100% rename from examples/whot-web/assets/svg/triangle/7_triangle.svg rename to examples/whot-web/static/assets/svg/triangle/7_triangle.svg diff --git a/examples/whot-web/assets/svg/triangle/8_triangle.svg b/examples/whot-web/static/assets/svg/triangle/8_triangle.svg similarity index 100% rename from examples/whot-web/assets/svg/triangle/8_triangle.svg rename to examples/whot-web/static/assets/svg/triangle/8_triangle.svg diff --git a/examples/whot-web/assets/svg/whot_back.svg b/examples/whot-web/static/assets/svg/whot_back.svg similarity index 100% rename from examples/whot-web/assets/svg/whot_back.svg rename to examples/whot-web/static/assets/svg/whot_back.svg diff --git a/examples/whot-web/table_top.jpeg b/examples/whot-web/static/assets/table_top.jpeg similarity index 100% rename from examples/whot-web/table_top.jpeg rename to examples/whot-web/static/assets/table_top.jpeg diff --git a/examples/whot-web/static/css/game.css b/examples/whot-web/static/css/game.css new file mode 100644 index 0000000..3984923 --- /dev/null +++ b/examples/whot-web/static/css/game.css @@ -0,0 +1,229 @@ +/* Base dark theme */ +body { + background-color: #121212; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + margin: 0; + padding: 2rem 0 0; + color: #f5f5f5; + min-height: 100vh; + display: grid; + place-items: center; +} + +/* Grid-based layout with left spacer, board, and sidebar */ +.game-layout { + display: grid; + grid-template-columns: 1fr auto auto; + align-items: center; + gap: 2rem; + width: 100%; + max-width: 1400px; + padding: 2rem; + box-sizing: border-box; +} + +/* Spacer div (invisible) to balance sidebar */ +.spacer { + width: 1fr; +} + +/* Game board */ +#board { + width: 800px; + height: 600px; + background-image: url('../assets/table_top.jpeg'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + border-radius: 20px; + border: 3px solid #FFD700; + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; +} + +/* Opponent's cards */ +#opponent_cards { + display: flex; + gap: 10px; + position: absolute; + top: 10px; + left: 50%; + transform: translateX(-50%); +} + +/* Middle area with center card and market */ +#middle_area { + display: flex; + gap: 20px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + align-items: center; +} + +/* Player's cards */ +#player_cards { + display: flex; + gap: 10px; + position: absolute; + bottom: 10px; + left: 50%; + transform: translateX(-50%); +} + +/* Cards */ +img { + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + transition: transform 0.3s ease; +} + +img:hover { + transform: scale(1.1); +} + +/* Sidebar */ +#sidebar { + min-width: 250px; + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* Status container */ +.status-container { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +/* Status label */ +.status-label { + font-size: 0.9rem; + font-weight: 600; + background-color: #a1000d; + padding: 0.4rem 0.8rem; + border-radius: 20px; + border: 2px solid #FFD700; + box-shadow: 0 0 8px rgba(255, 215, 0, 0.5); + color: #ffffff; + text-align: center; +} + +/* Join link container */ +#join_link_container { + background-color: #292929; + border-radius: 10px; + padding: 0.8rem; + box-shadow: 0 0 8px rgba(255, 215, 0, 0.4); + border: 1px solid #FFD700; +} + +#join_link_container p { + margin: 0 0 0.3rem; + font-size: 0.85rem; + color: #f5f5f5; +} + +#join_link_container a { + text-decoration: underline; + color: #FFD700; + word-break: break-word; + font-size: 0.85rem; +} + +/* Modal overlay */ +.modal { + display: none; + /* Hidden by default */ + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(18, 18, 18, 0.266); + justify-content: center; + align-items: center; +} + +/* Modal content container */ +.modal-content { + background-color: #292929; + border: 2px solid #FFD700; + border-radius: 20px; + padding: 2rem; + box-shadow: 0 0 12px rgba(255, 215, 0, 0.5); + max-width: 400px; + width: 90%; + display: flex; + flex-direction: column; + align-items: center; +} + +/* Modal title (styled like status-label) */ +.modal-title { + font-size: 1rem; + font-weight: bold; + background-color: #a1000d; + color: #ffffff; + border: 2px solid #FFD700; + padding: 0.5rem 1rem; + border-radius: 20px; + margin-bottom: 1rem; + text-align: center; + box-shadow: 0 0 8px rgba(255, 215, 0, 0.5); +} + +/* Modal buttons container */ +.modal-buttons { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + justify-content: center; +} + +/* Modal buttons (like old request buttons) */ +.modal-buttons button { + background-color: #1e1e1e; + border: 2px solid #FFD700; + color: #fff; + padding: 0.4rem 0.8rem; + border-radius: 8px; + font-weight: bold; + cursor: pointer; + box-shadow: 0 0 6px rgba(255, 215, 0, 0.4); + transition: background-color 0.2s ease; +} + +.modal-buttons button:hover { + background-color: #333; +} + +.notification { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + font-size: 0.9rem; + font-weight: 600; + background-color: #a1000d; + padding: 0.4rem 0.8rem; + border-radius: 20px; + border: 2px solid #FFD700; + box-shadow: 0 0 8px rgba(255, 215, 0, 0.5); + color: #ffffff; + text-align: center; + z-index: 1000; + opacity: 0; + transition: opacity 0.3s ease; + max-width: 400px; +} + +.notification.show { + opacity: 1; +} \ No newline at end of file diff --git a/examples/whot-web/static/css/index.css b/examples/whot-web/static/css/index.css new file mode 100644 index 0000000..c3c14be --- /dev/null +++ b/examples/whot-web/static/css/index.css @@ -0,0 +1,85 @@ +body { + background-color: #121212; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + color: #f5f5f5; +} + +h1 { + font-size: 2.5rem; + margin-bottom: 0.5rem; + color: #FFD700; +} + +p.description { + font-size: 1.1rem; + color: #ffffff; + margin-bottom: 2rem; +} + +.actions { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.btn-primary { + background-color: #a1000d; + color: #ffffff; + padding: 0.8rem 1.5rem; + border: 2px solid #FFD700; + color: #FFD700; + border-radius: 30px; + font-size: 1rem; + font-weight: bold; + cursor: pointer; + transition: background-color 0.3s ease, border-color 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; +} + +.input-container { + display: flex; + align-items: center; + border: 2px solid #555; + border-radius: 30px; + overflow: hidden; + padding: 0 0.5rem; + background-color: #1e1e1e; +} + +input[type="text"] { + background-color: transparent; + border: none; + padding: 0.8rem 1rem; + outline: none; + font-size: 1rem; + color: #f5f5f5; + flex-grow: 1; +} + +input[type="text"]::placeholder { + color: #888; +} + +button.join { + background: none; + border: none; + padding: 0 1rem; + font-weight: bold; + color: #666; + cursor: not-allowed; +} + +button.join.enabled { + color: #FFD700; + cursor: pointer; +} \ No newline at end of file diff --git a/examples/whot-web/script.js b/examples/whot-web/static/js/script.js similarity index 54% rename from examples/whot-web/script.js rename to examples/whot-web/static/js/script.js index 72b7d97..79991ab 100644 --- a/examples/whot-web/script.js +++ b/examples/whot-web/static/js/script.js @@ -3,12 +3,10 @@ function initGame(websocket) { // Send an "init" event according to who is connecting. const params = new URLSearchParams(window.location.search); let event = { type: "init" }; + if (params.has("join")) { // Second player joins an existing game. event.join = params.get("join"); - } else if (params.has("watch")) { - // Spectator watches an existing game. - event.watch = params.get("watch"); } else { // First player starts a new game. } @@ -61,7 +59,6 @@ function addPlayerCardImages(cards, websocket, player_id) { // Create a new image element const newCard = document.createElement('img'); - // const card = cards[i].toLowerCase().split(" "); const face = cards[i]["face"] const suit = cards[i]["suit"].toLowerCase(); @@ -75,7 +72,6 @@ function addPlayerCardImages(cards, websocket, player_id) { newCard.width = 100; // Set width newCard.height = 120; // Set height - console.log(player_id) newCard.onclick = () => { const event = { type: "play", @@ -90,8 +86,7 @@ function addPlayerCardImages(cards, websocket, player_id) { } } -function receiveMoves(websocket, player_id) { - const current_player = document.getElementById("current_player"); +function receiveEvents(websocket, player_id) { websocket.addEventListener("message", ({ data }) => { @@ -100,82 +95,46 @@ function receiveMoves(websocket, player_id) { switch (event.type) { case "init": - // Create links for inviting the second player and spectators. - const link = document.createElement("p"); - link.innerText = window.location.href + "?join=" + event.join; - document.body.appendChild(link); - break; - - case "play": - playCard(event, websocket); + // Create a container div for the join link + const joinDiv = document.createElement("div"); + joinDiv.id = "join_link_container"; + + // Add description + const infoText = document.createElement("p"); + infoText.textContent = "Share this link to invite:"; + joinDiv.appendChild(infoText); + + // Add the actual join link + const link = document.createElement("a"); + link.href = window.location.href + "?join=" + event.join; + link.innerText = link.href; + joinDiv.appendChild(link); + + // Append to the sidebar + const sidebar = document.getElementById("sidebar"); + sidebar.appendChild(joinDiv); break; - case "normal": - playCard(event, websocket); + case "start": + player_id.textContent = `Player_id: ${event.player_id}` break; - - case "hold_on": - playCard(event, websocket); + + case "play": + updateCards(event, websocket); break; - - case "general_market": - playCard(event, websocket); - break; - - case "pick_2": - playCard(event, websocket); - break; - - case "pick_3": - playCard(event, websocket); - break; - - case "suspension": - playCard(event, websocket); - break; case "request": - current_player.textContent = `Current Player: ${event.game_state["current_player"]}` - - const opponent2 = Object.keys(event.game_state["players"]).filter(key => key !== event.player_id) - - let div = document.getElementById("i_need"); - - addMiddleCardImage(event.game_state["pile_top"]) - addOponnentCardImages(event.game_state["players"][opponent2[0]]) - addPlayerCardImages(event.game_state["players"][event.player_id], websocket, player_id) - - div.style.visibility = "visible"; - - break; - - case "request_card": - current_player.textContent = `Current Player: ${event.game_state["current_player"]}` - let div2 = document.getElementById("i_need") - div2.style.visibility = "hidden" - showMessage(event.message) + document.getElementById("suitModal").style.display = "flex";; break; case "win": - showMessage(`Player ${event.winner} wins!`); + showNotification(`${event.winner} wins!`); // No further messages are expected; close the WebSocket connection. websocket.close(1000); break; - - case "player_id": - player_id.textContent = `Player_id: ${event.player_id}` - break; - - case "message": - showMessage(event.message) - break; - - case "failed": - console.log(`${event.current_player} failed`) - break; - case "error": - showMessage(event.message); + case "message": + showNotification(event.message) break; default: @@ -184,11 +143,38 @@ function receiveMoves(websocket, player_id) { }); } -function showMessage(message) { - window.setTimeout(() => window.alert(message), 50); +function showNotification(message) { + // Remove any existing notification + const existing = document.querySelector('.notification'); + if (existing) { + existing.remove(); + } + + // Create new notification + const notification = document.createElement('div'); + notification.className = 'notification'; + notification.textContent = message; + + // Add to page + document.body.appendChild(notification); + + // Show with animation + setTimeout(() => { + notification.classList.add('show'); + }, 10); + + // Remove after 15 seconds + setTimeout(() => { + notification.classList.remove('show'); + setTimeout(() => { + if (notification.parentElement) { + notification.remove(); + } + }, 300); + }, 15000); } -function playCard(event, websocket){ +function updateCards(event, websocket) { current_player.textContent = `Current Player: ${event.game_state["current_player"]}` const opponent = Object.keys(event.game_state["players"]).filter(key => key !== event.player_id) @@ -198,64 +184,40 @@ function playCard(event, websocket){ addPlayerCardImages(event.game_state["players"][event.player_id], websocket, player_id) } -function sendMoves(websocket, card, playBtn) { - // Don't send moves for a spectator watching a game. - const params = new URLSearchParams(window.location.search); - if (params.has("watch")) { - return; - } - console.log("Send moves") - // When clicking a column, send a "play" event for a move in that column. - playBtn.addEventListener("click", () => { - if (card.value) { - console.log(card.value) - - const event = { - type: "play", - card: card.value, - }; - websocket.send(JSON.stringify(event)); - } - }); -} - window.addEventListener("DOMContentLoaded", () => { // Open the WebSocket connection and register event handlers. const market = document.getElementById("market"); const player_id = document.getElementById("player_id"); - const i_need = document.getElementById("i_need"); - - console.log("Connecting to WebSocket..."); - console.log(WEBSOCKET_URL) + const modal = document.getElementById("suitModal"); + const websocket = new WebSocket(WEBSOCKET_URL); initGame(websocket); - receiveMoves(websocket, player_id); + receiveEvents(websocket, player_id); + // Add event handling on market card market.onclick = () => { const player = player_id.textContent.split(" ") const event = { type: "market", player_id: player[1] }; - console.log(event) - console.log("Market!!") - websocket.send(JSON.stringify(event)); + websocket.send(JSON.stringify(event)); } - for (const button of i_need.children){ + // Make requests + document.querySelectorAll(".suit-btn").forEach(button => { button.onclick = () => { const event = { type: "request", - suit: button.id + suit: button.id, }; - console.log("Request!!") - i_need.style.visibility = "hidden" - websocket.send(JSON.stringify(event)); - } - } + modal.style.display = "none";; + websocket.send(JSON.stringify(event)); + }; + }); - websocket.onclose = function(event) { + websocket.onclose = function (event) { console.log("WebSocket closed:", event); }; }); diff --git a/examples/whot-web/styles.css b/examples/whot-web/styles.css deleted file mode 100644 index e1cfd4a..0000000 --- a/examples/whot-web/styles.css +++ /dev/null @@ -1,72 +0,0 @@ -/* General board styling */ -#board { - width: 800px; - height: 600px; - margin: 0 auto; - background-image: url('table_top.jpeg'); /* Replace with your image path */ - background-size: cover; /* Ensures the image covers the entire div */ - background-position: center; /* Centers the image */ - background-repeat: no-repeat; /* Prevents the image from repeating */ - border-radius: 20px; - display: flex; - flex-direction: column; - justify-content: space-between; /* Distribute the sections vertically */ - align-items: center; /* Center content horizontally */ - position: relative; -} - -/* Opponent's cards at the top */ -#opponent_cards { - display: flex; - gap: 10px; /* Space between the cards */ - position: absolute; - top: 10px; /* Position at the top */ - left: 50%; - transform: translateX(-50%); -} - -/* Middle area contains middle card and market */ -#middle_area { - display: flex; - gap: 20px; /* Space between the middle card and market */ - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - align-items: center; -} - -/* Middle card */ -#middle_card { - display: flex; - justify-content: center; - align-items: center; -} - -/* Market (draw pile) */ -#market { - display: flex; - justify-content: center; - align-items: center; -} - -/* Player's cards at the bottom */ -#player_cards { - display: flex; - gap: 10px; /* Space between the cards */ - position: absolute; - bottom: 10px; /* Position at the bottom */ - left: 50%; - transform: translateX(-50%); -} - -/* Card images */ -img { - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); /* Adds shadow */ - transition: transform 0.3s ease; /* Smooth hover effect */ -} - -/* Hover effect for cards (optional) */ -img:hover { - transform: scale(1.1); /* Slightly enlarge the card on hover */ -}