Compare commits

..

8 Commits

Author SHA1 Message Date
0880
83b570214a Finishing game 2026-01-18 19:07:25 +03:30
0880
460255db9e Send UTC time 2026-01-18 19:06:57 +03:30
0880
56a01ac5a7 Handle finishing game 2026-01-18 19:06:03 +03:30
0880
4f6a45cbc7 UI Update 2026-01-18 19:05:38 +03:30
0880
4a998bca84 UI & SFX 2026-01-18 19:05:26 +03:30
0880
cc9cf0f1c2 Favicon 2026-01-18 19:04:57 +03:30
0880
3d7508fc29 SFX 2026-01-18 19:04:45 +03:30
0880
d2ed2b2842 Favicon 2026-01-18 19:03:06 +03:30
8 changed files with 176 additions and 11 deletions

62
app.py
View File

@@ -2,7 +2,7 @@ import asyncio
import json import json
import uuid import uuid
from collections.abc import Iterator from collections.abc import Iterator
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from enum import Enum from enum import Enum
from itertools import product from itertools import product
from pathlib import Path from pathlib import Path
@@ -166,13 +166,13 @@ class Room:
self.turn = Color.WHITE self.turn = Color.WHITE
self.board = Board() self.board = Board()
self.players = [] self.players = []
self.last_move = datetime.now() self.last_move = datetime.now(timezone.utc)
self.game_start = None self.game_start = None
self.state = State.NOT_FINISHED self.state = State.NOT_FINISHED
def start(self): def start(self):
self.last_move = datetime.now() self.last_move = datetime.now(timezone.utc)
self.game_start = datetime.now() self.game_start = datetime.now(timezone.utc)
def add_player(self) -> None | tuple[str, Color]: def add_player(self) -> None | tuple[str, Color]:
np: int = len(self.players) np: int = len(self.players)
@@ -192,6 +192,20 @@ class Room:
rooms: dict[str, Room] = {} rooms: dict[str, Room] = {}
favicon = Path("favicon.ico").read_bytes()
favicon_update = Path("favicon_update.ico").read_bytes()
@app.GET("/favicon.ico")
async def favicon_s(request):
return HTTPResponse(request, favicon, content_type="image/x-icon")
@app.GET("/favicon_update.ico")
async def favicon_update_s(request):
return HTTPResponse(request, favicon_update, content_type="image/x-icon")
@app.GET("/") @app.GET("/")
async def home(request): async def home(request):
return render(request, "index.html") return render(request, "index.html")
@@ -218,12 +232,25 @@ async def join(request, room_id):
"turn": room.turn.value, "turn": room.turn.value,
"board": room.board.serialize(), "board": room.board.serialize(),
"ready": len(room.players) == 2, "ready": len(room.players) == 2,
"state": room.state.value,
"start_time": (
room.game_start or datetime.now(timezone.utc)
).isoformat(),
}, },
) )
else: else:
return JSONResponse( return JSONResponse(
request, request,
{"code": "FULL", "error": "Room Full", "board": room.board.serialize()}, {
"code": "FULL",
"error": "Room Full",
"board": room.board.serialize(),
"turn": room.turn.value,
"state": room.state.value,
"start_time": (
room.game_start or datetime.now(timezone.utc)
).isoformat(),
},
) )
@@ -392,6 +419,8 @@ async def move(request: Request, room_id):
room = rooms[room_id] room = rooms[room_id]
if len(room.players) != 2: if len(room.players) != 2:
return 400, {"code": "PRES", "error": "Game has not been started"} return 400, {"code": "PRES", "error": "Game has not been started"}
if room.state != State.NOT_FINISHED:
return 400, {"code": "FINI", "error": "Game has not finished."}
board = room.board board = room.board
uid = req["uid"] uid = req["uid"]
src = req["from"].lower() src = req["from"].lower()
@@ -429,11 +458,27 @@ async def move(request: Request, room_id):
if c.y == 0 or c.y == 7 and piece_kind == "p": if c.y == 0 or c.y == 7 and piece_kind == "p":
board.grid[c.y][c.x] = Piece.WHITE_QUEEN if is_white else Piece.BLACK_QUEEN board.grid[c.y][c.x] = Piece.WHITE_QUEEN if is_white else Piece.BLACK_QUEEN
room.turn = Color.BLACK if color == Color.WHITE else Color.WHITE room.turn = Color.BLACK if color == Color.WHITE else Color.WHITE
room.last_move = datetime.now() room.last_move = datetime.now(timezone.utc)
opp_checkmate = True
for i in range(8):
for j in range(8):
P = board.index_xy(i, j)
if P != "E" and P.value.isupper() != is_white:
moves = generate_valid_moves(
P.value.lower(), board, not is_white, xy_to_pos_safe(i, j)
)
if len(moves) > 0:
opp_checkmate = False
break
if not opp_checkmate:
break
if opp_checkmate:
room.state = State.BLACK_WIN if is_white else State.WHITE_WIN
return { return {
"code": "MOVD", "code": "MOVD",
"color": color.value, "color": color.value,
"turn": room.turn.value, "turn": room.turn.value,
"state": room.state.value,
"board": board.serialize(), "board": board.serialize(),
} }
else: else:
@@ -445,11 +490,11 @@ async def move(request: Request, room_id):
async def poll(request, room_id): async def poll(request, room_id):
for key in rooms: for key in rooms:
room = rooms[key] room = rooms[key]
if (datetime.now() - room.last_move) >= timedelta(hours=24): if (datetime.now(timezone.utc) - room.last_move) >= timedelta(hours=24):
del rooms[key] del rooms[key]
if ( if (
room.game_start room.game_start
and (datetime.now() - room.game_start) >= timedelta(minutes=30) and (datetime.now(timezone.utc) - room.game_start) >= timedelta(minutes=30)
and room.state == State.NOT_FINISHED and room.state == State.NOT_FINISHED
): ):
room.state = State.TIE room.state = State.TIE
@@ -462,6 +507,7 @@ async def poll(request, room_id):
"board": room.board.serialize(), "board": room.board.serialize(),
"ready": len(room.players) == 2, "ready": len(room.players) == 2,
"state": room.state.value, "state": room.state.value,
"start_time": (room.game_start or datetime.now(timezone.utc)).isoformat(),
} }

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
favicon_update.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
icon_default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
icon_update.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -28,8 +28,32 @@
.center { .center {
display: flex; display: flex;
justify-content: center; flex-direction: column;
align-items: center;
margin: 24px 0; margin: 24px 0;
gap: 8px;
}
#stats {
font-family: 'Times New Roman', Times, serif;
font-weight: 100;
font-size: 32px;
color: #000;
}
#clock {
font-family: 'Times New Roman', Times, serif;
font-weight: 100;
font-size: 32px;
color: #000;
letter-spacing: 6px;
}
#turn {
border-top: 1px solid #68687aaa;
padding-top: 4px;
font-size: 14px;
color: #68687aaa;
} }
.join-menu { .join-menu {
@@ -80,6 +104,7 @@
} }
} }
</style> </style>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
</head> </head>
<body> <body>
@@ -89,6 +114,9 @@
<div class="center"> <div class="center">
<div id="join-menu" class="join-menu"><input type="text" id="room-id" placeholder="Room ID"><button <div id="join-menu" class="join-menu"><input type="text" id="room-id" placeholder="Room ID"><button
id="join-submit">Join</button></div> id="join-submit">Join</button></div>
<h2 id="stats"></h2>
<h2 id="clock"></h2>
<h4 id="turn"></h4>
</div> </div>
<div class="center"> <div class="center">
<canvas class="game" id="canvas"></canvas> <canvas class="game" id="canvas"></canvas>
@@ -112,11 +140,17 @@
let mHouse = [0, 0]; let mHouse = [0, 0];
const textures = {}; const textures = {};
const piecemoveSound = new Audio("/static/piecemove.mp3");
//const endgameSound = new Audio("/static/endgame.mp3");
let color = 0; let color = 0;
let turn = 0; let turn = 0;
let state = -1;
let start_time = undefined;
let ready = false; let ready = false;
let letterMap = ' '; let letterMap = ' ';
let numberMap = ' '; let numberMap = ' ';
let oldBoard = undefined;
let board = undefined; let board = undefined;
let UID = undefined; let UID = undefined;
let ROOM_ID = undefined; let ROOM_ID = undefined;
@@ -124,6 +158,82 @@
let selected = undefined; let selected = undefined;
let moves = []; let moves = [];
function handleVisibilityChange() {
if (document.visibilityState === 'visible') {
let link = document.querySelector("link[rel~='icon']");
link.href = "/favicon.ico";
link.type = "image/x-icon";
}
}
document.addEventListener('visibilitychange', handleVisibilityChange);
function boardOnChange() {
if (JSON.stringify(board) !== JSON.stringify(oldBoard)) {
if (document.visibilityState !== 'visible') {
let link = document.querySelector("link[rel~='icon']");
link.href = "/favicon_update.ico";
link.type = "image/x-icon";
}
console.log("PLAYING SOUND!!");
piecemoveSound.currentTime = Math.round(Math.random() * 4) * 0.5;
piecemoveSound.play()
.then(() => {
setTimeout(() => {
piecemoveSound.pause();
}, 400);
})
.catch(error => {
console.error("Playback failed:", error);
});
oldBoard = structuredClone(board);
}
}
function setUI() {
if (ready) {
if (state == -1) {
document.getElementById('stats').textCotnent = "";
const now = new Date();
const differenceMs = Math.abs(start_time.getTime() - now.getTime());
const totalSeconds = Math.floor(differenceMs / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
const paddedMinutes = String(minutes).padStart(2, '0');
const paddedSeconds = String(seconds).padStart(2, '0');
document.getElementById('clock').textCotnent = `${paddedMinutes}:${paddedSeconds}`;
if (UID === undefined || turn != color) {
if (turn == 0) {
document.getElementById('turn').textCotnent = "White's Turn";
} else if (turn == 0) {
document.getElementById('turn').textCotnent = "Black's Turn";
}
} else if (turn == color) {
document.getElementById('turn').textCotnent = "Your Turn";
}
} else {
document.getElementById('turn').textCotnent = "";
if (state == 0) {
document.getElementById('stats').textCotnent = "Tie";
} else if (state == 1) {
document.getElementById('stats').textCotnent = "White Won";
} else if (state == 2) {
document.getElementById('stats').textCotnent = "Black Won";
}
}
} else {
document.getElementById('turn').textCotnent = "";
document.getElementById('clock').textCotnent = "";
document.getElementById('stats').textCotnent = "Waiting for Opponent";
}
}
canvas.addEventListener('mousemove', (e) => { canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * width; const x = ((e.clientX - rect.left) / rect.width) * width;
@@ -131,7 +241,7 @@
mouse = [x, y]; mouse = [x, y];
i = Math.floor((x / width) * 8); i = Math.floor((x / width) * 8);
j = Math.floor((y / height) * 8); j = Math.floor((y / height) * 8);
if (ready) { if (ready && state == -1) {
canvas.style.cursor = 'default'; canvas.style.cursor = 'default';
if (board) { if (board) {
if (color == 0) { if (color == 0) {
@@ -157,7 +267,7 @@
canvas.addEventListener('mouseup', (e) => { canvas.addEventListener('mouseup', (e) => {
i = mHouse[0]; i = mHouse[0];
j = mHouse[1]; j = mHouse[1];
if (ready && board) { if (state == -1 && ready && board) {
if (board[j][i] !== "E" && (board[j][i] === board[j][i].toUpperCase() ? 0 : 1) === color && turn == color) { if (board[j][i] !== "E" && (board[j][i] === board[j][i].toUpperCase() ? 0 : 1) === color && turn == color) {
selected = [i, j]; selected = [i, j];
@@ -192,6 +302,7 @@
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
board = data.board.grid; board = data.board.grid;
boardOnChange();
console.log(data); console.log(data);
selected = undefined; selected = undefined;
moves = []; moves = [];
@@ -216,6 +327,11 @@
ready = data.ready; ready = data.ready;
turn = data.turn; turn = data.turn;
board = data.board.grid; board = data.board.grid;
boardOnChange();
state = data.state;
start_time = new Date(data.start_time);
setUI();
}) })
.catch((error) => console.error('Error:', error)); .catch((error) => console.error('Error:', error));
} }
@@ -252,11 +368,14 @@
color = data.color; color = data.color;
ready = data.ready; ready = data.ready;
turn = data.turn; turn = data.turn;
state = data.state;
start_time = new Date(data.start_time);
} }
ROOM_ID = rid; ROOM_ID = rid;
board = data.board.grid; board = data.board.grid;
numberMap = "12345678"; numberMap = "12345678";
letterMap = "abcdefgh"; letterMap = "abcdefgh";
setUI();
document.getElementById('join-menu').outerHTML = ''; document.getElementById('join-menu').outerHTML = '';
const urlID = extractIdFromPath(); const urlID = extractIdFromPath();
if (urlID == null) { if (urlID == null) {

BIN
static/endgame.mp3 Normal file

Binary file not shown.

BIN
static/piecemove.mp3 Normal file

Binary file not shown.