Files
shatranj/app.py
2026-01-15 16:06:25 +03:30

281 lines
8.3 KiB
Python

import asyncio
import json
import uuid
from datetime import datetime
from enum import Enum
from itertools import combinations
from typing import TypedDict
from slow import JSONAPI, App, JSONResponse, render
class Coord(TypedDict):
x: int
y: int
app = App()
class Color(Enum):
WHITE = 0
BLACK = 1
class Piece(Enum):
NONE = ""
EMPTY = "E"
# WHITE
WHITE_PAWN = "P"
WHITE_ROOK = "R"
WHITE_BISHOP = "B"
WHITE_KING = "K"
WHITE_QUEEN = "Q"
WHITE_CASTLE = "C"
# BLACK
BLACK_PAWN = "p"
BLACK_ROOK = "r"
BLACK_BISHOP = "b"
BLACK_KING = "k"
BLACK_QUEEN = "q"
BLACK_CASTLE = "c"
def xy_to_coord(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]))
class Board:
grid: list[list[Piece]]
def __init__(self):
self.grid = [[Piece.EMPTY] * 8] * 8
self.grid[0] = [
Piece.WHITE_CASTLE,
Piece.WHITE_ROOK,
Piece.WHITE_BISHOP,
Piece.WHITE_KING,
Piece.WHITE_QUEEN,
Piece.WHITE_BISHOP,
Piece.WHITE_ROOK,
Piece.WHITE_CASTLE,
]
self.grid[1] = [Piece.WHITE_PAWN] * 8
self.grid[6] = [Piece.BLACK_PAWN] * 8
self.grid[7] = [
Piece.BLACK_CASTLE,
Piece.BLACK_ROOK,
Piece.BLACK_BISHOP,
Piece.BLACK_KING,
Piece.BLACK_QUEEN,
Piece.BLACK_BISHOP,
Piece.BLACK_ROOK,
Piece.BLACK_CASTLE,
]
def index(self, pos: str) -> Piece:
let = "abcdefgh".index(pos[0])
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:
return Piece.NONE
return self.grid[xy.y][xy.x]
def serialize(self) -> dict:
return {"grid": self.grid}
class Room:
board: Board = Board()
turn: Color = Color.WHITE
players: list[uuid.UUID] = []
last_move: datetime
def start(self):
self.last_move = datetime.now()
def add_player(self) -> None | tuple[uuid.UUID, Color]:
np: int = len(self.players)
if np >= 2:
return None
elif np == 1:
uid = uuid.uuid4()
self.players.append(uid)
return uid, Color.BLACK
else:
uid = uuid.uuid4()
self.players.append(uid)
return uid, Color.WHITE
rooms: dict[str, Room] = {}
@app.GET("/")
async def home(request):
return render("index.html")
@app.POST("/join/<id>")
async def join(request, room_id):
room: Room = rooms.get(room_id, Room())
player = room.add_player()
if player:
return JSONResponse(
{
"code": "JOIN",
"id": player[0],
"color": player[1],
"board": room.board.serialize(),
}
)
else:
return JSONResponse(
{"code": "FULL", "error": "Room Full", "board": room.board.serialize()}
)
def parse(body: str) -> dict | None:
try:
return json.loads(body)
except Exception:
return None
@app.POST("/move/<id>")
@JSONAPI
async def move(request, room_id):
req = parse(request.body)
if not req:
return 400, {}
if "uid" not in req or not isinstance(req["uid"], str):
return 400, {}
if "from" not in req or not isinstance(req["from"], str):
return 400, {}
if "to" not in req or not isinstance(req["to"], str):
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
uid = req["uid"]
src = req["from"].lower()
dst = req["to"].lower()
color = Color.WHITE if room.players[0] == uid else Color.BLACK
turn = room.turn == color
if not turn:
return 400, {"code": "OTHR", "error": "Not your turn."}
if (
len(src) != 2
or len(dst) != 2
or src[0] not in "abcdefgh"
or src[1] not in "12345678"
or dst[0] not in "abcdefgh"
or dst[1] not in "12345678"
or board.index(src) != Piece.EMPTY
):
return 400, {"code": "LMOV", "error": "Bad Move"}
srcp = board.index(src)
dstp = board.index(dst)
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"}
if dstp != Piece.EMPTY and (
(dstp.value.isupper() and color == Color.WHITE)
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})
if (
board.index_xy(x + 1, y + dir)
not in [Piece.EMPTY, Piece.NONE, Piece.BLACK_KING, Piece.WHITE_KING]
and board.index_xy(x + 1, y + dir).value.upper() != is_white
):
valids.append({"x": x + 1, "y": y + 1})
if (
board.index_xy(x - 1, y + dir)
not in [Piece.EMPTY, Piece.NONE, Piece.BLACK_KING, Piece.WHITE_KING]
and board.index_xy(x - 1, y + dir).value.upper() != is_white
):
valids.append({"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())
if __name__ == "__main__":
asyncio.run(app.run(port=8080))