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/.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/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 bcdc5c1..0000000 --- a/examples/whot-web/index.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - Whot Game - - - -

Welcome to the Whot Game

- -

Current Player:

-

Player ID:

- -
-
- -
-
- -
-
- -
-
- -
- -
- -
- New - Join -
- - - - - - - \ No newline at end of file diff --git a/examples/whot-web/main.py b/examples/whot-web/main.py index 64f72ad..37f1a3b 100644 --- a/examples/whot-web/main.py +++ b/examples/whot-web/main.py @@ -1,32 +1,57 @@ -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 +from server import WhotServer + +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() + +# 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("html/game.html", request, context={ + "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, "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_static("/", path=".", name="static") +app.router.add_get("/game", handle_game) +app.router.add_static("/", path="static", 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 deleted file mode 100644 index 38064da..0000000 --- a/examples/whot-web/script.js +++ /dev/null @@ -1,224 +0,0 @@ -function initGame(websocket) { - websocket.addEventListener("open", () => { - // 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. - } - websocket.send(JSON.stringify(event)); - }); -} - -function addMiddleCardImage(text) { - const cardImg = document.getElementById("card_top"); - - const card = text.toLowerCase().split(" "); - - let path; - - if (card[1] != 'whot') { - path = `assets/images/${card[1]}/${card[0]}_${card[1]}.png` - } else { - path = `assets/images/20_whot.png` - } - cardImg.src = path; -} - -function addOponnentCardImages(num_cards) { - const opponent_cards = document.getElementById("opponent_cards"); - - // Remove all children - opponent_cards.replaceChildren(); - - for (let i = 0; i < num_cards; i++) { - // Create a new image element - const newCard = document.createElement('img'); - // Set attributes for the image - newCard.src = 'assets/images/whot_back.png'; // Path to the image - newCard.alt = 'Opponent Card'; // Alternative text - newCard.width = 100; // Set width - newCard.height = 120; // Set height - - // Append the new image to the opponent_cards div - opponent_cards.appendChild(newCard); - } -} - -function addPlayerCardImages(cards, websocket, player_id) { - const player_cards = document.getElementById("player_cards"); - - // Remove all children - player_cards.replaceChildren(); - - for (let i = 0; i < cards.length; i++) { - // Create a new image element - const newCard = document.createElement('img'); - - const card = cards[i].toLowerCase().split(" "); - - // Set attributes for the image - if (card[1] != 'whot') { - newCard.src = `assets/images/${card[1]}/${card[0]}_${card[1]}.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 = { - type: "play", - card: i, - player_id: player_id.textContent.split(" ")[1] - }; - websocket.send(JSON.stringify(event)); - } - - // Append the new image to the opponent_cards div - player_cards.appendChild(newCard); - } -} - -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; - //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}`) - - addMiddleCardImage(event.game_state["pile_top"]) - addOponnentCardImages(event.game_state[opponent]) - addPlayerCardImages(event.game_state[`player_${event.player_id}`], websocket, player_id) - 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}`) - - 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) - - 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) - break; - - case "win": - showMessage(`Player ${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": - console.log(event.message) - break; - - case "failed": - console.log(`${event.current_player} failed`) - break; - - case "error": - showMessage(event.message); - break; - - default: - throw new Error(`Unsupported event type: ${event.type}.`); - } - }); -} - -function showMessage(message) { - window.setTimeout(() => window.alert(message), 50); -} - -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"); - - const websocket = new WebSocket("ws://127.0.0.1:8765/"); - - initGame(websocket); - receiveMoves(websocket, player_id); - - market.onclick = () => { - const player = player_id.textContent.split(" ") - const event = { - type: "market", - player_id: player[1] - }; - console.log("Market!!") - websocket.send(JSON.stringify(event)); - } - - for ( const button of i_need.children){ - button.onclick = () => { - const event = { - type: "request", - suit: button.id - }; - console.log("Request!!") - i_need.style.visibility = "hidden" - websocket.send(JSON.stringify(event)); - } - } -}); diff --git a/examples/whot-web/server.py b/examples/whot-web/server.py index db0e768..647c514 100644 --- a/examples/whot-web/server.py +++ b/examples/whot-web/server.py @@ -2,12 +2,19 @@ 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 - -JOIN = {} +from dataclasses import dataclass class GameConnection: @@ -22,188 +29,343 @@ 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, server_event): + """ + sends message to a particular websocket client + """ + + await self.connections[socket_id].send(json.dumps(server_event)) + + async def broadcast(self, server_event, exceptions = None): + """ + sends message to all websocket client + """ + + # Todo: add exceptions in broadcast + + websockets.broadcast(self.connections.values(), json.dumps(server_event)) + +@dataclass +class Message: + receiver_ids: list[str] + content: str + +class WhotServer: + + def __init__(self): + self.JOIN = {} + + async def start(self, websocket: ClientConnection): + + game = Whot() + gameConnection = GameConnection(game) + + player_id = gameConnection.add_connection(websocket) + + join_key = secrets.token_urlsafe(4) + self.JOIN[join_key] = gameConnection + + try: + server_event = { + "type": "init", + "join": join_key + } + + await websocket.send(json.dumps(server_event)) + await self.play(websocket, game, player_id, gameConnection) + finally: + del self.JOIN[join_key] + + async def join(self, websocket: ClientConnection, join_key): + try: + gameConnection: GameConnection = self.JOIN[join_key] + + server_event = { + "type": "message", + "message": "New Player Joined" + } + + player_id = gameConnection.add_connection(websocket) + + await gameConnection.broadcast(server_event) + + await self.play(websocket, gameConnection.game, player_id, 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, gameConnection: GameConnection): + server_event = { + "type": "start", + "player_id": player_id, + } -async def play(websocket: ClientConnection, game: Whot, player_id: str, gameConnections: GameConnection): - event = { - "type": "player_id", - "player_id": player_id, - } - - await websocket.send(json.dumps(event)) - - while gameConnections.num_of_connections < 2: - await asyncio.sleep(1) + await websocket.send(json.dumps(server_event)) - game.start_game() + while gameConnection.num_of_connections < len(game.players): + await asyncio.sleep(1) - for i, socket in enumerate(gameConnections.connections, start=1): - event = { - "type": "play", - "player_id": i, - "game_state": game.view(f"player_{i}") - } + game.start_game() - await gameConnections.connections[socket].send(json.dumps(event)) + await self.send_event_to_all("play", game, gameConnection) - if game.request_mode == True: - current_player = game.current_player.player_id + if game.request_mode == True: + socket_id = game.current_player.player_id + await self.send_event_to_one(socket_id, "request", game, gameConnection) - event = { - "type": "request", - "player_id": 1, - "game_state": game.view(f"player_1") - } + async for message in websocket: + + client_event = json.loads(message) - await gameConnections.connections[current_player].send(json.dumps(event)) - - - async for message in websocket: - - 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"]) - result = game.play(card_index) - - if result["status"] == "Success": + card_index = int(client_event["card"]) - 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": - await gameConnections.connections[socket].send(json.dumps(event)) - - elif result["status"] == "Failed": - - for i, socket in enumerate(gameConnections.connections, start=1): - event = { - "type": "failed", - "current_player": game.game_state()["current_player"] - } + if result["type"] == "pick_2": + await self.handle_pick_two(game, gameConnection) - await gameConnections.connections[socket].send(json.dumps(event)) + elif result["type"] == "general_market": + await self.handle_general_market(game, gameConnection) + + elif result["type"] == "suspension": + await self.handle_suspension(game, gameConnection) - elif result['status'] == "Request": + elif result["type"] == "hold_on": + await self.handle_hold_on(game, gameConnection) - current_player = game.current_player.player_id + else: + message = Message( + receiver_ids=[game.current_player.player_id], + content="Your turn to play.") + + await self.send_event_to_all("play", game, gameConnection, message) + + elif result['type'] == "request": + await self.handle_request(game, gameConnection) - for i, socket in enumerate(gameConnections.connections, start=1): + elif result['status'] == False: - if socket == current_player: + await self.send_event_to_all("play", game, gameConnection) - event = { - "type": "request", - "player_id": i, - "game_state": game.view(f"player_{i}") + server_event = { + "type": "win", + "winner": result['player_id'], } + game.save("game.json") # Todo: This could be improved + + await gameConnection.broadcast(server_event) + + except GameNotStartedError: + server_event = { + "type": "message", + "message": "Game has not started yet" + } + await websocket.send(json.dumps(server_event)) - else: + except GameOverError: + server_event = { + "type": "message", + "message": "Game is over" + } + await websocket.send(json.dumps(server_event)) - event = { - "type": "play", - "player_id": i, - "game_state": game.view(f"player_{i}") - } + except InvalidMoveError: + server_event = { + "type": "message", + "message": "Invalid move" + } + await websocket.send(json.dumps(server_event)) - await gameConnections.connections[socket].send(json.dumps(event)) + except InvalidCardError: + server_event = { + "type": "message", + "message": "Invalid card" + } + await websocket.send(json.dumps(server_event)) - elif result['status'] == "GameOver": - event = { - "type": "win", - "winner": result['winner'], - } - game.save("game.json") - websockets.broadcast(gameConnections.connections.values(), json.dumps(event)) - else: - event = { - "type": "message", - "message":"It is not your turn" - } - - await websocket.send(json.dumps(event)) + except InvalidSuitError: + server_event = { + "type": "message", + "message": "Invalid suit" + } + await websocket.send(json.dumps(server_event)) - elif event["type"] == "market": + except Exception as e: + server_event = { + "type": "message", + "message": f"An error occurred: {str(e)}" + } + await websocket.send(json.dumps(server_event)) - if event["player_id"] == game.game_state()["current_player"]: + else: + server_event = { + "type": "message", + "message":"It is not your turn" + } + + await websocket.send(json.dumps(server_event)) - game.market() + elif client_event["type"] == "market": + + if client_event["player_id"] == game.game_state()["current_player"]: - for i, socket in enumerate(gameConnections.connections, start=1): - event = { - "type": "play", - "player_id": i, - "game_state": game.view(f"player_{i}") - } + game.market() - await gameConnections.connections[socket].send(json.dumps(event)) + message = Message( + receiver_ids=[game.current_player.player_id], + content="Your turn to play." + ) + + await self.send_event_to_all("play", game, gameConnection, message) - elif event["type"] == "request": - suit = event["suit"] + elif client_event["type"] == "request": + suit = client_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 i, socket in enumerate(gameConnections.connections, start=1): + for socket_id in gameConnection.connections: - if socket != requester: - event = { - "type": "request_card", - "message": f"{requester} requested for {card}", - "game_state": game.view(f"player_{i}") + server_event = { + "type": "play", + "player_id": socket_id, + "game_state": serialize_game_view(game.view(socket_id)) } - await gameConnections.connections[socket].send(json.dumps(event)) + await gameConnection.send(socket_id, server_event) -async def join(websocket: ClientConnection, join_key): - try: - gameConnection: GameConnection = JOIN[join_key] - print("Joined") - except KeyError: - print("Game doesn't exist") - return + if socket_id != requester: + server_event = { + "type": "message", + "message": f"{requester} needs {card}", + } - player_id = gameConnection.add_connection(websocket) + await gameConnection.send(socket_id, server_event) - await play(websocket, gameConnection.game, player_id, gameConnection) + async def handle_pick_two(self, game: Whot, gameConnection: GameConnection): + 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("play", game, gameConnection, message) -async def start(websocket: ClientConnection): - game = Whot(2, number_of_cards=4) - gameConnection = GameConnection(game) - player_id = gameConnection.add_connection(websocket) + 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("play", game, gameConnection, message) + + async def handle_suspension(self, game: Whot, gameConnection: GameConnection): + 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("play", game, gameConnection, message) + + async def handle_hold_on(self, game: Whot, gameConnection: GameConnection): + 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("play", game, gameConnection, message) + + async def handle_request(self, game: Whot, gameConnection: GameConnection): + current_player = game.current_player.player_id - join_key = secrets.token_urlsafe(4) - JOIN[join_key] = gameConnection + for socket_id in gameConnection.connections: - try: - event = { - "type": "init", - "join": join_key + 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 = { + "type": "request", + "player_id": socket_id, + "game_state": serialize_game_view(game.view(socket_id)) + } + + 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: + server_event = { + "type": type, + "player_id": socket_id, + "game_state": serialize_game_view(game.view(socket_id)) + } + + await gameConnection.send(socket_id, server_event) + + if message != None: + if socket_id in message.receiver_ids: + # Notify the player to pick two cards + + server_event = { + "type": "message", + "message": message.content + } + + await gameConnection.send(socket_id, server_event) + + 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(game.view(socket_id)) } - await websocket.send(json.dumps(event)) - await play(websocket, game, player_id, gameConnection) - finally: - del JOIN[join_key] + await gameConnection.send(socket_id, server_event) + + async def handle(self, websocket: ClientConnection): + message = await websocket.recv() + client_event = json.loads(message) -async def handler(websocket: ClientConnection): + assert client_event["type"] == "init" - 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 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/static/js/script.js b/examples/whot-web/static/js/script.js new file mode 100644 index 0000000..79991ab --- /dev/null +++ b/examples/whot-web/static/js/script.js @@ -0,0 +1,223 @@ +function initGame(websocket) { + websocket.addEventListener("open", () => { + // 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 { + // First player starts a new game. + } + websocket.send(JSON.stringify(event)); + }); +} + +function addMiddleCardImage(text) { + const cardImg = document.getElementById("card_top"); + const face = text["face"] + const suit = text["suit"].toLowerCase(); + + let path; + + if (suit != 'whot') { + path = `assets/images/${suit}/${face}_${suit}.png` + } else { + path = `assets/images/20_whot.png` + } + cardImg.src = path; +} + +function addOponnentCardImages(num_cards) { + const opponent_cards = document.getElementById("opponent_cards"); + + // Remove all children + opponent_cards.replaceChildren(); + + for (let i = 0; i < num_cards; i++) { + // Create a new image element + const newCard = document.createElement('img'); + // Set attributes for the image + newCard.src = 'assets/images/whot_back.png'; // Path to the image + newCard.alt = 'Opponent Card'; // Alternative text + newCard.width = 100; // Set width + newCard.height = 120; // Set height + + // Append the new image to the opponent_cards div + opponent_cards.appendChild(newCard); + } +} + +function addPlayerCardImages(cards, websocket, player_id) { + const player_cards = document.getElementById("player_cards"); + + // Remove all children + player_cards.replaceChildren(); + + for (let i = 0; i < cards.length; i++) { + // Create a new image element + const newCard = document.createElement('img'); + + const face = cards[i]["face"] + const suit = cards[i]["suit"].toLowerCase(); + + // Set attributes for 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 + + newCard.onclick = () => { + const event = { + type: "play", + card: i, + player_id: player_id.textContent.split(" ")[1] + }; + websocket.send(JSON.stringify(event)); + } + + // Append the new image to the opponent_cards div + player_cards.appendChild(newCard); + } +} + +function receiveEvents(websocket, player_id) { + + websocket.addEventListener("message", ({ data }) => { + + const event = JSON.parse(data); + + switch (event.type) { + + case "init": + // 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 "start": + player_id.textContent = `Player_id: ${event.player_id}` + break; + + case "play": + updateCards(event, websocket); + break; + + case "request": + document.getElementById("suitModal").style.display = "flex";; + break; + + case "win": + showNotification(`${event.winner} wins!`); + // No further messages are expected; close the WebSocket connection. + websocket.close(1000); + break; + + case "message": + showNotification(event.message) + break; + + default: + throw new Error(`Unsupported event type: ${event.type}.`); + } + }); +} + +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 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) + + addMiddleCardImage(event.game_state["pile_top"]) + addOponnentCardImages(event.game_state["players"][opponent[0]]) + addPlayerCardImages(event.game_state["players"][event.player_id], websocket, player_id) +} + +window.addEventListener("DOMContentLoaded", () => { + // Open the WebSocket connection and register event handlers. + const market = document.getElementById("market"); + const player_id = document.getElementById("player_id"); + const modal = document.getElementById("suitModal"); + + const websocket = new WebSocket(WEBSOCKET_URL); + + initGame(websocket); + 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] + }; + websocket.send(JSON.stringify(event)); + } + + // Make requests + document.querySelectorAll(".suit-btn").forEach(button => { + button.onclick = () => { + const event = { + type: "request", + suit: button.id, + }; + modal.style.display = "none";; + websocket.send(JSON.stringify(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 */ -} 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):