diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c77b69 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +https://fastapi.tiangolo.com/advanced/websockets/#await-for-messages-and-send-messages + +```python +fastapi dev server.py +``` diff --git a/app.py b/app.py deleted file mode 100644 index 9030173..0000000 --- a/app.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python - -import asyncio -import itertools -import json - -from websockets.asyncio.server import serve - -from connect4 import PLAYER1, PLAYER2, Connect4 - - - -async def handler(websocket): - # Initialize a Connect Four game. - game = Connect4() - - # Players take alternate turns, using the same browser. - turns = itertools.cycle([PLAYER1, PLAYER2]) - player = next(turns) - - async for message in websocket: - # Parse a "play" event from the UI. - event = json.loads(message) - assert event["type"] == "play" - column = event["column"] - - try: - # Play the move. - row = game.play(player, column) - except ValueError as exc: - # Send an "error" event if the move was illegal. - event = { - "type": "error", - "message": str(exc), - } - await websocket.send(json.dumps(event)) - continue - - # Send a "play" event to update the UI. - event = { - "type": "play", - "player": player, - "column": column, - "row": row, - } - await websocket.send(json.dumps(event)) - - # If move is winning, send a "win" event. - if game.winner is not None: - event = { - "type": "win", - "player": game.winner, - } - await websocket.send(json.dumps(event)) - - # Alternate turns. - player = next(turns) - - -async def main(): - async with serve(handler, "", 8001) as server: - await server.serve_forever() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/connect4.css b/connect4.css deleted file mode 100644 index 27f0baf..0000000 --- a/connect4.css +++ /dev/null @@ -1,105 +0,0 @@ -/* General layout */ - -body { - background-color: white; - display: flex; - flex-direction: column-reverse; - justify-content: center; - align-items: center; - margin: 0; - min-height: 100vh; -} - -/* Action buttons */ - -.actions { - display: flex; - flex-direction: row; - justify-content: space-evenly; - align-items: flex-end; - width: 720px; - height: 100px; -} - -.action { - color: darkgray; - font-family: "Helvetica Neue", sans-serif; - font-size: 20px; - line-height: 20px; - font-weight: 300; - text-align: center; - text-decoration: none; - text-transform: uppercase; - padding: 20px; - width: 120px; -} - -.action:hover { - background-color: darkgray; - color: white; - font-weight: 700; -} - -.action[href=""] { - display: none; -} - -/* Connect Four board */ - -.board { - background-color: blue; - display: flex; - flex-direction: row; - padding: 0 10px; - position: relative; -} - -.board::before, -.board::after { - background-color: blue; - content: ""; - height: 720px; - width: 20px; - position: absolute; -} - -.board::before { - left: -20px; -} - -.board::after { - right: -20px; -} - -.column { - display: flex; - flex-direction: column-reverse; - padding: 10px; -} - -.cell { - border-radius: 50%; - width: 80px; - height: 80px; - margin: 10px 0; -} - -.empty { - background-color: white; -} - -.column:hover .empty { - background-color: lightgray; -} - -.column:hover .empty ~ .empty { - background-color: white; -} - -.red { - background-color: red; -} - -.yellow { - background-color: yellow; -} diff --git a/connect4.js b/connect4.js deleted file mode 100644 index cb5eb9f..0000000 --- a/connect4.js +++ /dev/null @@ -1,45 +0,0 @@ -const PLAYER1 = "red"; - -const PLAYER2 = "yellow"; - -function createBoard(board) { - // Inject stylesheet. - const linkElement = document.createElement("link"); - linkElement.href = import.meta.url.replace(".js", ".css"); - linkElement.rel = "stylesheet"; - document.head.append(linkElement); - // Generate board. - for (let column = 0; column < 7; column++) { - const columnElement = document.createElement("div"); - columnElement.className = "column"; - columnElement.dataset.column = column; - for (let row = 0; row < 6; row++) { - const cellElement = document.createElement("div"); - cellElement.className = "cell empty"; - cellElement.dataset.column = column; - columnElement.append(cellElement); - } - board.append(columnElement); - } -} - -function playMove(board, player, column, row) { - // Check values of arguments. - if (player !== PLAYER1 && player !== PLAYER2) { - throw new Error(`player must be ${PLAYER1} or ${PLAYER2}.`); - } - const columnElement = board.querySelectorAll(".column")[column]; - if (columnElement === undefined) { - throw new RangeError("column must be between 0 and 6."); - } - const cellElement = columnElement.querySelectorAll(".cell")[row]; - if (cellElement === undefined) { - throw new RangeError("row must be between 0 and 5."); - } - // Place checker in cell. - if (!cellElement.classList.replace("empty", player)) { - throw new Error("cell must be empty."); - } -} - -export { PLAYER1, PLAYER2, createBoard, playMove }; diff --git a/connect4.py b/connect4.py deleted file mode 100644 index 1044769..0000000 --- a/connect4.py +++ /dev/null @@ -1,62 +0,0 @@ -__all__ = ["PLAYER1", "PLAYER2", "Connect4"] - -PLAYER1, PLAYER2 = "red", "yellow" - - -class Connect4: - """ - A Connect Four game. - - Play moves with :meth:`play`. - - Get past moves with :attr:`moves`. - - Check for a victory with :attr:`winner`. - - """ - - def __init__(self): - self.moves = [] - self.top = [0 for _ in range(7)] - self.winner = None - - @property - def last_player(self): - """ - Player who played the last move. - - """ - return PLAYER1 if len(self.moves) % 2 else PLAYER2 - - @property - def last_player_won(self): - """ - Whether the last move is winning. - - """ - b = sum(1 << (8 * column + row) for _, column, row in self.moves[::-2]) - return any(b & b >> v & b >> 2 * v & b >> 3 * v for v in [1, 7, 8, 9]) - - def play(self, player, column): - """ - Play a move in a column. - - Returns the row where the checker lands. - - Raises :exc:`ValueError` if the move is illegal. - - """ - if player == self.last_player: - raise ValueError("It isn't your turn.") - - row = self.top[column] - if row == 6: - raise ValueError("This slot is full.") - - self.moves.append((player, column, row)) - self.top[column] += 1 - - if self.winner is None and self.last_player_won: - self.winner = self.last_player - - return row diff --git a/html/index.html b/html/index.html new file mode 100644 index 0000000..c909c40 --- /dev/null +++ b/html/index.html @@ -0,0 +1,41 @@ + + + + Chat + + +

WebSocket Chat

+
+ + + +
+ + +
+ + + + diff --git a/index.html b/index.html deleted file mode 100644 index 8e38e89..0000000 --- a/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - Connect Four - - -
- - - diff --git a/main.js b/main.js deleted file mode 100644 index dd28f9a..0000000 --- a/main.js +++ /dev/null @@ -1,53 +0,0 @@ -import { createBoard, playMove } from "./connect4.js"; - -function showMessage(message) { - window.setTimeout(() => window.alert(message), 50); -} - -function receiveMoves(board, websocket) { - websocket.addEventListener("message", ({ data }) => { - const event = JSON.parse(data); - switch (event.type) { - case "play": - // Update the UI with the move. - playMove(board, event.player, event.column, event.row); - break; - case "win": - showMessage(`Player ${event.player} wins!`); - // No further messages are expected; close the WebSocket connection. - websocket.close(1000); - break; - case "error": - showMessage(event.message); - break; - default: - throw new Error(`Unsupported event type: ${event.type}.`); - } - }); -} - -function sendMoves(board, websocket) { - // When clicking a column, send a "play" event for a move in that column. - board.addEventListener("click", ({ target }) => { - const column = target.dataset.column; - // Ignore clicks outside a column. - if (column === undefined) { - return; - } - const event = { - type: "play", - column: parseInt(column, 10), - }; - websocket.send(JSON.stringify(event)); - }); -} - -window.addEventListener("DOMContentLoaded", () => { - // Initialize the UI. - const board = document.querySelector(".board"); - createBoard(board); - // Open the WebSocket connection and register event handlers. - const websocket = new WebSocket("ws://localhost:8001/"); - receiveMoves(board, websocket); - sendMoves(board, websocket); -}); diff --git a/requrements.txt b/requrements.txt deleted file mode 100644 index 7bc03d7..0000000 --- a/requrements.txt +++ /dev/null @@ -1 +0,0 @@ -websockets==15.0.1 diff --git a/server.py b/server.py new file mode 100644 index 0000000..3162180 --- /dev/null +++ b/server.py @@ -0,0 +1,81 @@ +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.responses import HTMLResponse + +app = FastAPI() + +html = """ + + + + Chat + + +

WebSocket Chat

+

Your ID:

+
+ + +
+ + + + +""" + + +class ConnectionManager: + def __init__(self): + self.active_connections: list[WebSocket] = [] + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + + async def send_personal_message(self, message: str, websocket: WebSocket): + await websocket.send_text(message) + + async def broadcast(self, message: str): + for connection in self.active_connections: + await connection.send_text(message) + + +manager = ConnectionManager() + + +@app.get("/") +async def get(): + return HTMLResponse(html) + + +@app.websocket("/ws/{client_id}") +async def websocket_endpoint(websocket: WebSocket, client_id: int): + await manager.connect(websocket) + try: + while True: + data = await websocket.receive_text() + await manager.send_personal_message(f"You wrote: {data}", websocket) + await manager.broadcast(f"Client #{client_id} says: {data}") + except WebSocketDisconnect: + manager.disconnect(websocket) + await manager.broadcast(f"Client #{client_id} left the chat")