Compare commits

..

49 Commits

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

3
.gitmodules vendored Normal file
View File

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

View File

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

600
app.py
View File

@@ -1,18 +1,53 @@
import asyncio
import json
import random
import re
import string
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 itertools import combinations
from typing import TypedDict
from itertools import product
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
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()
@@ -22,6 +57,13 @@ class Color(Enum):
BLACK = 1
class State(Enum):
NOT_FINISHED = -1
TIE = 0
WHITE_WIN = 1
BLACK_WIN = 2
class Piece(Enum):
NONE = ""
EMPTY = "E"
@@ -43,27 +85,41 @@ class Piece(Enum):
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:
return None
return "abcdefgh"[xy.x] + "12345678"[xy.y]
def coord_to_xy(pos: str) -> tuple[int, int]:
return ("abcdefgh".inedx(pos[0]), "12345678".inedx(pos[1]))
def coord_to_pos_safe(xy: Coord) -> str:
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:
grid: list[list[Piece]]
def __init__(self):
self.grid = [[Piece.EMPTY] * 8] * 8
self.grid: list[list[Piece]] = [[Piece.EMPTY] * 8 for _ in range(8)]
self.grid[0] = [
Piece.WHITE_CASTLE,
Piece.WHITE_ROOK,
Piece.WHITE_BISHOP,
Piece.WHITE_KING,
Piece.WHITE_QUEEN,
Piece.WHITE_KING,
Piece.WHITE_BISHOP,
Piece.WHITE_ROOK,
Piece.WHITE_CASTLE,
@@ -75,8 +131,8 @@ class Board:
Piece.BLACK_CASTLE,
Piece.BLACK_ROOK,
Piece.BLACK_BISHOP,
Piece.BLACK_KING,
Piece.BLACK_QUEEN,
Piece.BLACK_KING,
Piece.BLACK_BISHOP,
Piece.BLACK_ROOK,
Piece.BLACK_CASTLE,
@@ -87,34 +143,59 @@ class Board:
num = "12345678".index(pos[1])
return self.grid[num][let]
def index_xy(self, xy: Coord) -> Piece:
if xy.x < 0 or xy.x > 7 or xy.y < 0 or xy.y > 7:
def index_coord(self, coord: Coord) -> Piece:
if coord.x < 0 or coord.x > 7 or coord.y < 0 or coord.y > 7:
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:
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:
board: Board = Board()
turn: Color = Color.WHITE
players: list[uuid.UUID] = []
board: Board
turn: Color
players: list[str]
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):
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)
if np >= 2:
return None
elif np == 1:
uid = uuid.uuid4()
uid = str(uuid.uuid4())
self.players.append(uid)
self.start()
return uid, Color.BLACK
else:
uid = uuid.uuid4()
uid = str(uuid.uuid4())
self.players.append(uid)
return uid, Color.WHITE
@@ -122,40 +203,306 @@ class 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("/")
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):
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()
if player:
return JSONResponse(
request,
{
"code": "JOIN",
"id": player[0],
"color": player[1],
"color": player[1].value,
"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(),
},
)
else:
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:
return json.loads(body)
except Exception:
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
async def move(request, room_id):
async def move(request: Request, room_id):
req = parse(request.body)
if not req:
return 400, {}
@@ -165,11 +512,15 @@ async def move(request, room_id):
return 400, {}
if "to" not in req or not isinstance(req["to"], str):
return 400, {}
if len(req) > 3:
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"}
if room.state != State.NOT_FINISHED:
return 400, {"code": "FINI", "error": "Game has not finished."}
board = room.board
uid = req["uid"]
src = req["from"].lower()
@@ -185,9 +536,9 @@ async def move(request, room_id):
or src[1] not in "12345678"
or dst[0] not in "abcdefgh"
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)
dstp = board.index(dst)
piece_kind = srcp.value.lower()
@@ -199,82 +550,121 @@ async def move(request, room_id):
or (dstp.value.islower() and color == Color.BLACK)
):
return 400, {"code": "HTME", "error": "Cannot move to your own piece"}
valids: list[Coord] = []
if piece_kind == "p":
dir = 1 if is_white else -1
x, y = coord_to_xy(src)
if board.index_xy(x, y + dir) == Piece.EMPTY:
valids.append({"x": x, "y": y + dir})
if (y == 1 or y == 6) and board.index_xy(x, y + 2 * dir) == Piece.EMPTY:
valids.append({"x": x, "y": y + 2 * dir})
valid_moves = generate_valid_moves(piece_kind, board, is_white, src)
if (c := pos_to_coord(dst)) in valid_moves:
board.grid[c.y][c.x] = srcp
sx, sy = pos_to_coord(src)
board.grid[sy][sx] = Piece.EMPTY
if (c.y == 0 or c.y == 7) and piece_kind == "p":
board.grid[c.y][c.x] = Piece.WHITE_QUEEN if is_white else Piece.BLACK_QUEEN
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 (
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
room.game_start
and (datetime.now(timezone.utc) - room.game_start) >= timedelta(minutes=30)
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 (
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
len(src) != 2
or src[0] not in "abcdefgh"
or src[1] not in "12345678"
or srcp == Piece.EMPTY
):
valids.append({"x": x - 1, "y": y + 1})
if piece_kind == "b":
x, y = coord_to_xy(src)
comb = combinations([1, -1], 2)
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 == "q":
x, y = coord_to_xy(src)
comb = combinations([1, -1, 0], 2)
for dir in comb:
if dir[0] ** 2 + dir[1] ** 2 == 0:
continue # x=0 y=0 cannot move => invalid
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 == "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())
return 400, {"code": "LMOV", "error": "Bad Move"}
piece_kind = srcp.value.lower()
is_white = srcp.value.isupper()
if (is_white and color == Color.BLACK) or (not is_white and color == Color.WHITE):
return 400, {"code": "NOTU", "error": "Not your piece"}
valid_moves = generate_valid_moves(piece_kind, board, is_white, src)
return {
"moves": ",".join([coord_to_pos_safe(c) for c in valid_moves]),
}
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(path.read_bytes(), content_type="application/octet-stream")
except Exception:
return HTTPResponse("404 File Not Found", status=404)
if __name__ == "__main__":
asyncio.run(app.run(port=8080))
asyncio.run(app.run(port=8989))

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
favicon_update.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

512
game.html Normal file
View 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
View File

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

BIN
icon_default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
icon_update.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -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

Submodule libs added at fe65fafbe0

BIN
logo_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 KiB

BIN
static/black_bishop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 B

BIN
static/black_bishop.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

BIN
static/black_castle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

BIN
static/black_castle.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

BIN
static/black_king.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
static/black_king.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

BIN
static/black_pawn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

BIN
static/black_pawn.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

BIN
static/black_queen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

BIN
static/black_queen.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

BIN
static/black_rook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
static/black_rook.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B

BIN
static/endgame.mp3 Normal file

Binary file not shown.

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

BIN
static/logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
static/piecemove.mp3 Normal file

Binary file not shown.

BIN
static/white_bishop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
static/white_bishop.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

BIN
static/white_castle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

BIN
static/white_castle.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

BIN
static/white_king.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
static/white_king.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

BIN
static/white_pawn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/white_pawn.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

BIN
static/white_queen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
static/white_queen.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

BIN
static/white_rook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
static/white_rook.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B