Compare commits

...

6 Commits

Author SHA1 Message Date
0880
5119a09c29 SlowAPI Update 2026-01-17 01:59:12 +03:30
0880
6089eb0b21 Many changes 2026-01-17 01:59:04 +03:30
0880
678f51dc69 BIG CHANGES 2026-01-17 01:58:56 +03:30
0880
8f5c44b0ea README update 2026-01-17 01:58:49 +03:30
0880
543ec23aed logo for README 2026-01-17 01:58:42 +03:30
0880
e6a59e18b7 Textures assets 2026-01-17 01:58:31 +03:30
19 changed files with 824 additions and 186 deletions

View File

@@ -1,3 +1,5 @@
# shartanj ![شرطنج](./logo_bg.png)
## shartanj
بازی شرطنج بازی شرطنج

442
app.py
View File

@@ -1,18 +1,42 @@
import asyncio import asyncio
import json import json
import uuid import uuid
from datetime import datetime from collections.abc import Iterator
from datetime import datetime, timedelta
from enum import Enum from enum import Enum
from itertools import combinations from itertools import product
from typing import TypedDict from pathlib import Path
from typing import Any
from slow import JSONAPI, App, JSONResponse, render from slow import JSONAPI, App, HTTPResponse, JSONResponse, Request, render
class Coord(TypedDict): class Coord:
x: int x: int
y: int y: int
def __init__(self, x: int, y: int):
self.x = x
self.y = y
def __iter__(self) -> Iterator[int]:
yield self.x
yield self.y
def copy(self):
return Coord(self.x, self.y)
def __str__(self):
return coord_to_pos_safe(self)
def __eq__(self, other):
if isinstance(other, Coord):
return self.x == other.x and self.y == other.y
return False
def __repr__(self):
return f"Coord({self.x}, {self.y})"
app = App() app = App()
@@ -22,6 +46,13 @@ class Color(Enum):
BLACK = 1 BLACK = 1
class State(Enum):
NOT_FINISHED = -1
TIE = 0
WHITE_WIN = 1
BLACK_WIN = 2
class Piece(Enum): class Piece(Enum):
NONE = "" NONE = ""
EMPTY = "E" EMPTY = "E"
@@ -43,27 +74,41 @@ class Piece(Enum):
BLACK_CASTLE = "c" BLACK_CASTLE = "c"
def xy_to_coord(xy: Coord) -> None | str: def coord_to_pos(xy: Coord) -> None | str:
if xy.x < 0 or xy.x > 7 or xy.y < 0 or xy.y > 7: if xy.x < 0 or xy.x > 7 or xy.y < 0 or xy.y > 7:
return None return None
return "abcdefgh"[xy.x] + "12345678"[xy.y] return "abcdefgh"[xy.x] + "12345678"[xy.y]
def coord_to_xy(pos: str) -> tuple[int, int]: def coord_to_pos_safe(xy: Coord) -> str:
return ("abcdefgh".inedx(pos[0]), "12345678".inedx(pos[1])) return "abcdefgh"[xy.x] + "12345678"[xy.y]
def xy_to_pos(x: int, y: int) -> None | str:
if x < 0 or x > 7 or y < 0 or y > 7:
return None
return "abcdefgh"[x] + "12345678"[y]
def xy_to_pos_safe(x: int, y: int) -> str:
return "abcdefgh"[x] + "12345678"[y]
def pos_to_coord(pos: str) -> Coord:
return Coord("abcdefgh".index(pos[0]), "12345678".index(pos[1]))
class Board: class Board:
grid: list[list[Piece]] grid: list[list[Piece]]
def __init__(self): def __init__(self):
self.grid = [[Piece.EMPTY] * 8] * 8 self.grid: list[list[Piece]] = [[Piece.EMPTY] * 8 for _ in range(8)]
self.grid[0] = [ self.grid[0] = [
Piece.WHITE_CASTLE, Piece.WHITE_CASTLE,
Piece.WHITE_ROOK, Piece.WHITE_ROOK,
Piece.WHITE_BISHOP, Piece.WHITE_BISHOP,
Piece.WHITE_KING,
Piece.WHITE_QUEEN, Piece.WHITE_QUEEN,
Piece.WHITE_KING,
Piece.WHITE_BISHOP, Piece.WHITE_BISHOP,
Piece.WHITE_ROOK, Piece.WHITE_ROOK,
Piece.WHITE_CASTLE, Piece.WHITE_CASTLE,
@@ -75,8 +120,8 @@ class Board:
Piece.BLACK_CASTLE, Piece.BLACK_CASTLE,
Piece.BLACK_ROOK, Piece.BLACK_ROOK,
Piece.BLACK_BISHOP, Piece.BLACK_BISHOP,
Piece.BLACK_KING,
Piece.BLACK_QUEEN, Piece.BLACK_QUEEN,
Piece.BLACK_KING,
Piece.BLACK_BISHOP, Piece.BLACK_BISHOP,
Piece.BLACK_ROOK, Piece.BLACK_ROOK,
Piece.BLACK_CASTLE, Piece.BLACK_CASTLE,
@@ -87,34 +132,56 @@ class Board:
num = "12345678".index(pos[1]) num = "12345678".index(pos[1])
return self.grid[num][let] return self.grid[num][let]
def index_xy(self, xy: Coord) -> Piece: def index_coord(self, coord: Coord) -> Piece:
if xy.x < 0 or xy.x > 7 or xy.y < 0 or xy.y > 7: if coord.x < 0 or coord.x > 7 or coord.y < 0 or coord.y > 7:
return Piece.NONE return Piece.NONE
return self.grid[xy.y][xy.x] return self.grid[coord.y][coord.x]
def index_xy(self, x: int, y: int) -> Piece:
if x < 0 or x > 7 or y < 0 or y > 7:
return Piece.NONE
return self.grid[y][x]
def copy(self) -> "Board":
board = Board()
board.grid = [[Piece(p.value) for p in r] for r in self.grid]
return board
def serialize(self) -> dict: def serialize(self) -> dict:
return {"grid": self.grid} return {"grid": [[p.value for p in r] for r in self.grid]}
def __str__(self):
return "\n".join([" ".join([p.value for p in self.grid[i]]) for i in range(8)])
class Room: class Room:
board: Board = Board() board: Board
turn: Color = Color.WHITE turn: Color
players: list[uuid.UUID] = [] players: list[str]
last_move: datetime last_move: datetime
state: State
def __init__(self):
self.turn = Color.WHITE
self.board = Board()
self.players = []
self.last_move = datetime.now()
self.state = State.NOT_FINISHED
def start(self): def start(self):
self.last_move = datetime.now() self.last_move = datetime.now()
def add_player(self) -> None | tuple[uuid.UUID, Color]: def add_player(self) -> None | tuple[str, Color]:
np: int = len(self.players) np: int = len(self.players)
if np >= 2: if np >= 2:
return None return None
elif np == 1: elif np == 1:
uid = uuid.uuid4() uid = str(uuid.uuid4())
self.players.append(uid) self.players.append(uid)
self.start()
return uid, Color.BLACK return uid, Color.BLACK
else: else:
uid = uuid.uuid4() uid = str(uuid.uuid4())
self.players.append(uid) self.players.append(uid)
return uid, Color.WHITE return uid, Color.WHITE
@@ -124,20 +191,24 @@ rooms: dict[str, Room] = {}
@app.GET("/") @app.GET("/")
async def home(request): async def home(request):
return render("index.html") return render("index.html", {"API_ROOT": "http://127.0.0.1:8080"})
@app.POST("/join/<id>") @app.POST("/join/<room_id>")
async def join(request, room_id): async def join(request, room_id):
room: Room = rooms.get(room_id, Room()) if room_id not in rooms:
rooms[room_id] = Room()
room: Room = rooms[room_id]
player = room.add_player() player = room.add_player()
if player: if player:
return JSONResponse( return JSONResponse(
{ {
"code": "JOIN", "code": "JOIN",
"id": player[0], "id": player[0],
"color": player[1], "color": player[1].value,
"turn": room.turn.value,
"board": room.board.serialize(), "board": room.board.serialize(),
"ready": len(room.players) == 2,
} }
) )
else: else:
@@ -146,16 +217,157 @@ async def join(request, room_id):
) )
def parse(body: str) -> dict | None: def parse(body: str | bytes) -> dict[str, Any] | None:
try: try:
return json.loads(body) return json.loads(body)
except Exception: except Exception:
return None return None
@app.POST("/move/<id>") def get_piece_moves(piece_kind, board: Board, is_white, src: str) -> list[Coord]:
valids: list[Coord] = []
if piece_kind == "p":
dir = 1 if is_white else -1
x, y = pos_to_coord(src)
if board.index_xy(x, y + dir) == Piece.EMPTY:
valids.append(Coord(x=x, y=y + dir))
if (y == 1 or y == 6) and board.index_xy(x, y + 2 * dir) == Piece.EMPTY:
valids.append(Coord(x=x, y=y + 2 * dir))
if (
board.index_xy(x + 1, y + dir)
not in [Piece.EMPTY, Piece.NONE, Piece.BLACK_KING, Piece.WHITE_KING]
and board.index_xy(x + 1, y + dir).value.upper() != is_white
):
valids.append(Coord(x=x + 1, y=y + 1))
if (
board.index_xy(x - 1, y + dir)
not in [Piece.EMPTY, Piece.NONE, Piece.BLACK_KING, Piece.WHITE_KING]
and board.index_xy(x - 1, y + dir).value.upper() != is_white
):
valids.append(Coord(x=x - 1, y=y + 1))
elif piece_kind == "b":
x, y = pos_to_coord(src)
comb = product([1, -1], repeat=2)
for dir in comb:
target: Coord = Coord(x=x + dir[0], y=y + dir[1])
while (
board.index_coord(target) == Piece.EMPTY
and board.index_coord(target) != Piece.NONE
):
valids.append(target.copy())
target.x += dir[0]
target.y += dir[1]
if (
p := board.index_coord(target)
) != Piece.NONE and p.value.isupper() != is_white:
valids.append(target.copy())
elif piece_kind == "q":
x, y = pos_to_coord(src)
comb = product([1, -1, 0], repeat=2)
for dir in comb:
if dir[0] ** 2 + dir[1] ** 2 == 0:
continue # x=0 y=0 cannot move => invalid
target: Coord = Coord(x=x + dir[0], y=y + dir[1])
while (
board.index_coord(target) == Piece.EMPTY
and board.index_coord(target) != Piece.NONE
):
valids.append(target.copy())
target.x += dir[0]
target.y += dir[1]
if (
p := board.index_coord(target)
) != Piece.NONE and p.value.isupper() != is_white:
valids.append(target.copy())
elif piece_kind == "c":
x, y = pos_to_coord(src)
comb = [(-1, 0), (1, 0), (0, -1), (0, 1)]
for dir in comb:
target: Coord = Coord(x=x + dir[0], y=y + dir[1])
while (
board.index_coord(target) == Piece.EMPTY
and board.index_coord(target) != Piece.NONE
):
valids.append(target.copy())
target.x += dir[0]
target.y += dir[1]
if (
p := board.index_coord(target)
) != Piece.NONE and p.value.isupper() != is_white:
valids.append(target.copy())
elif piece_kind == "k":
x, y = pos_to_coord(src)
comb = product([-1, 0, 1], repeat=2)
for dir in comb:
if dir[0] ** 2 + dir[1] ** 2 == 0:
continue # x=0 y=0 cannot move => invalid
target: Coord = Coord(x=x + dir[0], y=y + dir[1])
if (
p := board.index_coord(target)
) != Piece.NONE and p.value.isupper() != is_white:
valids.append(target.copy())
elif piece_kind == "r":
x, y = pos_to_coord(src)
moves = [(2, 1), (2, -1), (-2, 1), (-2, -1), (1, 2), (-1, 2), (1, -2), (-1, -2)]
for m in moves:
target: Coord = Coord(x=x + m[0], y=y + m[1])
if (
p := board.index_coord(target)
) != Piece.NONE and p.value.isupper() != is_white:
valids.append(target.copy())
return valids
def generate_valid_moves(
piece_kind: str, board: Board, is_white: bool, src: str
) -> list[Coord]:
print(" -- FIRST ORDER ---------------------------")
possible_moves: list[Coord] = get_piece_moves(piece_kind, board, is_white, src)
print(" -- FIRST ORDER ---------------------------")
valids: list[Coord] = []
king: Coord
for i in range(8):
for j in range(8):
p = board.grid[i][j]
if (
p in [Piece.BLACK_KING, Piece.WHITE_KING]
and p.value.isupper() == is_white
):
king = Coord(j, i)
break
for m in possible_moves:
fake_board = board.copy()
fake_board.grid[m.y][m.x] = fake_board.index(src)
sx, sy = pos_to_coord(src)
fake_board.grid[sy][sx] = Piece.EMPTY
king_safe = True
for i in range(8):
for j in range(8):
p = board.grid[i][j]
if p != Piece.EMPTY and p.value.isupper() != is_white:
# Enemy
enemy_moves = get_piece_moves(
p.value.lower(), board, not is_white, xy_to_pos_safe(j, i)
)
if king not in enemy_moves:
continue
new_enemy_moves = get_piece_moves(
p.value.lower(), fake_board, not is_white, xy_to_pos_safe(j, i)
)
if king in new_enemy_moves:
king_safe = False
break
if not king_safe:
break
if king_safe:
valids.append(m)
return valids
@app.POST("/move/<room_id>")
@JSONAPI @JSONAPI
async def move(request, room_id): async def move(request: Request, room_id):
req = parse(request.body) req = parse(request.body)
if not req: if not req:
return 400, {} return 400, {}
@@ -165,6 +377,8 @@ async def move(request, room_id):
return 400, {} return 400, {}
if "to" not in req or not isinstance(req["to"], str): if "to" not in req or not isinstance(req["to"], str):
return 400, {} return 400, {}
if len(req) > 3:
return 400, {}
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]
@@ -185,9 +399,9 @@ async def move(request, room_id):
or src[1] not in "12345678" or src[1] not in "12345678"
or dst[0] not in "abcdefgh" or dst[0] not in "abcdefgh"
or dst[1] not in "12345678" or dst[1] not in "12345678"
or board.index(src) != Piece.EMPTY or board.index(src) == Piece.EMPTY
): ):
return 400, {"code": "LMOV", "error": "Bad Move"} return 400, {"code": "LMOV", "error": "Bad Move 1"}
srcp = board.index(src) srcp = board.index(src)
dstp = board.index(dst) dstp = board.index(dst)
piece_kind = srcp.value.lower() piece_kind = srcp.value.lower()
@@ -199,81 +413,99 @@ async def move(request, room_id):
or (dstp.value.islower() and color == Color.BLACK) or (dstp.value.islower() and color == Color.BLACK)
): ):
return 400, {"code": "HTME", "error": "Cannot move to your own piece"} return 400, {"code": "HTME", "error": "Cannot move to your own piece"}
valids: list[Coord] = [] valid_moves = generate_valid_moves(piece_kind, board, is_white, src)
if piece_kind == "p": if (c := pos_to_coord(dst)) in valid_moves:
dir = 1 if is_white else -1 board.grid[c.y][c.x] = srcp
x, y = coord_to_xy(src) sx, sy = pos_to_coord(src)
if board.index_xy(x, y + dir) == Piece.EMPTY: board.grid[sy][sx] = Piece.EMPTY
valids.append({"x": x, "y": y + dir}) if c.y == 0 or c.y == 7 and srcp in [Piece.BLACK_PAWN, Piece.WHITE_PAWN]:
if (y == 1 or y == 6) and board.index_xy(x, y + 2 * dir) == Piece.EMPTY: board.grid[c.y][c.x] = Piece.WHITE_QUEEN if is_white else Piece.BLACK_QUEEN
valids.append({"x": x, "y": y + 2 * dir}) room.turn = Color.BLACK if color == Color.WHITE else Color.WHITE
if ( room.last_move = datetime.now()
board.index_xy(x + 1, y + dir) return {
not in [Piece.EMPTY, Piece.NONE, Piece.BLACK_KING, Piece.WHITE_KING] "code": "MOVD",
and board.index_xy(x + 1, y + dir).value.upper() != is_white "color": color.value,
): "turn": room.turn.value,
valids.append({"x": x + 1, "y": y + 1}) "board": board.serialize(),
if ( }
board.index_xy(x - 1, y + dir) else:
not in [Piece.EMPTY, Piece.NONE, Piece.BLACK_KING, Piece.WHITE_KING] return 400, {"code": "LMOV", "error": "Bad Move 2"}
and board.index_xy(x - 1, y + dir).value.upper() != is_white
):
valids.append({"x": x - 1, "y": y + 1}) @app.GET("/poll/<room_id>")
if piece_kind == "b": @JSONAPI
x, y = coord_to_xy(src) async def poll(request, room_id):
comb = combinations([1, -1], 2) for key in rooms:
for dir in comb: room = rooms[key]
scl = 1 if (datetime.now() - room.last_move) >= timedelta(hours=24):
target: Coord = {"x": x + dir[0] * scl, "y": y + dir[1] * scl} del rooms[key]
while board.index_xy(target) not in [Piece.EMPTY, Piece.NONE]: if room_id not in rooms:
valids.append(target.copy()) return 400, {"code": "NOEX", "error": "Room does not exist"}
scl += 1 room = rooms[room_id]
target: Coord = {"x": x + dir[0] * scl, "y": y + dir[1] * scl} return {
if ( "code": "STAT",
p := board.index_xy(target) "turn": room.turn.value,
) != Piece.NONE and p.value.isupper() != is_white: "board": room.board.serialize(),
valids.append(target.copy()) "ready": len(room.players) == 2,
if piece_kind == "q": "state": room.state.value,
x, y = coord_to_xy(src) }
comb = combinations([1, -1, 0], 2)
for dir in comb:
if dir[0] ** 2 + dir[1] ** 2 == 0: @app.POST("/moves/<room_id>")
continue # x=0 y=0 cannot move => invalid @JSONAPI
scl = 1 async def generate_moves(request: Request, room_id):
target: Coord = {"x": x + dir[0] * scl, "y": y + dir[1] * scl} req = parse(request.body)
while board.index_xy(target) not in [Piece.EMPTY, Piece.NONE]: if not req:
valids.append(target.copy()) return 400, {}
scl += 1 if "from" not in req or not isinstance(req["from"], str):
target: Coord = {"x": x + dir[0] * scl, "y": y + dir[1] * scl} return 400, {}
if ( if len(req) > 1:
p := board.index_xy(target) return 400, {}
) != Piece.NONE and p.value.isupper() != is_white: if room_id not in rooms:
valids.append(target.copy()) return 400, {"code": "NOEX", "error": "Room does not exist"}
if piece_kind == "c": room = rooms[room_id]
x, y = coord_to_xy(src) if len(room.players) != 2:
comb = [(-1, 0), (1, 0), (0, -1), (0, 1)] return 400, {"code": "PRES", "error": "Game has not been started"}
for dir in comb: board = room.board
scl = 1 src = req["from"].lower()
target: Coord = {"x": x + dir[0] * scl, "y": y + dir[1] * scl} srcp = board.index(src)
while board.index_xy(target) not in [Piece.EMPTY, Piece.NONE]: color = Color.WHITE if srcp.value.isupper() else Color.BLACK
valids.append(target.copy()) if (
scl += 1 len(src) != 2
target: Coord = {"x": x + dir[0] * scl, "y": y + dir[1] * scl} or src[0] not in "abcdefgh"
if ( or src[1] not in "12345678"
p := board.index_xy(target) or srcp == Piece.EMPTY
) != Piece.NONE and p.value.isupper() != is_white: ):
valids.append(target.copy()) return 400, {"code": "LMOV", "error": "Bad Move"}
if piece_kind == "k": piece_kind = srcp.value.lower()
x, y = coord_to_xy(src) is_white = srcp.value.isupper()
comb = combinations([-1, 0, 1], 2) if (is_white and color == Color.BLACK) or (not is_white and color == Color.WHITE):
for dir in comb: return 400, {"code": "NOTU", "error": "Not your piece"}
if dir[0] ** 2 + dir[1] ** 2 == 0: valid_moves = generate_valid_moves(piece_kind, board, is_white, src)
continue # x=0 y=0 cannot move => invalid return {
target: Coord = {"x": x + dir[0], "y": y + dir[1]} "moves": ",".join([coord_to_pos_safe(c) for c in valid_moves]),
if ( }
p := board.index_xy(target)
) != Piece.NONE and p.value.isupper() != is_white:
valids.append(target.copy()) def sanitize_filename(filename: str, base: Path = Path.cwd().resolve()) -> Path:
p = (base / filename).resolve()
if not p.is_relative_to(base):
raise ValueError("Filename leaves current directory")
return p
@app.GET("/static/<fn>")
async def static(request, fn):
try:
path = sanitize_filename(fn, Path("static/").resolve())
return (
HTTPResponse("", content_type="application/octet-stream")
+ path.read_bytes()
)
except Exception:
return HTTPResponse("404 File Not Found", status=404)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,9 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title> <title>شرطنج</title>
<style> <style>
* { * {
padding: 0; padding: 0;
@@ -12,68 +13,379 @@
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
color: 262942; color: 262942;
} }
body { body {
background-color: #f8e8d3; background-color: #f8e8d3;
} }
.title {
font-family: 'Times New Roman', Times, serif;
text-transform: lowercase;
letter-spacing: 0.25rem;
font-size: 4rem;
font-weight: 100;
color: #48517b;
padding: 0 0.5rem 0.5rem 0.5rem;
display: block;
width: min-content;
border-bottom: 2px solid #48517b;
}
header { header {
display: flex; display: flex;
padding: 1rem;
justify-content: center; justify-content: center;
width: 100vw; width: 100vw;
user-select: none; user-select: none;
height: 230px;
} }
#board {
width: 38rem;
height: 38rem;
display: grid;
grid-template-columns: repeat(8, 1fr);
cursor: not-allowed;
}
.center { .center {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin: 24px 0;
}
.join-menu {
display: flex;
width: min-content;
border-radius: 8px;
overflow: hidden;
border: 1px solid #48517b56;
}
.join-menu * {
border-radius: 0;
}
input,
button {
outline: none;
border: 0;
border-radius: 8px;
}
input {
padding: 8px;
}
button {
padding: 8px 24px;
cursor: pointer;
}
.piece {
width: 100%;
height: 100%;
z-index: 20;
position: relative;
pointer-events: none;
}
.game {
width: 600px;
height: 600px;
} }
</style> </style>
</head> </head>
<body> <body>
<header> <header>
<h1 class="title">shatranj</h1> <img src="/static/logo.png" alt="">
</header> </header>
<div class="center"> <div class="center">
<div id="board"> <div id="join-menu" class="join-menu"><input type="text" id="room-id" placeholder="Room ID"><button
id="join-submit">Join</button></div>
</div> </div>
<div class="center">
<canvas class="game" id="canvas"></canvas>
</div> </div>
<script> <script>
const container = document.getElementById("board"); document.addEventListener('DOMContentLoaded', () => {
const canvas = document.getElementById('canvas');
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
const ctx = canvas.getContext('2d');
const cols = ["A", "B", "C", "D", "E", "F", "G", "H"]; const width = canvas.width;
const height = canvas.height;
for (let i = 0; i < 64; i++) { const primary = '#f0d9b5';
let col = i % 8; const accent = '#b58863';
let row = Math.floor(i/8);
const div = document.createElement("div"); ctx.font = 'bolder 16px Arial';
div.className = "house";
div.id = cols[col] + (row+1).toString() let mouse = [0, 0];
if (i % 2 == row % 2) { let mHouse = [0, 0];
div.style.backgroundColor = "#262942" const textures = {};
} else {
div.style.backgroundColor = "#faf5f0" let color = 0;
let turn = 0;
let ready = false;
let letterMap = ' ';
let numberMap = ' ';
let board = undefined;
let UID = undefined;
let ROOM_ID = undefined;
let selected = undefined;
let moves = [];
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * width;
const y = ((e.clientY - rect.top) / rect.height) * height;
mouse = [x, y];
i = Math.floor((x / width) * 8);
j = Math.floor((y / height) * 8);
if (ready) {
canvas.style.cursor = 'default';
if (board) {
if (color == 0) {
j = 7 - j;
} else {
i = 7 - i;
}
mHouse = [i, j];
if (board[j][i] !== "E" && (board[j][i] === board[j][i].toUpperCase() ? 0 : 1) === color && turn == color) {
canvas.style.cursor = 'pointer';
}
moves.forEach(move => {
if (move[0] == i && move[1] == j) {
canvas.style.cursor = 'pointer';
}
})
}
} else {
canvas.style.cursor = 'not-allowed';
}
});
canvas.addEventListener('mouseup', (e) => {
i = mHouse[0];
j = mHouse[1];
if (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) {
selected = [i, j];
moves = [];
fetch('{{ API_ROOT }}/moves/' + ROOM_ID, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ from: "ABCDEFGH"[i] + "12345678"[j] })
})
.then((response) => response.json())
.then((data) => {
moves_str = data.moves.split(",");
console.log(data.moves);
moves_str.forEach(move => {
moves.push(["abcdefgh".indexOf(move[0].toLowerCase()), "12345678".indexOf(move[1])])
});
})
.catch((error) => console.error('Error:', error));
} else if (turn == color && selected) {
moves.forEach(move => {
console.log(move);
if (move[0] == i && move[1] == j) {
console.log("YAY");
fetch('{{ API_ROOT }}/move/' + ROOM_ID, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ uid: UID, from: "ABCDEFGH"[selected[0]] + "12345678"[selected[1]], to: "ABCDEFGH"[i] + "12345678"[j] })
})
.then((response) => response.json())
.then((data) => {
board = data.board.grid;
console.log(data);
selected = undefined;
moves = [];
})
.catch((error) => console.error('Error:', error));
}
});
} else {
moves = [];
selected = undefined;
}
}
});
function update() {
if (ROOM_ID) {
fetch('{{ API_ROOT }}/poll/' + ROOM_ID, {
method: 'GET',
})
.then((response) => response.json())
.then((data) => {
ready = data.ready;
turn = data.turn;
board = data.board.grid;
})
.catch((error) => console.error('Error:', error));
}
} }
container.appendChild(div);
} setInterval(update, 1500);
function extractIdFromPath() {
const currentPath = window.location.pathname;
const idRegex = /^\/([^/]+)$/;
const match = currentPath.match(idRegex);
if (match) {
const extractedId = match[1];
return extractedId;
} else {
return null;
}
}
function join(rid) {
if (rid.length > 0 && /^[a-zA-Z0-9_-]+$/.test(rid)) {
fetch('{{ API_ROOT }}/join/' + rid, {
method: 'POST',
})
.then((response) => response.json())
.then((data) => {
if (data.code === "FULL") {
alert("Game is full you can only spectate.");
} else {
UID = data.id;
color = data.color;
ready = data.ready;
turn = data.turn;
}
ROOM_ID = rid;
board = data.board.grid;
numberMap = "12345678";
letterMap = "abcdefgh";
document.getElementById('join-menu').outerHTML = '';
const urlID = extractIdFromPath();
if (urlID == null) {
history.pushState({ pageId: ROOM_ID }, '', `/${ROOM_ID}`);
}
})
.catch((error) => console.error('Error:', error));
} else {
alert("Invalid Room ID");
}
}
const urlID = extractIdFromPath();
if (urlID != null) {
join(urlID);
}
document.getElementById('join-submit').onclick = () => {
let rid = document.getElementById('room-id').value.trim();
join(rid);
};
function drawCenter(texture, x, y, width, height, padding) {
ctx.drawImage(
texture, x + padding, y + padding, width - padding * 2,
height - padding * 2);
}
function draw() {
ctx.clearRect(0, 0, width, height);
const w = width / 8;
const h = height / 8;
for (let i = 0; i < 8; i++) {
for (let j = 0; j < 8; j++) {
let I = i;
let J = j;
if (color == 0) {
J = 7 - J;
} else {
I = 7 - I;
}
ctx.beginPath();
if ((i * 8 + j) % 2 == i % 2) {
ctx.fillStyle = primary;
} else {
ctx.fillStyle = accent;
}
ctx.rect(i * w, j * h, w, h);
ctx.fill();
if (selected && selected[0] == I && selected[1] == J) {
ctx.fillStyle = "#00f0f0aa"
ctx.rect(i * w, j * h, w, h);
ctx.fill();
}
moves.forEach(move => {
if (move[0] == I && move[1] == J) {
if (board[J][I] !== 'E') {
ctx.fillStyle = "#fa2026aa"
} else {
ctx.fillStyle = "#f0f000aa"
}
ctx.rect(i * w, j * h, w, h);
ctx.fill();
}
});
if ((i * 8 + j) % 2 == i % 2) {
ctx.fillStyle = accent;
} else {
ctx.fillStyle = primary;
}
if (i == 7) {
ctx.fillText(numberMap[J], i * w + w - 16, j * h + 16);
}
if (j == 7) {
ctx.fillText(letterMap[I], i * w + 6, j * h + h - 6);
}
if (board) {
if (board[J][I] !== 'E') {
drawCenter(textures[board[J][I]], i * w, j * h, w, h, 6);
}
}
}
}
// ctx.beginPath();
// ctx.fillStyle = 'red';
// ctx.arc(mouse[0], mouse[1], 6, 0, Math.PI * 2);
// ctx.fill();
}
function loop() {
draw();
requestAnimationFrame(loop);
}
const assets = {
p: '/static/black_pawn.png',
r: '/static/black_rook.png',
c: '/static/black_castle.png',
b: '/static/black_bishop.png',
k: '/static/black_king.png',
q: '/static/black_queen.png',
P: '/static/white_pawn.png',
R: '/static/white_rook.png',
C: '/static/white_castle.png',
B: '/static/white_bishop.png',
K: '/static/white_king.png',
Q: '/static/white_queen.png',
};
const loadPromises = Object.entries(assets).map(([key, src]) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = src;
img.onload = () => resolve({ key, img });
img.onerror = () => reject(new Error(`Failed to load ${src}`));
});
});
Promise.all(loadPromises)
.then((loadedAssets) => {
loadedAssets.forEach(({ key, img }) => {
textures[key] = img;
});
loop();
})
.catch((err) => {
console.error('Failed to load assets:', err);
});
});
</script> </script>
</body> </body>
</html> </html>

BIN
logo_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

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

View File

@@ -1,23 +1,64 @@
import asyncio import asyncio
import http.client
import json import json
import re import re
import urllib.parse
from pathlib import Path from pathlib import Path
from typing import Any, Awaitable, Callable, Optional
PR = re.compile(r"\<([a-zA-Z_]+)\>") PR = re.compile(r"\<([a-zA-Z_][a-zA-Z0-9_]*)\>")
async def _default_404_route(request): class Headers:
return "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\n404 Not Found contact admin" 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)
async def _default_405_route(request): class Request:
return "HTTP/1.1 405 Method Not Allowed\r\nContent-Type: text/html\r\n\r\n405 Method Not Allowed" 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: class App:
def __init__(self): def __init__(self):
self.routes: dict[re.Pattern[str], dict[str, callable]] = {} self.routes: dict[
self.error_routes: dict[int, callable] = { re.Pattern[str], dict[str, Callable[[Request, ...], Awaitable[bytes]]]
] = {}
self.error_routes: dict[int, Callable[[Request, ...], Awaitable[bytes]]] = {
404: _default_404_route, 404: _default_404_route,
405: _default_405_route, 405: _default_405_route,
} }
@@ -27,10 +68,14 @@ class App:
iter = PR.finditer(temp) iter = PR.finditer(temp)
for m in iter: for m in iter:
name = m[1] name = m[1]
re_temp = re.sub(m[0], r"(?P<" + name + r">[a-zA-Z0-9_-]+)", re_temp) re_temp = re.sub(
m[0], r"(?P<" + name + r">[a-zA-Z0-9\-._~/:%&=]+)", re_temp
)
return re.compile(re_temp) return re.compile(re_temp)
def _serve(self, path, method, func): def _serve(
self, path: str, method: str, func: Callable[[Request, ...], Awaitable[bytes]]
):
if method not in ["GET", "POST", "PUT", "DELETE"]: if method not in ["GET", "POST", "PUT", "DELETE"]:
raise RuntimeError(f'Invalid method "{method}".') raise RuntimeError(f'Invalid method "{method}".')
pat = self._pattern_to_regex(path) pat = self._pattern_to_regex(path)
@@ -40,40 +85,49 @@ class App:
raise RuntimeWarning(f'Path "{path}" already exists.') raise RuntimeWarning(f'Path "{path}" already exists.')
self.routes[pat][method] = func self.routes[pat][method] = func
def GET(self, path): def GET(self, path: str):
"""Decorator to register a GET HTTP route.""" """Decorator to register a GET HTTP route."""
def decorator(func): def decorator(func: Callable[[Request, ...], Awaitable[bytes]]):
self._serve(path, "GET", func) self._serve(path, "GET", func)
return func return func
return decorator return decorator
def POST(self, path): def POST(self, path: str):
"""Decorator to register a POST HTTP route.""" """Decorator to register a POST HTTP route."""
def decorator(func): def decorator(func: Callable[[Request, ...], Awaitable[bytes]]):
self._serve(path, "POST", func) self._serve(path, "POST", func)
return func return func
return decorator return decorator
def PUT(self, path): def PUT(self, path: str):
"""Decorator to register a PUT HTTP route.""" """Decorator to register a PUT HTTP route."""
def decorator(func): def decorator(func: Callable[[Request, ...], Awaitable[bytes]]):
self._serve(path, "PUT", func) self._serve(path, "PUT", func)
return func return func
def DELETE(self, path): def DELETE(self, path: str):
"""Decorator to register a DELETE HTTP route.""" """Decorator to register a DELETE HTTP route."""
def decorator(func): def decorator(func: Callable[[Request, ...], Awaitable[bytes]]):
self._serve(path, "DELETE", func) self._serve(path, "DELETE", func)
return func return func
return decorator 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): def error(self, code):
"""Decorator to register an error route.""" """Decorator to register an error route."""
@@ -83,12 +137,14 @@ class App:
return decorator return decorator
def resolve(self, path, method) -> tuple[callable, dict]: def resolve(self, path, method) -> tuple[Callable[..., Awaitable[bytes]], dict]:
for pattern, route in self.routes.items(): for pattern, route in self.routes.items():
if m := pattern.fullmatch(path): if m := pattern.fullmatch(path):
if method not in route: if method not in route:
return self.error_routes[405], {} return self.error_routes[405], {}
return route[method], m.groupdict() return route[method], {
k: urllib.parse.unquote(v) for k, v in m.groupdict().items()
}
return self.error_routes[404], {} return self.error_routes[404], {}
async def handle_client( async def handle_client(
@@ -110,60 +166,86 @@ class App:
assert protocol == "HTTP/1.1" assert protocol == "HTTP/1.1"
headers = {} 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)) content_length = int(headers.get("Content-Length", 0))
body = await reader.read(content_length) if content_length else b"" body = await reader.read(content_length) if content_length else b""
route, kwargs = self.resolve(path) route, kwargs = self.resolve(path, method)
response = await route( response = await route(
request={ request=Request(
"method": method, method=method,
"path": path, path=path,
"headers": headers, headers=headers,
"body": body, body=body,
}, ),
**kwargs, **kwargs,
) )
writer.write(response.encode(encoding="utf-8")) writer.write(response)
await writer.drain() await writer.drain()
except Exception as e: except Exception as e:
print(f"Error: {e}") print(f"Internal Server Error: {e}")
finally: finally:
writer.close() writer.close()
await writer.wait_closed() await writer.wait_closed()
async def run(self, host="127.0.0.1", port=80): async def run(self, host="127.0.0.1", port=8000):
"""Start the async server.""" """Start the async server."""
server = await asyncio.start_server(self.handle_client, host, port) server = await asyncio.start_server(self.handle_client, host, port)
print(f"Serving on {host}:{port}") print(f"Serving on http://{host}:{port}")
async with server: async with server:
await server.serve_forever() await server.serve_forever()
def HTTPResponse(content: str, status=200, content_type="text/plain; charset=utf-8"): AccessControlAllowOrigin = "*"
return f"HTTP/1.1 {status} OK\r\nContent-Type: {content_type}\r\n\r\n{content}"
def render(file: str | Path): def HTTPResponse(
if type(file) is str: 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) file = Path(file)
content: str = file.read_text() 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") return HTTPResponse(content, content_type="text/html; charset=utf-8")
def JSONResponse(d: dict, status=200): def JSONResponse(d: dict, status=200) -> bytes:
return HTTPResponse( return HTTPResponse(
json.dumps(d), status=status, content_type="text/json; charset=utf-8" json.dumps(d), status=status, content_type="text/json; charset=utf-8"
) )
def JSONAPI(func): def JSONAPI(func):
def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
result = func(*args, **kwargs) result = await func(*args, **kwargs)
if not isinstance(result, dict): if not isinstance(result, dict):
if ( if (
isinstance(result, tuple) isinstance(result, tuple)
@@ -174,3 +256,5 @@ def JSONAPI(func):
return JSONResponse(result[1], result[0]) return JSONResponse(result[1], result[0])
raise RuntimeError("Return value of JSONAPI route is not a dictionary") raise RuntimeError("Return value of JSONAPI route is not a dictionary")
return JSONResponse(result) return JSONResponse(result)
return wrapper

BIN
static/black_bishop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 B

BIN
static/black_castle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

BIN
static/black_king.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
static/black_pawn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

BIN
static/black_queen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

BIN
static/black_rook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

BIN
static/white_bishop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
static/white_castle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

BIN
static/white_king.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
static/white_pawn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/white_queen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
static/white_rook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB