first commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.venv
|
||||
.idea
|
||||
__pycache__
|
||||
65
app.py
Normal file
65
app.py
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/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())
|
||||
105
connect4.css
Normal file
105
connect4.css
Normal file
@@ -0,0 +1,105 @@
|
||||
/* 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;
|
||||
}
|
||||
45
connect4.js
Normal file
45
connect4.js
Normal file
@@ -0,0 +1,45 @@
|
||||
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 };
|
||||
62
connect4.py
Normal file
62
connect4.py
Normal file
@@ -0,0 +1,62 @@
|
||||
__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
|
||||
10
index.html
Normal file
10
index.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Connect Four</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="board"></div>
|
||||
<script src="main.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
53
main.js
Normal file
53
main.js
Normal file
@@ -0,0 +1,53 @@
|
||||
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);
|
||||
});
|
||||
1
requrements.txt
Normal file
1
requrements.txt
Normal file
@@ -0,0 +1 @@
|
||||
websockets==15.0.1
|
||||
Reference in New Issue
Block a user