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/") 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/") @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))