Compare commits

..

20 Commits

Author SHA1 Message Date
0880
5b3e2cd0d3 Implement castling and create a utility function for applying move on a board and taking into account promotion during legal move generation 2026-01-21 14:23:22 +03:30
0880
d4a7b41d70 Allow leaving queue to not break other user's games 2026-01-21 14:01:23 +03:30
0880
ce5b660350 Add quick match making UI 2026-01-21 13:59:03 +03:30
0880
eecc9becbc styling 2026-01-21 13:58:31 +03:30
0880
752061f92f Fix piece movement generation 2026-01-21 13:58:14 +03:30
0880
e786d42e98 update 2026-01-21 13:40:08 +03:30
0880
ab13de5bc3 Fix winner condition 2026-01-21 13:25:19 +03:30
0880
a9d6476b75 Move boardOnChange after state update 2026-01-21 12:52:38 +03:30
0880
b269656833 Remove request in response 2026-01-21 12:14:19 +03:30
0880
d48ff10b2f Fix store id in quick_map 2026-01-21 12:13:17 +03:30
0880
8c0d13d096 Define quick matchmaking globals 2026-01-21 12:11:58 +03:30
0880
1bc950d97b Remove users who have left matchmaking 2026-01-21 11:43:09 +03:30
0880
890faab9fc Fix favicon endpoint 2026-01-21 11:31:40 +03:30
0880
ded7da8e73 Win sound 2026-01-21 11:29:25 +03:30
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
4 changed files with 162 additions and 68 deletions

137
app.py
View File

@@ -15,12 +15,11 @@ from typing import Any
from libs.slow import ( from libs.slow import (
JSONAPI, JSONAPI,
App, App,
HTTPResponse,
JSONResponse,
Request, Request,
redirect, redirect,
render, render,
) )
from libs.slow.responses import HTTPResponse, JSONResponse
class Coord: class Coord:
@@ -165,6 +164,32 @@ class Board:
def __str__(self): def __str__(self):
return "\n".join([" ".join([p.value for p in self.grid[i]]) for i in range(8)]) return "\n".join([" ".join([p.value for p in self.grid[i]]) for i in range(8)])
def make_move(self, src: str, dst: str):
sx, sy = pos_to_coord(src)
dx, dy = pos_to_coord(dst)
self.grid[dy][dx] = self.index(src)
piece_kind = self.grid[sy][sx].value.lower()
is_white = self.grid[sy][sx].value.isupper()
if piece_kind == "p" and (dy == 7 or dy == 0):
self.grid[dy][dx] = Piece.WHITE_QUEEN if is_white else Piece.BLACK_QUEEN
if (
is_white and dst == "e1" and piece_kind == "k" and dst == "g1"
): # Castling (White)
castle_from = pos_to_coord("h1")
castle_to = pos_to_coord("f1")
self.grid[castle_from.y][castle_from.x] = Piece.EMPTY
self.grid[castle_to.y][castle_to.x] = Piece.WHITE_CASTLE
if (
not is_white and src == "e8" and piece_kind == "k" and dst == "g8"
): # Castling (Black)
castle_from = pos_to_coord("h8")
castle_to = pos_to_coord("f8")
self.grid[castle_from.y][castle_from.x] = Piece.EMPTY
self.grid[castle_to.y][castle_to.x] = Piece.BLACK_CASTLE
self.grid[sy][sx] = Piece.EMPTY
class Room: class Room:
board: Board board: Board
@@ -210,12 +235,12 @@ favicon_update = Path("favicon_update.ico").read_bytes()
@app.GET("/favicon.ico") @app.GET("/favicon.ico")
async def favicon_s(request): async def favicon_s(request):
return HTTPResponse(request, favicon, content_type="image/x-icon") return HTTPResponse(favicon, content_type="image/x-icon")
@app.GET("/favicon_update.ico") @app.GET("/favicon_update.ico")
async def favicon_update_s(request): async def favicon_update_s(request):
return HTTPResponse(request, favicon_update, content_type="image/x-icon") return HTTPResponse(favicon_update, content_type="image/x-icon")
letters = string.ascii_lowercase + string.digits letters = string.ascii_lowercase + string.digits
@@ -231,25 +256,25 @@ def room_key():
@app.POST("/create_room") @app.POST("/create_room")
async def new_room(request): async def new_room(request):
if len(rooms) == len(letters) ** 4: if len(rooms) == len(letters) ** 4:
return HTTPResponse(request, "Out of service", status=501) return HTTPResponse("Out of service", status=501)
key = room_key() key = room_key()
rooms[key] = Room() rooms[key] = Room()
return JSONResponse( return JSONResponse(
request,
{ {
"id": key, "id": key,
}, },
) )
quick_queue: deque[str] quick_queue: deque[str] = deque()
quick_map: dict[str, Room] quick_map: dict[str, Room] = {}
quick_last_map: dict[str, datetime] = {}
lock = asyncio.Lock() lock = asyncio.Lock()
@app.POST("/quick") @app.POST("/quick")
async def quick_match(request: Request): async def quick_match(request: Request):
global quick_queue, quick_map global quick_queue, quick_map, quick_last_map
data = parse(request.body) data = parse(request.body)
async with lock: async with lock:
if ( if (
@@ -262,6 +287,13 @@ async def quick_match(request: Request):
second = None second = None
position: int = 0 position: int = 0
# UPDATE LOGIC # UPDATE LOGIC
now = datetime.now(timezone.utc)
for q, t in quick_last_map.items():
if t - now >= timedelta(seconds=3):
quick_queue.remove(q)
del quick_map[q]
del quick_last_map[q]
while position < len(quick_queue): while position < len(quick_queue):
if quick_queue[position] not in quick_map: if quick_queue[position] not in quick_map:
if not first: if not first:
@@ -274,26 +306,33 @@ async def quick_match(request: Request):
room = Room() room = Room()
k = room_key() k = room_key()
rooms[k] = room rooms[k] = room
quick_map[first] = room quick_map[first] = k
quick_map[second] = room quick_map[second] = k
qid = data["queue_id"] qid = data["queue_id"]
quick_last_map[qid] = datetime.now(timezone.utc)
if qid in quick_map: if qid in quick_map:
return JSONResponse(request, {"room_id": quick_map[qid]}) return JSONResponse({"room_id": quick_map[qid]})
else: else:
return JSONResponse( return JSONResponse({}) # Client handles empty as continue to wait
request, {} elif (
) # Client handles empty as continue to wait data
and len(data) == 2
and "queue_id" in data
and data["queue_id"] in quick_queue
and "leave" in data
):
quick_queue.remove(data["queue_id"])
else: else:
qid = str(uuid.uuid4()) qid = str(uuid.uuid4())
quick_queue.append(qid) quick_queue.append(qid)
return JSONResponse(request, {"queue_id": qid}) quick_last_map[qid] = datetime.now(timezone.utc)
return JSONResponse({"queue_id": qid})
@app.GET("/") @app.GET("/")
async def home(request): async def home(request):
return render(request, "home.html") return render("home.html")
@app.GET("/<id>") @app.GET("/<id>")
@@ -302,18 +341,17 @@ async def game(request, id):
return redirect("/") return redirect("/")
if id not in rooms: if id not in rooms:
return redirect("/") return redirect("/")
return render(request, "game.html") return render("game.html")
@app.POST("/join/<room_id>") @app.POST("/join/<room_id>")
async def join(request, room_id): async def join(request, room_id):
if room_id not in rooms: if room_id not in rooms:
return JSONResponse(request, {"code": "NOGO", "error": "Room not found."}, 404) return JSONResponse({"code": "NOGO", "error": "Room not found."}, 404)
room: Room = rooms[room_id] 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],
@@ -329,7 +367,6 @@ async def join(request, room_id):
) )
else: else:
return JSONResponse( return JSONResponse(
request,
{ {
"code": "FULL", "code": "FULL",
"error": "Room Full", "error": "Room Full",
@@ -361,14 +398,12 @@ def get_piece_moves(piece_kind, board: Board, is_white, src: str) -> list[Coord]
if (y == 1 or y == 6) and board.index_xy(x, y + 2 * dir) == Piece.EMPTY: 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)) valids.append(Coord(x=x, y=y + 2 * dir))
if ( if (
board.index_xy(x + 1, y + dir) board.index_xy(x + 1, y + dir) not in [Piece.EMPTY, Piece.NONE]
not in [Piece.EMPTY, Piece.NONE, Piece.BLACK_KING, Piece.WHITE_KING]
and board.index_xy(x + 1, y + dir).value.isupper() != is_white and board.index_xy(x + 1, y + dir).value.isupper() != is_white
): ):
valids.append(Coord(x=x + 1, y=y + dir)) valids.append(Coord(x=x + 1, y=y + dir))
if ( if (
board.index_xy(x - 1, y + dir) board.index_xy(x - 1, y + dir) not in [Piece.EMPTY, Piece.NONE]
not in [Piece.EMPTY, Piece.NONE, Piece.BLACK_KING, Piece.WHITE_KING]
and board.index_xy(x - 1, y + dir).value.isupper() != is_white and board.index_xy(x - 1, y + dir).value.isupper() != is_white
): ):
valids.append(Coord(x=x - 1, y=y + dir)) valids.append(Coord(x=x - 1, y=y + dir))
@@ -425,6 +460,20 @@ def get_piece_moves(piece_kind, board: Board, is_white, src: str) -> list[Coord]
elif piece_kind == "k": elif piece_kind == "k":
x, y = pos_to_coord(src) x, y = pos_to_coord(src)
comb = product([-1, 0, 1], repeat=2) comb = product([-1, 0, 1], repeat=2)
if (
src == "e1"
and is_white
and board.index("f1") == Piece.EMPTY
and board.index("g1") == Piece.EMPTY
): # Castle (White)
valids.append(pos_to_coord("g1"))
if (
src == "e8"
and is_white
and board.index("f8") == Piece.EMPTY
and board.index("g8") == Piece.EMPTY
): # Castling (Black)
valids.append(pos_to_coord("g8"))
for dir in comb: for dir in comb:
if dir[0] ** 2 + dir[1] ** 2 == 0: if dir[0] ** 2 + dir[1] ** 2 == 0:
continue # x=0 y=0 cannot move => invalid continue # x=0 y=0 cannot move => invalid
@@ -465,33 +514,19 @@ def generate_valid_moves(
): ):
king = Coord(j, i) king = Coord(j, i)
break break
for m in possible_moves: for m in possible_moves:
fake_board = board.copy() fake_board = board.copy()
fake_board.grid[m.y][m.x] = fake_board.index(src) fake_board.make_move(src, coord_to_pos_safe(m))
sx, sy = pos_to_coord(src)
fake_board.grid[sy][sx] = Piece.EMPTY
king_safe = True king_safe = True
for i in range(8): for i in range(8):
for j in range(8): for j in range(8):
p = board.grid[i][j] p = board.index_xy(j, i)
if p != Piece.EMPTY and p.value.isupper() != is_white: 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( new_enemy_moves = get_piece_moves(
p.value.lower(), p.value.lower(),
fake_board, fake_board,
not is_white, not is_white,
xy_to_pos_safe(nj, ni), xy_to_pos_safe(j, i),
) )
if king in new_enemy_moves: if king in new_enemy_moves:
king_safe = False king_safe = False
@@ -554,12 +589,8 @@ async def move(request: Request, room_id):
): ):
return 400, {"code": "HTME", "error": "Cannot move to your own piece"} return 400, {"code": "HTME", "error": "Cannot move to your own piece"}
valid_moves = generate_valid_moves(piece_kind, board, is_white, src) valid_moves = generate_valid_moves(piece_kind, board, is_white, src)
if (c := pos_to_coord(dst)) in valid_moves: if pos_to_coord(dst) in valid_moves:
board.grid[c.y][c.x] = srcp board.make_move(src, dst)
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.turn = Color.BLACK if color == Color.WHITE else Color.WHITE
room.last_move = datetime.now(timezone.utc) room.last_move = datetime.now(timezone.utc)
opp_checkmate = True opp_checkmate = True
@@ -576,7 +607,7 @@ async def move(request: Request, room_id):
if not opp_checkmate: if not opp_checkmate:
break break
if opp_checkmate: if opp_checkmate:
room.state = State.BLACK_WIN if is_white else State.WHITE_WIN room.state = State.WHITE_WIN if is_white else State.BLACK_WIN
return { return {
"code": "MOVD", "code": "MOVD",
"color": color.value, "color": color.value,
@@ -664,11 +695,9 @@ async def static(request, fn):
try: try:
path = sanitize_filename(fn, Path("static/").resolve()) path = sanitize_filename(fn, Path("static/").resolve())
return HTTPResponse( return HTTPResponse(path.read_bytes(), content_type="application/octet-stream")
request, path.read_bytes(), content_type="application/octet-stream"
)
except Exception: except Exception:
return HTTPResponse(request, "404 File Not Found", status=404) return HTTPResponse("404 File Not Found", status=404)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -141,11 +141,12 @@
const textures = {}; const textures = {};
const piecemoveSound = new Audio("/static/piecemove.mp3"); const piecemoveSound = new Audio("/static/piecemove.mp3");
//const endgameSound = new Audio("/static/endgame.mp3"); const endgameSound = new Audio("/static/endgame.mp3");
let color = 0; let color = 0;
let turn = 0; let turn = 0;
let state = -1; let state = -1;
let oldState = -1;
let start_time = undefined; let start_time = undefined;
let ready = false; let ready = false;
let letterMap = ' '; let letterMap = ' ';
@@ -187,14 +188,18 @@
.catch(error => { .catch(error => {
console.error("Playback failed:", error); console.error("Playback failed:", error);
}); });
if (oldState !== state && state >= 1) {
endgameSound.play();
}
oldBoard = structuredClone(board); oldBoard = structuredClone(board);
oldState = state;
} }
} }
function setUI() { function setUI() {
if (ready) { if (ready) {
if (state == -1) { if (state == -1) {
document.getElementById('stats').textCotnent = ""; document.getElementById('stats').innerText = "";
const now = new Date(); const now = new Date();
const differenceMs = 30 * 60 * 1000 - Math.abs(start_time.getTime() - now.getTime()); const differenceMs = 30 * 60 * 1000 - Math.abs(start_time.getTime() - now.getTime());
const totalSeconds = Math.floor(differenceMs / 1000); const totalSeconds = Math.floor(differenceMs / 1000);
@@ -210,7 +215,7 @@
if (UID === undefined || turn != color) { if (UID === undefined || turn != color) {
if (turn == 0) { if (turn == 0) {
document.getElementById('turn').innerText = "White's Turn"; document.getElementById('turn').innerText = "White's Turn";
} else if (turn == 0) { } else if (turn == 1) {
document.getElementById('turn').innerText = "Black's Turn"; document.getElementById('turn').innerText = "Black's Turn";
} }
} else if (turn == color) { } else if (turn == color) {
@@ -240,7 +245,7 @@
mouse = [x, y]; mouse = [x, y];
i = Math.floor((x / width) * 8); i = Math.floor((x / width) * 8);
j = Math.floor((y / height) * 8); j = Math.floor((y / height) * 8);
if (ready && state == -1) { if (ready && state == -1 && UID !== undefined) {
canvas.style.cursor = 'default'; canvas.style.cursor = 'default';
if (board) { if (board) {
if (color == 0) { if (color == 0) {
@@ -326,9 +331,9 @@
ready = data.ready; ready = data.ready;
turn = data.turn; turn = data.turn;
board = data.board.grid; board = data.board.grid;
boardOnChange();
state = data.state; state = data.state;
start_time = new Date(data.start_time); start_time = new Date(data.start_time);
boardOnChange();
setUI(); setUI();
}) })

View File

@@ -70,7 +70,6 @@
.code { .code {
padding: 16px; padding: 16px;
border: none; border: none;
border-bottom: 2px solid rgba(92, 92, 92, 0.664);
font-size: 1.5rem; font-size: 1.5rem;
text-align: center; text-align: center;
letter-spacing: 8px; letter-spacing: 8px;
@@ -94,19 +93,18 @@
.group { .group {
display: flex; display: flex;
height: 60px; height: 48px;
gap: 4px; gap: 12px;
} }
.group * { .group * {
height: 60px; height: 48px;
margin-bottom: 4px; margin-bottom: 4px;
} }
.vs { .vs {
height: calc(100% - 8px); height: calc(100% - 8px);
width: 2px; width: 2px;
margin: 4px;
background: rgba(92, 92, 92, 0.664); background: rgba(92, 92, 92, 0.664);
} }
@@ -126,6 +124,13 @@
background-repeat: no-repeat; background-repeat: no-repeat;
} }
#quick_match_info {
margin: 14px 0;
font-size: 18px;
user-select: none;
color: rgba(92, 92, 92, 0.664);
}
</style> </style>
<link rel="icon" href="/favicon.ico" type="image/x-icon"> <link rel="icon" href="/favicon.ico" type="image/x-icon">
</head> </head>
@@ -145,19 +150,74 @@
<button id="join" class="button">join</button> <button id="join" class="button">join</button>
</div> </div>
<div class="or"></div> <div class="or"></div>
Public games not added yet. <div id="quick_match_info" style="visibility: hidden;">Searching for a game</div>
<button class="button" id="quick_match">quick match</button>
</div> </div>
<script> <script>
let dots = 0;
setInterval(() => {
let text = "Searching for a game";
for (let i = 0; i < dots; i++) {
text += ".";
}
document.getElementById("quick_match_info").innerText = text;
dots += 1;
if (dots == 4) {
dots = 0;
}
}, 1000);
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const code = document.getElementById("code"); const code = document.getElementById("code");
const pat = new RegExp("^[a-z0-9]{4}$") const pat = new RegExp("^[a-z0-9]{4}$")
document.getElementById("join").addEventListener("click", () => { document.getElementById("join").addEventListener("click", () => {
if (!reg.test(code.value)) { if (!pat.test(code.value.toLowerCase())) {
alert("Invalid ID"); alert("Invalid ID");
} else { } else {
window.location.href = window.location.origin + "/" + code.value; window.location.href = window.location.origin + "/" + code.value.toLowerCase();
}
});
const qm = document.getElementById("quick_match");
let matchmaking = -1;
let qid = -1;
qm.addEventListener("click", () => {
if (qm.innerText.toLowerCase() === "cancel") {
qm.innerText = "quick match";
document.getElementById("quick_match_info").style.visibility = "hidden";
clearInterval(-1);
} else if (qm.innerText.toLowerCase() === "quick match") {
qm.innerText = "cancel";
document.getElementById("quick_match_info").style.visibility = "visible";
fetch("/quick", {
method: 'POST',
})
.then((response) => response.json())
.then((data) => {
qid = data.queue_id;
matchmaking = setInterval(() => {
fetch("/quick", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ "queue_id": qid }),
})
.then((response) => response.json())
.then((data) => {
if (data.room_id) {
clearInterval(matchmaking);
window.location.href = window.location.origin + "/" + data.room_id;
}
}).catch((error) => {
console.error(error);
})
}, 1500);
}).catch((error) => {
console.error(error);
})
} }
}); });
document.getElementById("create").addEventListener("click", () => { document.getElementById("create").addEventListener("click", () => {

2
libs

Submodule libs updated: 24334d1def...c455adea07