Compare commits

...

43 Commits

Author SHA1 Message Date
0880
694ccc14a4 Fix variable name mistake couldn't join 2026-01-21 02:12:39 +03:30
0880
6ee7a435e0 Set inner text for clearing stats 2026-01-21 02:10:56 +03:30
0880
1d5b449d6d Fix spectator mouse 2026-01-21 02:10:24 +03:30
0880
e0af5c2d33 Fix UI turn display 2026-01-21 02:08:12 +03:30
0880
78ed9e1daf FIX huge mistake 2026-01-21 02:07:03 +03:30
0880
1a81ae7f43 Update SlowAPI 2026-01-21 02:06:54 +03:30
0880
b75e03cd3e Change matchmaking update logic condition 2026-01-20 19:59:08 +03:30
0880
c367ebad1f Quick matchmaking API 2026-01-20 19:57:38 +03:30
0880
17d66398b4 imports 2026-01-20 19:57:19 +03:30
0880
4240312603 Don't allow automatic room creation 2026-01-20 19:57:01 +03:30
0880
5c268911b4 Add home page 2026-01-20 19:55:31 +03:30
0880
2599f50484 redirect 2026-01-20 19:55:17 +03:30
0880
e220431041 Move index.html to game.html 2026-01-20 19:54:57 +03:30
0880
baac9b3e74 Fix king protection 2026-01-19 18:30:09 +03:30
0880
4daf04d340 Fix capturing king 2026-01-19 18:18:15 +03:30
0880
f19638a4e6 Fix pawn upgrade 2026-01-19 18:14:37 +03:30
0880
fe7884233c Fix UI 2026-01-18 19:13:25 +03:30
0880
83a837f339 Remove debug print 2026-01-18 19:13:18 +03:30
0880
250ef92502 Send ready to spectators 2026-01-18 19:12:59 +03:30
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
0880
bfe6c47d8e Update SlowAPI 2026-01-17 19:32:33 +03:30
0880
539c739c76 Fix a bunch of bugs 2026-01-17 19:29:13 +03:30
0880
dd91a8723b Remove API_ROOT 2026-01-17 18:56:21 +03:30
0880
975e813947 Remove API_ROOT 2026-01-17 18:56:03 +03:30
0880
467b7d4706 Remove debug 2026-01-17 18:55:42 +03:30
0880
14075ee2a3 Update 2026-01-17 18:55:31 +03:30
0880
adcda176cc Update SlowAPI and remove hacky HTTP response 2026-01-17 18:32:37 +03:30
0880
21e298ea9f Updates 2026-01-17 18:18:16 +03:30
0880
fe84972374 Use optimized images 2026-01-17 15:30:52 +03:30
0880
ebfe70f0bb Optimize images 2026-01-17 15:30:32 +03:30
0880
6e61b4d3d0 Remove debug logs 2026-01-17 15:28:31 +03:30
0880
1d7defd069 Make game responsive 2026-01-17 15:27:11 +03:30
0880
8e14646806 Move slowAPI to submodule 2026-01-17 15:13:58 +03:30
0880
af254864bd Move slowAPI to submodule 2026-01-17 15:13:42 +03:30
0880
5beeed465e Add joining by ID 2026-01-17 02:01:19 +03:30
0880
f9e2f846f6 Change port 2026-01-17 02:00:28 +03:30
27 changed files with 523 additions and 329 deletions

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "libs"]
path = libs
url = http://chai.bokhary.ir/0880/SlowAPI.git

220
app.py
View File

@@ -1,14 +1,25 @@
import asyncio import asyncio
import json import json
import random
import re
import string
import uuid import uuid
from collections import deque
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
from typing import Any from typing import Any
from slow import JSONAPI, App, HTTPResponse, JSONResponse, Request, render from libs.slow import (
JSONAPI,
App,
Request,
redirect,
render,
)
from libs.slow.responses import HTTPResponse, JSONResponse
class Coord: class Coord:
@@ -27,7 +38,7 @@ class Coord:
return Coord(self.x, self.y) return Coord(self.x, self.y)
def __str__(self): def __str__(self):
return coord_to_pos_safe(self) return str(coord_to_pos(self))
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, Coord): if isinstance(other, Coord):
@@ -159,17 +170,20 @@ class Room:
turn: Color turn: Color
players: list[str] players: list[str]
last_move: datetime last_move: datetime
game_start: datetime | None
state: State state: State
def __init__(self): def __init__(self):
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.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(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)
@@ -189,19 +203,114 @@ 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")
letters = string.ascii_lowercase + string.digits
def room_key():
key: str = "".join(random.choices(letters, k=4))
while key in rooms.keys():
key: str = "".join(random.choices(letters, k=4))
return key
@app.POST("/create_room")
async def new_room(request):
if len(rooms) == len(letters) ** 4:
return HTTPResponse("Out of service", status=501)
key = room_key()
rooms[key] = Room()
return JSONResponse(
request,
{
"id": key,
},
)
quick_queue: deque[str]
quick_map: dict[str, Room]
lock = asyncio.Lock()
@app.POST("/quick")
async def quick_match(request: Request):
global quick_queue, quick_map
data = parse(request.body)
async with lock:
if (
data
and len(data) == 1
and "queue_id" in data
and data["queue_id"] in quick_queue
):
first = None
second = None
position: int = 0
# UPDATE LOGIC
while position < len(quick_queue):
if quick_queue[position] not in quick_map:
if not first:
first = quick_queue[position]
else:
second = quick_queue[position]
break
position += 1
if first and second:
room = Room()
k = room_key()
rooms[k] = room
quick_map[first] = room
quick_map[second] = room
qid = data["queue_id"]
if qid in quick_map:
return JSONResponse({"room_id": quick_map[qid]})
else:
return JSONResponse({}) # Client handles empty as continue to wait
else:
qid = str(uuid.uuid4())
quick_queue.append(qid)
return JSONResponse({"queue_id": qid})
@app.GET("/") @app.GET("/")
async def home(request): async def home(request):
return render("index.html", {"API_ROOT": "http://127.0.0.1:8080"}) return render("home.html")
@app.GET("/<id>")
async def game(request, id):
if not re.match(r"[a-z0-9]{4}", id):
return redirect("/")
if id not in rooms:
return redirect("/")
return render("game.html")
@app.POST("/join/<room_id>") @app.POST("/join/<room_id>")
async def join(request, room_id): async def join(request, room_id):
if room_id not in rooms: if room_id not in rooms:
rooms[room_id] = Room() return JSONResponse({"code": "NOGO", "error": "Room not found."}, 404)
room: Room = rooms[room_id] room: Room = rooms[room_id]
player = room.add_player() player = room.add_player()
if player: if player:
return JSONResponse( return JSONResponse(
request,
{ {
"code": "JOIN", "code": "JOIN",
"id": player[0], "id": player[0],
@@ -209,11 +318,26 @@ 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(
{"code": "FULL", "error": "Room Full", "board": room.board.serialize()} request,
{
"code": "FULL",
"error": "Room Full",
"board": room.board.serialize(),
"ready": len(room.players) == 2,
"turn": room.turn.value,
"state": room.state.value,
"start_time": (
room.game_start or datetime.now(timezone.utc)
).isoformat(),
},
) )
@@ -236,15 +360,15 @@ def get_piece_moves(piece_kind, board: Board, is_white, src: str) -> list[Coord]
if ( if (
board.index_xy(x + 1, y + dir) board.index_xy(x + 1, y + dir)
not in [Piece.EMPTY, Piece.NONE, Piece.BLACK_KING, Piece.WHITE_KING] not in [Piece.EMPTY, Piece.NONE, Piece.BLACK_KING, Piece.WHITE_KING]
and board.index_xy(x + 1, y + dir).value.upper() != is_white and board.index_xy(x + 1, y + dir).value.isupper() != is_white
): ):
valids.append(Coord(x=x + 1, y=y + 1)) valids.append(Coord(x=x + 1, y=y + dir))
if ( if (
board.index_xy(x - 1, y + dir) board.index_xy(x - 1, y + dir)
not in [Piece.EMPTY, Piece.NONE, Piece.BLACK_KING, Piece.WHITE_KING] not in [Piece.EMPTY, Piece.NONE, Piece.BLACK_KING, Piece.WHITE_KING]
and board.index_xy(x - 1, y + dir).value.upper() != is_white and board.index_xy(x - 1, y + dir).value.isupper() != is_white
): ):
valids.append(Coord(x=x - 1, y=y + 1)) valids.append(Coord(x=x - 1, y=y + dir))
elif piece_kind == "b": elif piece_kind == "b":
x, y = pos_to_coord(src) x, y = pos_to_coord(src)
comb = product([1, -1], repeat=2) comb = product([1, -1], repeat=2)
@@ -302,28 +426,31 @@ def get_piece_moves(piece_kind, board: Board, is_white, src: str) -> list[Coord]
if dir[0] ** 2 + dir[1] ** 2 == 0: if dir[0] ** 2 + dir[1] ** 2 == 0:
continue # x=0 y=0 cannot move => invalid continue # x=0 y=0 cannot move => invalid
target: Coord = Coord(x=x + dir[0], y=y + dir[1]) target: Coord = Coord(x=x + dir[0], y=y + dir[1])
if ( if (p := board.index_coord(target)) != Piece.NONE and (
p := board.index_coord(target) p.value.isupper() != is_white or p.value == "E"
) != Piece.NONE and p.value.isupper() != is_white: ):
valids.append(target.copy()) valids.append(target.copy())
elif piece_kind == "r": elif piece_kind == "r":
x, y = pos_to_coord(src) x, y = pos_to_coord(src)
moves = [(2, 1), (2, -1), (-2, 1), (-2, -1), (1, 2), (-1, 2), (1, -2), (-1, -2)] moves = [(2, 1), (2, -1), (-2, 1), (-2, -1), (1, 2), (-1, 2), (1, -2), (-1, -2)]
for m in moves: for m in moves:
target: Coord = Coord(x=x + m[0], y=y + m[1]) target: Coord = Coord(x=x + m[0], y=y + m[1])
if ( if (p := board.index_coord(target)) != Piece.NONE and (
p := board.index_coord(target) p.value.isupper() != is_white or p.value == "E"
) != Piece.NONE and p.value.isupper() != is_white: ):
valids.append(target.copy()) valids.append(target.copy())
v_temp = [a for a in valids]
valids.clear()
for a in v_temp:
if board.index_coord(a).value.lower != "k":
valids.append(a)
return valids return valids
def generate_valid_moves( def generate_valid_moves(
piece_kind: str, board: Board, is_white: bool, src: str piece_kind: str, board: Board, is_white: bool, src: str
) -> list[Coord]: ) -> list[Coord]:
print(" -- FIRST ORDER ---------------------------")
possible_moves: list[Coord] = get_piece_moves(piece_kind, board, is_white, src) possible_moves: list[Coord] = get_piece_moves(piece_kind, board, is_white, src)
print(" -- FIRST ORDER ---------------------------")
valids: list[Coord] = [] valids: list[Coord] = []
king: Coord king: Coord
for i in range(8): for i in range(8):
@@ -344,7 +471,7 @@ def generate_valid_moves(
king_safe = True king_safe = True
for i in range(8): for i in range(8):
for j in range(8): for j in range(8):
p = board.grid[i][j] p = board.grid[j][i]
if p != Piece.EMPTY and p.value.isupper() != is_white: if p != Piece.EMPTY and p.value.isupper() != is_white:
# Enemy # Enemy
enemy_moves = get_piece_moves( enemy_moves = get_piece_moves(
@@ -352,8 +479,16 @@ def generate_valid_moves(
) )
if king not in enemy_moves: if king not in enemy_moves:
continue continue
ni = i
nj = j
if j == sx and i == sy:
ni = m.y
nj = m.x
new_enemy_moves = get_piece_moves( new_enemy_moves = get_piece_moves(
p.value.lower(), fake_board, not is_white, xy_to_pos_safe(j, i) p.value.lower(),
fake_board,
not is_white,
xy_to_pos_safe(nj, ni),
) )
if king in new_enemy_moves: if king in new_enemy_moves:
king_safe = False king_safe = False
@@ -384,6 +519,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()
@@ -418,14 +555,30 @@ async def move(request: Request, room_id):
board.grid[c.y][c.x] = srcp board.grid[c.y][c.x] = srcp
sx, sy = pos_to_coord(src) sx, sy = pos_to_coord(src)
board.grid[sy][sx] = Piece.EMPTY board.grid[sy][sx] = Piece.EMPTY
if c.y == 0 or c.y == 7 and srcp in [Piece.BLACK_PAWN, Piece.WHITE_PAWN]: 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:
@@ -437,8 +590,14 @@ 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 (
room.game_start
and (datetime.now(timezone.utc) - room.game_start) >= timedelta(minutes=30)
and room.state == State.NOT_FINISHED
):
room.state = State.TIE
if room_id not in rooms: if room_id not in rooms:
return 400, {"code": "NOEX", "error": "Room does not exist"} return 400, {"code": "NOEX", "error": "Room does not exist"}
room = rooms[room_id] room = rooms[room_id]
@@ -448,6 +607,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(),
} }
@@ -500,13 +660,11 @@ def sanitize_filename(filename: str, base: Path = Path.cwd().resolve()) -> Path:
async def static(request, fn): async def static(request, fn):
try: try:
path = sanitize_filename(fn, Path("static/").resolve()) path = sanitize_filename(fn, Path("static/").resolve())
return (
HTTPResponse("", content_type="application/octet-stream") return HTTPResponse(path.read_bytes(), content_type="application/octet-stream")
+ path.read_bytes()
)
except Exception: except Exception:
return HTTPResponse("404 File Not Found", status=404) return HTTPResponse("404 File Not Found", status=404)
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(app.run(port=8080)) asyncio.run(app.run(port=8989))

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

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 {
@@ -70,18 +94,29 @@
.game { .game {
width: 600px; width: 600px;
height: 600px; aspect-ratio: 1/1;
}
@media (max-width: 768px) {
.game {
width: 80%;
aspect-ratio: 1/1;
}
} }
</style> </style>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
</head> </head>
<body> <body>
<header> <header>
<img src="/static/logo.png" alt=""> <img src="/static/logo.webp" alt="">
</header> </header>
<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" style="display: none;"><input type="text" id="room-id"
id="join-submit">Join</button></div> placeholder="Room ID"><button 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>
@@ -105,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;
@@ -117,6 +158,81 @@
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";
}
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').innerText = "";
const now = new Date();
const differenceMs = 30 * 60 * 1000 - 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').innerText = `${paddedMinutes}:${paddedSeconds}`;
if (UID === undefined || turn != color) {
if (turn == 0) {
document.getElementById('turn').innerText = "White's Turn";
} else if (turn == 1) {
document.getElementById('turn').innerText = "Black's Turn";
}
} else if (turn == color) {
document.getElementById('turn').innerText = "Your Turn";
}
} else {
document.getElementById('turn').innerText = "";
if (state == 0) {
document.getElementById('stats').innerText = "Tie";
} else if (state == 1) {
document.getElementById('stats').innerText = "White Won";
} else if (state == 2) {
document.getElementById('stats').innerText = "Black Won";
}
}
} else {
document.getElementById('turn').innerText = "";
document.getElementById('clock').innerText = "";
document.getElementById('stats').innerText = "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;
@@ -124,7 +240,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 && UID !== undefined) {
canvas.style.cursor = 'default'; canvas.style.cursor = 'default';
if (board) { if (board) {
if (color == 0) { if (color == 0) {
@@ -150,15 +266,12 @@
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) {
console.log(turn);
console.log(color);
console.log(selected);
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];
moves = []; moves = [];
fetch('{{ API_ROOT }}/moves/' + ROOM_ID, { fetch('/moves/' + ROOM_ID, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -178,8 +291,7 @@
moves.forEach(move => { moves.forEach(move => {
console.log(move); console.log(move);
if (move[0] == i && move[1] == j) { if (move[0] == i && move[1] == j) {
console.log("YAY"); fetch('/move/' + ROOM_ID, {
fetch('{{ API_ROOT }}/move/' + ROOM_ID, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -189,6 +301,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 = [];
@@ -205,7 +318,7 @@
function update() { function update() {
if (ROOM_ID) { if (ROOM_ID) {
fetch('{{ API_ROOT }}/poll/' + ROOM_ID, { fetch('/poll/' + ROOM_ID, {
method: 'GET', method: 'GET',
}) })
.then((response) => response.json()) .then((response) => response.json())
@@ -213,6 +326,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));
} }
@@ -237,7 +355,7 @@
function join(rid) { function join(rid) {
if (rid.length > 0 && /^[a-zA-Z0-9_-]+$/.test(rid)) { if (rid.length > 0 && /^[a-zA-Z0-9_-]+$/.test(rid)) {
fetch('{{ API_ROOT }}/join/' + rid, { fetch('/join/' + rid, {
method: 'POST', method: 'POST',
}) })
.then((response) => response.json()) .then((response) => response.json())
@@ -249,11 +367,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) {
@@ -349,19 +470,19 @@
} }
const assets = { const assets = {
p: '/static/black_pawn.png', p: '/static/black_pawn.webp',
r: '/static/black_rook.png', r: '/static/black_rook.webp',
c: '/static/black_castle.png', c: '/static/black_castle.webp',
b: '/static/black_bishop.png', b: '/static/black_bishop.webp',
k: '/static/black_king.png', k: '/static/black_king.webp',
q: '/static/black_queen.png', q: '/static/black_queen.webp',
P: '/static/white_pawn.png', P: '/static/white_pawn.webp',
R: '/static/white_rook.png', R: '/static/white_rook.webp',
C: '/static/white_castle.png', C: '/static/white_castle.webp',
B: '/static/white_bishop.png', B: '/static/white_bishop.webp',
K: '/static/white_king.png', K: '/static/white_king.webp',
Q: '/static/white_queen.png', Q: '/static/white_queen.webp',
}; };
const loadPromises = Object.entries(assets).map(([key, src]) => { const loadPromises = Object.entries(assets).map(([key, src]) => {

182
home.html Normal file
View File

@@ -0,0 +1,182 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>شرطنج</title>
<style>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
font-family: Arial, Helvetica, sans-serif;
color: 262942;
}
body {
background-color: #f8e8d3;
}
header {
display: flex;
justify-content: center;
width: 100vw;
user-select: none;
height: 230px;
}
.center {
display: flex;
flex-direction: column;
align-items: center;
margin: 24px auto;
gap: 8px;
max-width: 70%;
}
.or {
width: 100%;
position: relative;
margin: 10px 0;
}
.or::before {
content: '';
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 2px;
transform: translateY(-50%);
background: rgba(92, 92, 92, 0.664);
}
.or::after {
content: 'OR';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
font-size: 1rem;
padding: 8px;
font-weight: bolder;
background: #f8e8d3;
color: rgba(92, 92, 92, 0.664);
}
.code {
padding: 16px;
border: none;
border-bottom: 2px solid rgba(92, 92, 92, 0.664);
font-size: 1.5rem;
text-align: center;
letter-spacing: 8px;
text-transform: lowercase;
width: 200px;
outline: none;
}
.button {
background: rgb(85, 122, 75);
color: rgb(255, 255, 255);
font-family: 'Times New Roman', Times, serif;
font-weight: lighter;
text-transform: uppercase;
font-size: 1.3rem;
padding: 12px 30px;
cursor: pointer;
border: 0;
outline: none;
}
.group {
display: flex;
height: 60px;
gap: 4px;
}
.group * {
height: 60px;
margin-bottom: 4px;
}
.vs {
height: calc(100% - 8px);
width: 2px;
margin: 4px;
background: rgba(92, 92, 92, 0.664);
}
.code::placeholder {
letter-spacing: normal;
}
.background {
width: 100%;
aspect-ratio: 16/9;
z-index: -1000;
bottom: 0;
position: fixed;
image-rendering: pixelated;
background-image: url("/static/background.png");
background-size: contain;
background-repeat: no-repeat;
}
</style>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
</head>
<body>
<header>
<img src="/static/logo.webp" alt="">
</header>
<div class="background"></div>
<div class="center">
<div class="group">
<button id="create" class="button">create</button>
<div class="vs"></div>
<input type="text" id="code" class="code" placeholder="invite code" pattern="[a-z0-9A-Z]{4}" maxlength="4">
<button id="join" class="button">join</button>
</div>
<div class="or"></div>
Public games not added yet.
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const code = document.getElementById("code");
const pat = new RegExp("^[a-z0-9]{4}$")
document.getElementById("join").addEventListener("click", () => {
if (!pat.test(code.value)) {
alert("Invalid ID");
} else {
window.location.href = window.location.origin + "/" + code.value;
}
});
document.getElementById("create").addEventListener("click", () => {
fetch('/create_room', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
.then((response) => response.json())
.then((data) => {
window.location.href = window.location.origin + "/" + data.id;
}).catch((error) => {
console.log(error);
})
});
});
</script>
</body>
</html>

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

1
libs Submodule

Submodule libs added at fe65fafbe0

View File

@@ -1,11 +0,0 @@
from .slow import JSONAPI, App, Headers, HTTPResponse, JSONResponse, Request, render
__all__ = [
"JSONAPI",
"App",
"HTTPResponse",
"JSONResponse",
"render",
"Request",
"Headers",
]

View File

@@ -1,260 +0,0 @@
import asyncio
import http.client
import json
import re
import urllib.parse
from pathlib import Path
from typing import Any, Awaitable, Callable, Optional
PR = re.compile(r"\<([a-zA-Z_][a-zA-Z0-9_]*)\>")
class Headers:
def __init__(self):
self._d: dict[str, str] = {}
def get(self, key: str, default: Optional[Any] = None) -> str | Any:
return self._d.get(key.lower(), default)
def set(self, key: str, value: str) -> None:
self._d[key.lower()] = value
def __str__(self):
return str(self._d)
class Request:
def __init__(self, method: str, path: str, headers: Headers, body: bytes):
self.method = method
self.path = path
self.headers = headers
self.body = body
def __str__(self):
return str(
{
"method": self.method,
"path": self.path,
"headers": self.headers,
"body": self.body,
}
)
async def _default_404_route(request: Request):
return "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\n404 Not Found contact admin".encode(
encoding="utf-8"
)
async def _default_405_route(request: Request):
return "HTTP/1.1 405 Method Not Allowed\r\nContent-Type: text/html\r\n\r\n405 Method Not Allowed".encode(
encoding="utf-8"
)
class App:
def __init__(self):
self.routes: dict[
re.Pattern[str], dict[str, Callable[[Request, ...], Awaitable[bytes]]]
] = {}
self.error_routes: dict[int, Callable[[Request, ...], Awaitable[bytes]]] = {
404: _default_404_route,
405: _default_405_route,
}
def _pattern_to_regex(self, temp) -> re.Pattern[str]:
re_temp = temp
iter = PR.finditer(temp)
for m in iter:
name = m[1]
re_temp = re.sub(
m[0], r"(?P<" + name + r">[a-zA-Z0-9\-._~/:%&=]+)", re_temp
)
return re.compile(re_temp)
def _serve(
self, path: str, method: str, func: Callable[[Request, ...], Awaitable[bytes]]
):
if method not in ["GET", "POST", "PUT", "DELETE"]:
raise RuntimeError(f'Invalid method "{method}".')
pat = self._pattern_to_regex(path)
if pat not in self.routes:
self.routes[pat] = {}
if method in self.routes[pat]:
raise RuntimeWarning(f'Path "{path}" already exists.')
self.routes[pat][method] = func
def GET(self, path: str):
"""Decorator to register a GET HTTP route."""
def decorator(func: Callable[[Request, ...], Awaitable[bytes]]):
self._serve(path, "GET", func)
return func
return decorator
def POST(self, path: str):
"""Decorator to register a POST HTTP route."""
def decorator(func: Callable[[Request, ...], Awaitable[bytes]]):
self._serve(path, "POST", func)
return func
return decorator
def PUT(self, path: str):
"""Decorator to register a PUT HTTP route."""
def decorator(func: Callable[[Request, ...], Awaitable[bytes]]):
self._serve(path, "PUT", func)
return func
def DELETE(self, path: str):
"""Decorator to register a DELETE HTTP route."""
def decorator(func: Callable[[Request, ...], Awaitable[bytes]]):
self._serve(path, "DELETE", func)
return func
return decorator
def OPTIONS(self, path: str):
"""Decorator to register a OPTIONS HTTP route."""
def decorator(func: Callable[[Request, ...], Awaitable[bytes]]):
self._serve(path, "OPTIONS", func)
return func
return decorator
def error(self, code):
"""Decorator to register an error route."""
def decorator(func):
self.error_routes[code] = func
return func
return decorator
def resolve(self, path, method) -> tuple[Callable[..., Awaitable[bytes]], dict]:
for pattern, route in self.routes.items():
if m := pattern.fullmatch(path):
if method not in route:
return self.error_routes[405], {}
return route[method], {
k: urllib.parse.unquote(v) for k, v in m.groupdict().items()
}
return self.error_routes[404], {}
async def handle_client(
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
):
"""Handle an incoming connection (HTTP or WebSocket)."""
try:
# Read the initial HTTP request line
request_line = await reader.readline()
if not request_line:
return
# Parse request line
parts = request_line.decode(encoding="utf-8").strip().split()
if len(parts) < 3:
return
method, path, protocol = parts[0], parts[1], parts[2]
assert protocol == "HTTP/1.1"
headers: Headers = Headers()
while True:
line = await reader.readline()
if line == b"\r\n" or line == b"\n" or not line: # End of headers
break
line = line.decode("utf-8").strip()
if ":" in line:
key, value = line.split(":", 1)
headers.set(key.strip(), value.strip())
content_length = int(headers.get("Content-Length", 0))
body = await reader.read(content_length) if content_length else b""
route, kwargs = self.resolve(path, method)
response = await route(
request=Request(
method=method,
path=path,
headers=headers,
body=body,
),
**kwargs,
)
writer.write(response)
await writer.drain()
except Exception as e:
print(f"Internal Server Error: {e}")
finally:
writer.close()
await writer.wait_closed()
async def run(self, host="127.0.0.1", port=8000):
"""Start the async server."""
server = await asyncio.start_server(self.handle_client, host, port)
print(f"Serving on http://{host}:{port}")
async with server:
await server.serve_forever()
AccessControlAllowOrigin = "*"
def HTTPResponse(
content: str, status=200, content_type="text/plain; charset=utf-8", headers=[]
) -> bytes:
global AccessControlAllowOrigin
head: str = f"HTTP/1.1 {status} {http.client.responses.get(status, 'Unkown Status Code')}\r\nContent-Type: {content_type}\r\n"
head += f"Access-Control-Allow-Origin: {AccessControlAllowOrigin}\r\n" # CORS
head += "Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS\r\n" # CORS
head += "\r\n".join(headers) + ("\r\n" if len(headers) > 0 else "")
head += "\r\n"
return (head + content).encode(encoding="utf-8")
_value_pattern = re.compile(r"\{\{\s*([a-zA-Z_][a-zA-Z_0-9]*)\s*\}\}")
def render(file: str | Path, variables: dict[str, Any] = {}) -> bytes:
if isinstance(file, str):
file = Path(file)
content: str = file.read_text(encoding="utf-8")
for m in _value_pattern.findall(content):
if m in variables:
content = re.sub(r"\{\{\s*" + m + r"\s*\}\}", variables[m], content)
return HTTPResponse(content, content_type="text/html; charset=utf-8")
def JSONResponse(d: dict, status=200) -> bytes:
return HTTPResponse(
json.dumps(d), status=status, content_type="text/json; charset=utf-8"
)
def JSONAPI(func):
async def wrapper(*args, **kwargs):
result = await func(*args, **kwargs)
if not isinstance(result, dict):
if (
isinstance(result, tuple)
and len(result) == 2
and isinstance(result[1], dict)
and isinstance(result[0], int)
):
return JSONResponse(result[1], result[0])
raise RuntimeError("Return value of JSONAPI route is not a dictionary")
return JSONResponse(result)
return wrapper

BIN
static/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 KiB

BIN
static/black_bishop.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

BIN
static/black_castle.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

BIN
static/black_king.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

BIN
static/black_pawn.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

BIN
static/black_queen.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

BIN
static/black_rook.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B

BIN
static/endgame.mp3 Normal file

Binary file not shown.

BIN
static/logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
static/piecemove.mp3 Normal file

Binary file not shown.

BIN
static/white_bishop.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

BIN
static/white_castle.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

BIN
static/white_king.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

BIN
static/white_pawn.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

BIN
static/white_queen.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

BIN
static/white_rook.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B