Compare commits
49 Commits
b3547ecdd9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
694ccc14a4 | ||
|
|
6ee7a435e0 | ||
|
|
1d5b449d6d | ||
|
|
e0af5c2d33 | ||
|
|
78ed9e1daf | ||
|
|
1a81ae7f43 | ||
|
|
b75e03cd3e | ||
|
|
c367ebad1f | ||
|
|
17d66398b4 | ||
|
|
4240312603 | ||
|
|
5c268911b4 | ||
|
|
2599f50484 | ||
|
|
e220431041 | ||
|
|
baac9b3e74 | ||
|
|
4daf04d340 | ||
|
|
f19638a4e6 | ||
|
|
fe7884233c | ||
|
|
83a837f339 | ||
|
|
250ef92502 | ||
|
|
83b570214a | ||
|
|
460255db9e | ||
|
|
56a01ac5a7 | ||
|
|
4f6a45cbc7 | ||
|
|
4a998bca84 | ||
|
|
cc9cf0f1c2 | ||
|
|
3d7508fc29 | ||
|
|
d2ed2b2842 | ||
|
|
bfe6c47d8e | ||
|
|
539c739c76 | ||
|
|
dd91a8723b | ||
|
|
975e813947 | ||
|
|
467b7d4706 | ||
|
|
14075ee2a3 | ||
|
|
adcda176cc | ||
|
|
21e298ea9f | ||
|
|
fe84972374 | ||
|
|
ebfe70f0bb | ||
|
|
6e61b4d3d0 | ||
|
|
1d7defd069 | ||
|
|
8e14646806 | ||
|
|
af254864bd | ||
|
|
5beeed465e | ||
|
|
f9e2f846f6 | ||
|
|
5119a09c29 | ||
|
|
6089eb0b21 | ||
|
|
678f51dc69 | ||
|
|
8f5c44b0ea | ||
|
|
543ec23aed | ||
|
|
e6a59e18b7 |
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "libs"]
|
||||||
|
path = libs
|
||||||
|
url = http://chai.bokhary.ir/0880/SlowAPI.git
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
# shartanj
|

|
||||||
|
|
||||||
|
## shartanj
|
||||||
|
|
||||||
بازی شرطنج
|
بازی شرطنج
|
||||||
|
|||||||
600
app.py
@@ -1,18 +1,53 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import string
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from collections import deque
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
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 libs.slow import (
|
||||||
|
JSONAPI,
|
||||||
|
App,
|
||||||
|
Request,
|
||||||
|
redirect,
|
||||||
|
render,
|
||||||
|
)
|
||||||
|
from libs.slow.responses import HTTPResponse, JSONResponse
|
||||||
|
|
||||||
|
|
||||||
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 str(coord_to_pos(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 +57,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 +85,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 +131,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 +143,59 @@ 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
|
||||||
|
game_start: datetime | None
|
||||||
|
state: State
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.turn = Color.WHITE
|
||||||
|
self.board = Board()
|
||||||
|
self.players = []
|
||||||
|
self.last_move = datetime.now(timezone.utc)
|
||||||
|
self.game_start = None
|
||||||
|
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[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
|
||||||
|
|
||||||
@@ -122,40 +203,306 @@ 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")
|
return render("home.html")
|
||||||
|
|
||||||
|
|
||||||
@app.POST("/join/<id>")
|
@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>")
|
||||||
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:
|
||||||
|
return JSONResponse({"code": "NOGO", "error": "Room not found."}, 404)
|
||||||
|
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],
|
||||||
"color": player[1],
|
"color": player[1].value,
|
||||||
|
"turn": room.turn.value,
|
||||||
"board": room.board.serialize(),
|
"board": room.board.serialize(),
|
||||||
}
|
"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(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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.isupper() != is_white
|
||||||
|
):
|
||||||
|
valids.append(Coord(x=x + 1, y=y + 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.isupper() != is_white
|
||||||
|
):
|
||||||
|
valids.append(Coord(x=x - 1, y=y + dir))
|
||||||
|
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 or p.value == "E"
|
||||||
|
):
|
||||||
|
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 or p.value == "E"
|
||||||
|
):
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def generate_valid_moves(
|
||||||
|
piece_kind: str, board: Board, is_white: bool, src: str
|
||||||
|
) -> list[Coord]:
|
||||||
|
possible_moves: list[Coord] = get_piece_moves(piece_kind, board, is_white, src)
|
||||||
|
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[j][i]
|
||||||
|
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
|
||||||
|
ni = i
|
||||||
|
nj = j
|
||||||
|
if j == sx and i == sy:
|
||||||
|
ni = m.y
|
||||||
|
nj = m.x
|
||||||
|
new_enemy_moves = get_piece_moves(
|
||||||
|
p.value.lower(),
|
||||||
|
fake_board,
|
||||||
|
not is_white,
|
||||||
|
xy_to_pos_safe(nj, ni),
|
||||||
|
)
|
||||||
|
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,11 +512,15 @@ 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]
|
||||||
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()
|
||||||
@@ -185,9 +536,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,82 +550,121 @@ 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 piece_kind == "p":
|
||||||
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
|
||||||
|
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 {
|
||||||
|
"code": "MOVD",
|
||||||
|
"color": color.value,
|
||||||
|
"turn": room.turn.value,
|
||||||
|
"state": room.state.value,
|
||||||
|
"board": board.serialize(),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return 400, {"code": "LMOV", "error": "Bad Move 2"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.GET("/poll/<room_id>")
|
||||||
|
@JSONAPI
|
||||||
|
async def poll(request, room_id):
|
||||||
|
for key in rooms:
|
||||||
|
room = rooms[key]
|
||||||
|
if (datetime.now(timezone.utc) - room.last_move) >= timedelta(hours=24):
|
||||||
|
del rooms[key]
|
||||||
if (
|
if (
|
||||||
board.index_xy(x + 1, y + dir)
|
room.game_start
|
||||||
not in [Piece.EMPTY, Piece.NONE, Piece.BLACK_KING, Piece.WHITE_KING]
|
and (datetime.now(timezone.utc) - room.game_start) >= timedelta(minutes=30)
|
||||||
and board.index_xy(x + 1, y + dir).value.upper() != is_white
|
and room.state == State.NOT_FINISHED
|
||||||
):
|
):
|
||||||
valids.append({"x": x + 1, "y": y + 1})
|
room.state = State.TIE
|
||||||
|
if room_id not in rooms:
|
||||||
|
return 400, {"code": "NOEX", "error": "Room does not exist"}
|
||||||
|
room = rooms[room_id]
|
||||||
|
return {
|
||||||
|
"code": "STAT",
|
||||||
|
"turn": room.turn.value,
|
||||||
|
"board": room.board.serialize(),
|
||||||
|
"ready": len(room.players) == 2,
|
||||||
|
"state": room.state.value,
|
||||||
|
"start_time": (room.game_start or datetime.now(timezone.utc)).isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.POST("/moves/<room_id>")
|
||||||
|
@JSONAPI
|
||||||
|
async def generate_moves(request: Request, room_id):
|
||||||
|
req = parse(request.body)
|
||||||
|
if not req:
|
||||||
|
return 400, {}
|
||||||
|
if "from" not in req or not isinstance(req["from"], str):
|
||||||
|
return 400, {}
|
||||||
|
if len(req) > 1:
|
||||||
|
return 400, {}
|
||||||
|
if room_id not in rooms:
|
||||||
|
return 400, {"code": "NOEX", "error": "Room does not exist"}
|
||||||
|
room = rooms[room_id]
|
||||||
|
if len(room.players) != 2:
|
||||||
|
return 400, {"code": "PRES", "error": "Game has not been started"}
|
||||||
|
board = room.board
|
||||||
|
src = req["from"].lower()
|
||||||
|
srcp = board.index(src)
|
||||||
|
color = Color.WHITE if srcp.value.isupper() else Color.BLACK
|
||||||
if (
|
if (
|
||||||
board.index_xy(x - 1, y + dir)
|
len(src) != 2
|
||||||
not in [Piece.EMPTY, Piece.NONE, Piece.BLACK_KING, Piece.WHITE_KING]
|
or src[0] not in "abcdefgh"
|
||||||
and board.index_xy(x - 1, y + dir).value.upper() != is_white
|
or src[1] not in "12345678"
|
||||||
|
or srcp == Piece.EMPTY
|
||||||
):
|
):
|
||||||
valids.append({"x": x - 1, "y": y + 1})
|
return 400, {"code": "LMOV", "error": "Bad Move"}
|
||||||
if piece_kind == "b":
|
piece_kind = srcp.value.lower()
|
||||||
x, y = coord_to_xy(src)
|
is_white = srcp.value.isupper()
|
||||||
comb = combinations([1, -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"}
|
||||||
scl = 1
|
valid_moves = generate_valid_moves(piece_kind, board, is_white, src)
|
||||||
target: Coord = {"x": x + dir[0] * scl, "y": y + dir[1] * scl}
|
return {
|
||||||
while board.index_xy(target) not in [Piece.EMPTY, Piece.NONE]:
|
"moves": ",".join([coord_to_pos_safe(c) for c in valid_moves]),
|
||||||
valids.append(target.copy())
|
}
|
||||||
scl += 1
|
|
||||||
target: Coord = {"x": x + dir[0] * scl, "y": y + dir[1] * scl}
|
|
||||||
if (
|
def sanitize_filename(filename: str, base: Path = Path.cwd().resolve()) -> Path:
|
||||||
p := board.index_xy(target)
|
p = (base / filename).resolve()
|
||||||
) != Piece.NONE and p.value.isupper() != is_white:
|
|
||||||
valids.append(target.copy())
|
if not p.is_relative_to(base):
|
||||||
if piece_kind == "q":
|
raise ValueError("Filename leaves current directory")
|
||||||
x, y = coord_to_xy(src)
|
|
||||||
comb = combinations([1, -1, 0], 2)
|
return p
|
||||||
for dir in comb:
|
|
||||||
if dir[0] ** 2 + dir[1] ** 2 == 0:
|
|
||||||
continue # x=0 y=0 cannot move => invalid
|
@app.GET("/static/<fn>")
|
||||||
scl = 1
|
async def static(request, fn):
|
||||||
target: Coord = {"x": x + dir[0] * scl, "y": y + dir[1] * scl}
|
try:
|
||||||
while board.index_xy(target) not in [Piece.EMPTY, Piece.NONE]:
|
path = sanitize_filename(fn, Path("static/").resolve())
|
||||||
valids.append(target.copy())
|
|
||||||
scl += 1
|
return HTTPResponse(path.read_bytes(), content_type="application/octet-stream")
|
||||||
target: Coord = {"x": x + dir[0] * scl, "y": y + dir[1] * scl}
|
except Exception:
|
||||||
if (
|
return HTTPResponse("404 File Not Found", status=404)
|
||||||
p := board.index_xy(target)
|
|
||||||
) != Piece.NONE and p.value.isupper() != is_white:
|
|
||||||
valids.append(target.copy())
|
|
||||||
if piece_kind == "c":
|
|
||||||
x, y = coord_to_xy(src)
|
|
||||||
comb = [(-1, 0), (1, 0), (0, -1), (0, 1)]
|
|
||||||
for dir in comb:
|
|
||||||
scl = 1
|
|
||||||
target: Coord = {"x": x + dir[0] * scl, "y": y + dir[1] * scl}
|
|
||||||
while board.index_xy(target) not in [Piece.EMPTY, Piece.NONE]:
|
|
||||||
valids.append(target.copy())
|
|
||||||
scl += 1
|
|
||||||
target: Coord = {"x": x + dir[0] * scl, "y": y + dir[1] * scl}
|
|
||||||
if (
|
|
||||||
p := board.index_xy(target)
|
|
||||||
) != Piece.NONE and p.value.isupper() != is_white:
|
|
||||||
valids.append(target.copy())
|
|
||||||
if piece_kind == "k":
|
|
||||||
x, y = coord_to_xy(src)
|
|
||||||
comb = combinations([-1, 0, 1], 2)
|
|
||||||
for dir in comb:
|
|
||||||
if dir[0] ** 2 + dir[1] ** 2 == 0:
|
|
||||||
continue # x=0 y=0 cannot move => invalid
|
|
||||||
target: Coord = {"x": x + dir[0], "y": y + dir[1]}
|
|
||||||
if (
|
|
||||||
p := board.index_xy(target)
|
|
||||||
) != Piece.NONE and p.value.isupper() != is_white:
|
|
||||||
valids.append(target.copy())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(app.run(port=8080))
|
asyncio.run(app.run(port=8989))
|
||||||
|
|||||||
BIN
favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
favicon_update.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
512
game.html
Normal file
@@ -0,0 +1,512 @@
|
|||||||
|
<!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 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 {
|
||||||
|
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;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.game {
|
||||||
|
width: 80%;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<img src="/static/logo.webp" alt="">
|
||||||
|
</header>
|
||||||
|
<div class="center">
|
||||||
|
<div id="join-menu" class="join-menu" style="display: none;"><input type="text" id="room-id"
|
||||||
|
placeholder="Room ID"><button id="join-submit">Join</button></div>
|
||||||
|
<h2 id="stats"></h2>
|
||||||
|
<h2 id="clock"></h2>
|
||||||
|
<h4 id="turn"></h4>
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
<canvas class="game" id="canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const canvas = document.getElementById('canvas');
|
||||||
|
canvas.width = canvas.clientWidth;
|
||||||
|
canvas.height = canvas.clientHeight;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
const width = canvas.width;
|
||||||
|
const height = canvas.height;
|
||||||
|
|
||||||
|
const primary = '#f0d9b5';
|
||||||
|
const accent = '#b58863';
|
||||||
|
|
||||||
|
ctx.font = 'bolder 16px Arial';
|
||||||
|
|
||||||
|
let mouse = [0, 0];
|
||||||
|
let mHouse = [0, 0];
|
||||||
|
const textures = {};
|
||||||
|
|
||||||
|
const piecemoveSound = new Audio("/static/piecemove.mp3");
|
||||||
|
//const endgameSound = new Audio("/static/endgame.mp3");
|
||||||
|
|
||||||
|
let color = 0;
|
||||||
|
let turn = 0;
|
||||||
|
let state = -1;
|
||||||
|
let start_time = undefined;
|
||||||
|
let ready = false;
|
||||||
|
let letterMap = ' ';
|
||||||
|
let numberMap = ' ';
|
||||||
|
let oldBoard = undefined;
|
||||||
|
let board = undefined;
|
||||||
|
let UID = undefined;
|
||||||
|
let ROOM_ID = undefined;
|
||||||
|
|
||||||
|
let selected = undefined;
|
||||||
|
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) => {
|
||||||
|
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 && state == -1 && UID !== undefined) {
|
||||||
|
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 (state == -1 && ready && board) {
|
||||||
|
if (board[j][i] !== "E" && (board[j][i] === board[j][i].toUpperCase() ? 0 : 1) === color && turn == color) {
|
||||||
|
|
||||||
|
selected = [i, j];
|
||||||
|
moves = [];
|
||||||
|
fetch('/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) {
|
||||||
|
fetch('/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;
|
||||||
|
boardOnChange();
|
||||||
|
console.log(data);
|
||||||
|
selected = undefined;
|
||||||
|
moves = [];
|
||||||
|
})
|
||||||
|
.catch((error) => console.error('Error:', error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
moves = [];
|
||||||
|
selected = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
if (ROOM_ID) {
|
||||||
|
fetch('/poll/' + ROOM_ID, {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
ready = data.ready;
|
||||||
|
turn = data.turn;
|
||||||
|
board = data.board.grid;
|
||||||
|
boardOnChange();
|
||||||
|
state = data.state;
|
||||||
|
start_time = new Date(data.start_time);
|
||||||
|
|
||||||
|
setUI();
|
||||||
|
})
|
||||||
|
.catch((error) => console.error('Error:', error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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('/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;
|
||||||
|
state = data.state;
|
||||||
|
start_time = new Date(data.start_time);
|
||||||
|
}
|
||||||
|
ROOM_ID = rid;
|
||||||
|
board = data.board.grid;
|
||||||
|
numberMap = "12345678";
|
||||||
|
letterMap = "abcdefgh";
|
||||||
|
setUI();
|
||||||
|
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.webp',
|
||||||
|
r: '/static/black_rook.webp',
|
||||||
|
c: '/static/black_castle.webp',
|
||||||
|
b: '/static/black_bishop.webp',
|
||||||
|
k: '/static/black_king.webp',
|
||||||
|
q: '/static/black_queen.webp',
|
||||||
|
|
||||||
|
P: '/static/white_pawn.webp',
|
||||||
|
R: '/static/white_rook.webp',
|
||||||
|
C: '/static/white_castle.webp',
|
||||||
|
B: '/static/white_bishop.webp',
|
||||||
|
K: '/static/white_king.webp',
|
||||||
|
Q: '/static/white_queen.webp',
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
182
home.html
Normal 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
|
After Width: | Height: | Size: 1.7 KiB |
BIN
icon_update.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
79
index.html
@@ -1,79 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Document</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
color: 262942;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
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 {
|
|
||||||
display: flex;
|
|
||||||
padding: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100vw;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
#board {
|
|
||||||
width: 38rem;
|
|
||||||
height: 38rem;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(8, 1fr);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1 class="title">shatranj</h1>
|
|
||||||
</header>
|
|
||||||
<div class="center">
|
|
||||||
<div id="board">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
const container = document.getElementById("board");
|
|
||||||
|
|
||||||
const cols = ["A", "B", "C", "D", "E", "F", "G", "H"];
|
|
||||||
|
|
||||||
for (let i = 0; i < 64; i++) {
|
|
||||||
let col = i % 8;
|
|
||||||
let row = Math.floor(i/8);
|
|
||||||
const div = document.createElement("div");
|
|
||||||
div.className = "house";
|
|
||||||
div.id = cols[col] + (row+1).toString()
|
|
||||||
if (i % 2 == row % 2) {
|
|
||||||
div.style.backgroundColor = "#262942"
|
|
||||||
} else {
|
|
||||||
div.style.backgroundColor = "#faf5f0"
|
|
||||||
}
|
|
||||||
container.appendChild(div);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
1
libs
Submodule
BIN
logo_bg.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
@@ -1,3 +0,0 @@
|
|||||||
from .slow import JSONAPI, App, HTTPResponse, JSONResponse, render
|
|
||||||
|
|
||||||
__all__ = ["JSONAPI", "App", "HTTPResponse", "JSONResponse", "render"]
|
|
||||||
176
slow/slow.py
@@ -1,176 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
PR = re.compile(r"\<([a-zA-Z_]+)\>")
|
|
||||||
|
|
||||||
|
|
||||||
async def _default_404_route(request):
|
|
||||||
return "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\n404 Not Found contact admin"
|
|
||||||
|
|
||||||
|
|
||||||
async def _default_405_route(request):
|
|
||||||
return "HTTP/1.1 405 Method Not Allowed\r\nContent-Type: text/html\r\n\r\n405 Method Not Allowed"
|
|
||||||
|
|
||||||
|
|
||||||
class App:
|
|
||||||
def __init__(self):
|
|
||||||
self.routes: dict[re.Pattern[str], dict[str, callable]] = {}
|
|
||||||
self.error_routes: dict[int, callable] = {
|
|
||||||
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, method, func):
|
|
||||||
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):
|
|
||||||
"""Decorator to register a GET HTTP route."""
|
|
||||||
|
|
||||||
def decorator(func):
|
|
||||||
self._serve(path, "GET", func)
|
|
||||||
return func
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def POST(self, path):
|
|
||||||
"""Decorator to register a POST HTTP route."""
|
|
||||||
|
|
||||||
def decorator(func):
|
|
||||||
self._serve(path, "POST", func)
|
|
||||||
return func
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def PUT(self, path):
|
|
||||||
"""Decorator to register a PUT HTTP route."""
|
|
||||||
|
|
||||||
def decorator(func):
|
|
||||||
self._serve(path, "PUT", func)
|
|
||||||
return func
|
|
||||||
|
|
||||||
def DELETE(self, path):
|
|
||||||
"""Decorator to register a DELETE HTTP route."""
|
|
||||||
|
|
||||||
def decorator(func):
|
|
||||||
self._serve(path, "DELETE", 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, 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], m.groupdict()
|
|
||||||
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 = {}
|
|
||||||
|
|
||||||
content_length = int(headers.get("Content-Length", 0))
|
|
||||||
body = await reader.read(content_length) if content_length else b""
|
|
||||||
|
|
||||||
route, kwargs = self.resolve(path)
|
|
||||||
response = await route(
|
|
||||||
request={
|
|
||||||
"method": method,
|
|
||||||
"path": path,
|
|
||||||
"headers": headers,
|
|
||||||
"body": body,
|
|
||||||
},
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
writer.write(response.encode(encoding="utf-8"))
|
|
||||||
|
|
||||||
await writer.drain()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
finally:
|
|
||||||
writer.close()
|
|
||||||
await writer.wait_closed()
|
|
||||||
|
|
||||||
async def run(self, host="127.0.0.1", port=80):
|
|
||||||
"""Start the async server."""
|
|
||||||
|
|
||||||
server = await asyncio.start_server(self.handle_client, host, port)
|
|
||||||
|
|
||||||
print(f"Serving on {host}:{port}")
|
|
||||||
async with server:
|
|
||||||
await server.serve_forever()
|
|
||||||
|
|
||||||
|
|
||||||
def HTTPResponse(content: str, status=200, content_type="text/plain; charset=utf-8"):
|
|
||||||
return f"HTTP/1.1 {status} OK\r\nContent-Type: {content_type}\r\n\r\n{content}"
|
|
||||||
|
|
||||||
|
|
||||||
def render(file: str | Path):
|
|
||||||
if type(file) is str:
|
|
||||||
file = Path(file)
|
|
||||||
content: str = file.read_text()
|
|
||||||
return HTTPResponse(content, content_type="text/html; charset=utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def JSONResponse(d: dict, status=200):
|
|
||||||
return HTTPResponse(
|
|
||||||
json.dumps(d), status=status, content_type="text/json; charset=utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def JSONAPI(func):
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
result = 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)
|
|
||||||
BIN
static/background.png
Normal file
|
After Width: | Height: | Size: 873 KiB |
BIN
static/black_bishop.png
Normal file
|
After Width: | Height: | Size: 1005 B |
BIN
static/black_bishop.webp
Normal file
|
After Width: | Height: | Size: 400 B |
BIN
static/black_castle.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
static/black_castle.webp
Normal file
|
After Width: | Height: | Size: 324 B |
BIN
static/black_king.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
static/black_king.webp
Normal file
|
After Width: | Height: | Size: 608 B |
BIN
static/black_pawn.png
Normal file
|
After Width: | Height: | Size: 484 B |
BIN
static/black_pawn.webp
Normal file
|
After Width: | Height: | Size: 364 B |
BIN
static/black_queen.png
Normal file
|
After Width: | Height: | Size: 973 B |
BIN
static/black_queen.webp
Normal file
|
After Width: | Height: | Size: 462 B |
BIN
static/black_rook.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
static/black_rook.webp
Normal file
|
After Width: | Height: | Size: 444 B |
BIN
static/endgame.mp3
Normal file
BIN
static/logo.png
Normal file
|
After Width: | Height: | Size: 326 KiB |
BIN
static/logo.webp
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
static/piecemove.mp3
Normal file
BIN
static/white_bishop.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
static/white_bishop.webp
Normal file
|
After Width: | Height: | Size: 508 B |
BIN
static/white_castle.png
Normal file
|
After Width: | Height: | Size: 715 B |
BIN
static/white_castle.webp
Normal file
|
After Width: | Height: | Size: 424 B |
BIN
static/white_king.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
static/white_king.webp
Normal file
|
After Width: | Height: | Size: 684 B |
BIN
static/white_pawn.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
static/white_pawn.webp
Normal file
|
After Width: | Height: | Size: 602 B |
BIN
static/white_queen.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
static/white_queen.webp
Normal file
|
After Width: | Height: | Size: 794 B |
BIN
static/white_rook.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
static/white_rook.webp
Normal file
|
After Width: | Height: | Size: 554 B |