Many changes

This commit is contained in:
0880
2026-01-17 01:59:04 +03:30
parent 678f51dc69
commit 6089eb0b21

438
app.py
View File

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