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