Compare commits

...

19 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
5 changed files with 310 additions and 31 deletions

122
app.py
View File

@@ -1,6 +1,10 @@
import asyncio import asyncio
import json import json
import random
import re
import string
import uuid import uuid
from collections import deque
from collections.abc import Iterator from collections.abc import Iterator
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from enum import Enum from enum import Enum
@@ -8,7 +12,14 @@ from itertools import product
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from libs.slow import JSONAPI, App, HTTPResponse, JSONResponse, Request, render from libs.slow import (
JSONAPI,
App,
Request,
redirect,
render,
)
from libs.slow.responses import HTTPResponse, JSONResponse
class Coord: class Coord:
@@ -206,20 +217,95 @@ async def favicon_update_s(request):
return HTTPResponse(request, favicon_update, content_type="image/x-icon") 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("/") @app.GET("/")
async def home(request): async def home(request):
return render(request, "index.html") return render("home.html")
@app.GET("/<id>") @app.GET("/<id>")
async def home_with_id(request, id): async def game(request, id):
return render(request, "index.html") 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>") @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:
rooms[room_id] = Room() 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:
@@ -245,6 +331,7 @@ async def join(request, room_id):
"code": "FULL", "code": "FULL",
"error": "Room Full", "error": "Room Full",
"board": room.board.serialize(), "board": room.board.serialize(),
"ready": len(room.players) == 2,
"turn": room.turn.value, "turn": room.turn.value,
"state": room.state.value, "state": room.state.value,
"start_time": ( "start_time": (
@@ -352,6 +439,11 @@ def get_piece_moves(piece_kind, board: Board, is_white, src: str) -> list[Coord]
p.value.isupper() != is_white or p.value == "E" p.value.isupper() != is_white or p.value == "E"
): ):
valids.append(target.copy()) 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 return valids
@@ -379,7 +471,7 @@ def generate_valid_moves(
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.grid[j][i]
if p != Piece.EMPTY and p.value.isupper() != is_white: if p != Piece.EMPTY and p.value.isupper() != is_white:
# Enemy # Enemy
enemy_moves = get_piece_moves( enemy_moves = get_piece_moves(
@@ -387,8 +479,16 @@ def generate_valid_moves(
) )
if king not in enemy_moves: if king not in enemy_moves:
continue 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(), fake_board, not is_white, xy_to_pos_safe(j, i) p.value.lower(),
fake_board,
not is_white,
xy_to_pos_safe(nj, ni),
) )
if king in new_enemy_moves: if king in new_enemy_moves:
king_safe = False king_safe = False
@@ -455,7 +555,7 @@ async def move(request: Request, room_id):
board.grid[c.y][c.x] = srcp board.grid[c.y][c.x] = srcp
sx, sy = pos_to_coord(src) sx, sy = pos_to_coord(src)
board.grid[sy][sx] = Piece.EMPTY board.grid[sy][sx] = Piece.EMPTY
if c.y == 0 or c.y == 7 and piece_kind == "p": 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 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)
@@ -561,11 +661,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

@@ -112,8 +112,8 @@
<img src="/static/logo.webp" alt=""> <img src="/static/logo.webp" alt="">
</header> </header>
<div class="center"> <div class="center">
<div id="join-menu" class="join-menu"><input type="text" id="room-id" placeholder="Room ID"><button <div id="join-menu" class="join-menu" style="display: none;"><input type="text" id="room-id"
id="join-submit">Join</button></div> placeholder="Room ID"><button id="join-submit">Join</button></div>
<h2 id="stats"></h2> <h2 id="stats"></h2>
<h2 id="clock"></h2> <h2 id="clock"></h2>
<h4 id="turn"></h4> <h4 id="turn"></h4>
@@ -177,7 +177,6 @@
link.href = "/favicon_update.ico"; link.href = "/favicon_update.ico";
link.type = "image/x-icon"; link.type = "image/x-icon";
} }
console.log("PLAYING SOUND!!");
piecemoveSound.currentTime = Math.round(Math.random() * 4) * 0.5; piecemoveSound.currentTime = Math.round(Math.random() * 4) * 0.5;
piecemoveSound.play() piecemoveSound.play()
.then(() => { .then(() => {
@@ -195,9 +194,9 @@
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 = 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);
const minutes = Math.floor(totalSeconds / 60); const minutes = Math.floor(totalSeconds / 60);
@@ -206,31 +205,31 @@
const paddedMinutes = String(minutes).padStart(2, '0'); const paddedMinutes = String(minutes).padStart(2, '0');
const paddedSeconds = String(seconds).padStart(2, '0'); const paddedSeconds = String(seconds).padStart(2, '0');
document.getElementById('clock').textCotnent = `${paddedMinutes}:${paddedSeconds}`; document.getElementById('clock').innerText = `${paddedMinutes}:${paddedSeconds}`;
if (UID === undefined || turn != color) { if (UID === undefined || turn != color) {
if (turn == 0) { if (turn == 0) {
document.getElementById('turn').textCotnent = "White's Turn"; document.getElementById('turn').innerText = "White's Turn";
} else if (turn == 0) { } else if (turn == 1) {
document.getElementById('turn').textCotnent = "Black's Turn"; document.getElementById('turn').innerText = "Black's Turn";
} }
} else if (turn == color) { } else if (turn == color) {
document.getElementById('turn').textCotnent = "Your Turn"; document.getElementById('turn').innerText = "Your Turn";
} }
} else { } else {
document.getElementById('turn').textCotnent = ""; document.getElementById('turn').innerText = "";
if (state == 0) { if (state == 0) {
document.getElementById('stats').textCotnent = "Tie"; document.getElementById('stats').innerText = "Tie";
} else if (state == 1) { } else if (state == 1) {
document.getElementById('stats').textCotnent = "White Won"; document.getElementById('stats').innerText = "White Won";
} else if (state == 2) { } else if (state == 2) {
document.getElementById('stats').textCotnent = "Black Won"; document.getElementById('stats').innerText = "Black Won";
} }
} }
} else { } else {
document.getElementById('turn').textCotnent = ""; document.getElementById('turn').innerText = "";
document.getElementById('clock').textCotnent = ""; document.getElementById('clock').innerText = "";
document.getElementById('stats').textCotnent = "Waiting for Opponent"; document.getElementById('stats').innerText = "Waiting for Opponent";
} }
} }
@@ -241,7 +240,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) {

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>

2
libs

Submodule libs updated: 3164c446c6...fe65fafbe0

BIN
static/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 KiB