Compare commits

..

39 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
0880
83b570214a Finishing game 2026-01-18 19:07:25 +03:30
0880
460255db9e Send UTC time 2026-01-18 19:06:57 +03:30
0880
56a01ac5a7 Handle finishing game 2026-01-18 19:06:03 +03:30
0880
4f6a45cbc7 UI Update 2026-01-18 19:05:38 +03:30
0880
4a998bca84 UI & SFX 2026-01-18 19:05:26 +03:30
0880
cc9cf0f1c2 Favicon 2026-01-18 19:04:57 +03:30
0880
3d7508fc29 SFX 2026-01-18 19:04:45 +03:30
0880
d2ed2b2842 Favicon 2026-01-18 19:03:06 +03:30
0880
bfe6c47d8e Update SlowAPI 2026-01-17 19:32:33 +03:30
0880
539c739c76 Fix a bunch of bugs 2026-01-17 19:29:13 +03:30
0880
dd91a8723b Remove API_ROOT 2026-01-17 18:56:21 +03:30
0880
975e813947 Remove API_ROOT 2026-01-17 18:56:03 +03:30
0880
467b7d4706 Remove debug 2026-01-17 18:55:42 +03:30
0880
14075ee2a3 Update 2026-01-17 18:55:31 +03:30
0880
adcda176cc Update SlowAPI and remove hacky HTTP response 2026-01-17 18:32:37 +03:30
0880
21e298ea9f Updates 2026-01-17 18:18:16 +03:30
0880
fe84972374 Use optimized images 2026-01-17 15:30:52 +03:30
0880
ebfe70f0bb Optimize images 2026-01-17 15:30:32 +03:30
0880
6e61b4d3d0 Remove debug logs 2026-01-17 15:28:31 +03:30
0880
1d7defd069 Make game responsive 2026-01-17 15:27:11 +03:30
24 changed files with 514 additions and 61 deletions

216
app.py
View File

@@ -1,14 +1,25 @@
import asyncio
import json
import random
import re
import string
import uuid
from collections import deque
from collections.abc import Iterator
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from enum import Enum
from itertools import product
from pathlib import Path
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:
@@ -27,7 +38,7 @@ class Coord:
return Coord(self.x, self.y)
def __str__(self):
return coord_to_pos_safe(self)
return str(coord_to_pos(self))
def __eq__(self, other):
if isinstance(other, Coord):
@@ -159,17 +170,20 @@ class Room:
turn: Color
players: list[str]
last_move: datetime
game_start: datetime | None
state: State
def __init__(self):
self.turn = Color.WHITE
self.board = Board()
self.players = []
self.last_move = datetime.now()
self.last_move = datetime.now(timezone.utc)
self.game_start = None
self.state = State.NOT_FINISHED
def start(self):
self.last_move = datetime.now()
self.last_move = datetime.now(timezone.utc)
self.game_start = datetime.now(timezone.utc)
def add_player(self) -> None | tuple[str, Color]:
np: int = len(self.players)
@@ -189,27 +203,114 @@ class Room:
rooms: dict[str, Room] = {}
API_ROOT = "http://127.0.0.1:8080"
favicon = Path("favicon.ico").read_bytes()
favicon_update = Path("favicon_update.ico").read_bytes()
@app.GET("/favicon.ico")
async def favicon_s(request):
return HTTPResponse(request, favicon, content_type="image/x-icon")
@app.GET("/favicon_update.ico")
async def favicon_update_s(request):
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("/")
async def home(request):
return render("index.html", {"API_ROOT": API_ROOT})
return render("home.html")
@app.GET("/<id>")
async def home_with_id(request, id):
return render("index.html", {"API_ROOT": API_ROOT})
async def game(request, id):
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>")
async def join(request, room_id):
if room_id not in rooms:
rooms[room_id] = Room()
return JSONResponse({"code": "NOGO", "error": "Room not found."}, 404)
room: Room = rooms[room_id]
player = room.add_player()
if player:
return JSONResponse(
request,
{
"code": "JOIN",
"id": player[0],
@@ -217,11 +318,26 @@ async def join(request, room_id):
"turn": room.turn.value,
"board": room.board.serialize(),
"ready": len(room.players) == 2,
}
"state": room.state.value,
"start_time": (
room.game_start or datetime.now(timezone.utc)
).isoformat(),
},
)
else:
return JSONResponse(
{"code": "FULL", "error": "Room Full", "board": room.board.serialize()}
request,
{
"code": "FULL",
"error": "Room Full",
"board": room.board.serialize(),
"ready": len(room.players) == 2,
"turn": room.turn.value,
"state": room.state.value,
"start_time": (
room.game_start or datetime.now(timezone.utc)
).isoformat(),
},
)
@@ -244,15 +360,15 @@ def get_piece_moves(piece_kind, board: Board, is_white, src: str) -> list[Coord]
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
and board.index_xy(x + 1, y + dir).value.isupper() != is_white
):
valids.append(Coord(x=x + 1, y=y + 1))
valids.append(Coord(x=x + 1, y=y + 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
and board.index_xy(x - 1, y + dir).value.isupper() != is_white
):
valids.append(Coord(x=x - 1, y=y + 1))
valids.append(Coord(x=x - 1, y=y + dir))
elif piece_kind == "b":
x, y = pos_to_coord(src)
comb = product([1, -1], repeat=2)
@@ -310,28 +426,31 @@ def get_piece_moves(piece_kind, board: Board, is_white, src: str) -> list[Coord]
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:
if (p := board.index_coord(target)) != Piece.NONE and (
p.value.isupper() != is_white or p.value == "E"
):
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:
if (p := board.index_coord(target)) != Piece.NONE and (
p.value.isupper() != is_white or p.value == "E"
):
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
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):
@@ -352,7 +471,7 @@ def generate_valid_moves(
king_safe = True
for i 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:
# Enemy
enemy_moves = get_piece_moves(
@@ -360,8 +479,16 @@ def generate_valid_moves(
)
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(
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:
king_safe = False
@@ -392,6 +519,8 @@ async def move(request: Request, room_id):
room = rooms[room_id]
if len(room.players) != 2:
return 400, {"code": "PRES", "error": "Game has not been started"}
if room.state != State.NOT_FINISHED:
return 400, {"code": "FINI", "error": "Game has not finished."}
board = room.board
uid = req["uid"]
src = req["from"].lower()
@@ -426,14 +555,30 @@ async def move(request: Request, room_id):
board.grid[c.y][c.x] = srcp
sx, sy = pos_to_coord(src)
board.grid[sy][sx] = Piece.EMPTY
if c.y == 0 or c.y == 7 and srcp in [Piece.BLACK_PAWN, Piece.WHITE_PAWN]:
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.last_move = datetime.now()
room.last_move = datetime.now(timezone.utc)
opp_checkmate = True
for i in range(8):
for j in range(8):
P = board.index_xy(i, j)
if P != "E" and P.value.isupper() != is_white:
moves = generate_valid_moves(
P.value.lower(), board, not is_white, xy_to_pos_safe(i, j)
)
if len(moves) > 0:
opp_checkmate = False
break
if not opp_checkmate:
break
if opp_checkmate:
room.state = State.BLACK_WIN if is_white else State.WHITE_WIN
return {
"code": "MOVD",
"color": color.value,
"turn": room.turn.value,
"state": room.state.value,
"board": board.serialize(),
}
else:
@@ -445,8 +590,14 @@ async def move(request: Request, room_id):
async def poll(request, room_id):
for key in rooms:
room = rooms[key]
if (datetime.now() - room.last_move) >= timedelta(hours=24):
if (datetime.now(timezone.utc) - room.last_move) >= timedelta(hours=24):
del rooms[key]
if (
room.game_start
and (datetime.now(timezone.utc) - room.game_start) >= timedelta(minutes=30)
and room.state == State.NOT_FINISHED
):
room.state = State.TIE
if room_id not in rooms:
return 400, {"code": "NOEX", "error": "Room does not exist"}
room = rooms[room_id]
@@ -456,6 +607,7 @@ async def poll(request, room_id):
"board": room.board.serialize(),
"ready": len(room.players) == 2,
"state": room.state.value,
"start_time": (room.game_start or datetime.now(timezone.utc)).isoformat(),
}
@@ -508,10 +660,8 @@ def sanitize_filename(filename: str, base: Path = Path.cwd().resolve()) -> Path:
async def static(request, fn):
try:
path = sanitize_filename(fn, Path("static/").resolve())
return (
HTTPResponse("", content_type="application/octet-stream")
+ path.read_bytes()
)
return HTTPResponse(path.read_bytes(), content_type="application/octet-stream")
except Exception:
return HTTPResponse("404 File Not Found", status=404)

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
favicon_update.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -28,8 +28,32 @@
.center {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
margin: 24px 0;
gap: 8px;
}
#stats {
font-family: 'Times New Roman', Times, serif;
font-weight: 100;
font-size: 32px;
color: #000;
}
#clock {
font-family: 'Times New Roman', Times, serif;
font-weight: 100;
font-size: 32px;
color: #000;
letter-spacing: 6px;
}
#turn {
border-top: 1px solid #68687aaa;
padding-top: 4px;
font-size: 14px;
color: #68687aaa;
}
.join-menu {
@@ -70,18 +94,29 @@
.game {
width: 600px;
height: 600px;
aspect-ratio: 1/1;
}
@media (max-width: 768px) {
.game {
width: 80%;
aspect-ratio: 1/1;
}
}
</style>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
</head>
<body>
<header>
<img src="/static/logo.png" alt="">
<img src="/static/logo.webp" alt="">
</header>
<div class="center">
<div id="join-menu" class="join-menu"><input type="text" id="room-id" placeholder="Room ID"><button
id="join-submit">Join</button></div>
<div id="join-menu" class="join-menu" style="display: none;"><input type="text" id="room-id"
placeholder="Room ID"><button id="join-submit">Join</button></div>
<h2 id="stats"></h2>
<h2 id="clock"></h2>
<h4 id="turn"></h4>
</div>
<div class="center">
<canvas class="game" id="canvas"></canvas>
@@ -105,11 +140,17 @@
let mHouse = [0, 0];
const textures = {};
const piecemoveSound = new Audio("/static/piecemove.mp3");
//const endgameSound = new Audio("/static/endgame.mp3");
let color = 0;
let turn = 0;
let state = -1;
let start_time = undefined;
let ready = false;
let letterMap = ' ';
let numberMap = ' ';
let oldBoard = undefined;
let board = undefined;
let UID = undefined;
let ROOM_ID = undefined;
@@ -117,6 +158,81 @@
let selected = undefined;
let moves = [];
function handleVisibilityChange() {
if (document.visibilityState === 'visible') {
let link = document.querySelector("link[rel~='icon']");
link.href = "/favicon.ico";
link.type = "image/x-icon";
}
}
document.addEventListener('visibilitychange', handleVisibilityChange);
function boardOnChange() {
if (JSON.stringify(board) !== JSON.stringify(oldBoard)) {
if (document.visibilityState !== 'visible') {
let link = document.querySelector("link[rel~='icon']");
link.href = "/favicon_update.ico";
link.type = "image/x-icon";
}
piecemoveSound.currentTime = Math.round(Math.random() * 4) * 0.5;
piecemoveSound.play()
.then(() => {
setTimeout(() => {
piecemoveSound.pause();
}, 400);
})
.catch(error => {
console.error("Playback failed:", error);
});
oldBoard = structuredClone(board);
}
}
function setUI() {
if (ready) {
if (state == -1) {
document.getElementById('stats').innerText = "";
const now = new Date();
const differenceMs = 30 * 60 * 1000 - Math.abs(start_time.getTime() - now.getTime());
const totalSeconds = Math.floor(differenceMs / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
const paddedMinutes = String(minutes).padStart(2, '0');
const paddedSeconds = String(seconds).padStart(2, '0');
document.getElementById('clock').innerText = `${paddedMinutes}:${paddedSeconds}`;
if (UID === undefined || turn != color) {
if (turn == 0) {
document.getElementById('turn').innerText = "White's Turn";
} else if (turn == 1) {
document.getElementById('turn').innerText = "Black's Turn";
}
} else if (turn == color) {
document.getElementById('turn').innerText = "Your Turn";
}
} else {
document.getElementById('turn').innerText = "";
if (state == 0) {
document.getElementById('stats').innerText = "Tie";
} else if (state == 1) {
document.getElementById('stats').innerText = "White Won";
} else if (state == 2) {
document.getElementById('stats').innerText = "Black Won";
}
}
} else {
document.getElementById('turn').innerText = "";
document.getElementById('clock').innerText = "";
document.getElementById('stats').innerText = "Waiting for Opponent";
}
}
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * width;
@@ -124,7 +240,7 @@
mouse = [x, y];
i = Math.floor((x / width) * 8);
j = Math.floor((y / height) * 8);
if (ready) {
if (ready && state == -1 && UID !== undefined) {
canvas.style.cursor = 'default';
if (board) {
if (color == 0) {
@@ -150,15 +266,12 @@
canvas.addEventListener('mouseup', (e) => {
i = mHouse[0];
j = mHouse[1];
if (ready && board) {
console.log(turn);
console.log(color);
console.log(selected);
if (state == -1 && ready && board) {
if (board[j][i] !== "E" && (board[j][i] === board[j][i].toUpperCase() ? 0 : 1) === color && turn == color) {
selected = [i, j];
moves = [];
fetch('{{ API_ROOT }}/moves/' + ROOM_ID, {
fetch('/moves/' + ROOM_ID, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -178,8 +291,7 @@
moves.forEach(move => {
console.log(move);
if (move[0] == i && move[1] == j) {
console.log("YAY");
fetch('{{ API_ROOT }}/move/' + ROOM_ID, {
fetch('/move/' + ROOM_ID, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -189,6 +301,7 @@
.then((response) => response.json())
.then((data) => {
board = data.board.grid;
boardOnChange();
console.log(data);
selected = undefined;
moves = [];
@@ -205,7 +318,7 @@
function update() {
if (ROOM_ID) {
fetch('{{ API_ROOT }}/poll/' + ROOM_ID, {
fetch('/poll/' + ROOM_ID, {
method: 'GET',
})
.then((response) => response.json())
@@ -213,6 +326,11 @@
ready = data.ready;
turn = data.turn;
board = data.board.grid;
boardOnChange();
state = data.state;
start_time = new Date(data.start_time);
setUI();
})
.catch((error) => console.error('Error:', error));
}
@@ -237,7 +355,7 @@
function join(rid) {
if (rid.length > 0 && /^[a-zA-Z0-9_-]+$/.test(rid)) {
fetch('{{ API_ROOT }}/join/' + rid, {
fetch('/join/' + rid, {
method: 'POST',
})
.then((response) => response.json())
@@ -249,11 +367,14 @@
color = data.color;
ready = data.ready;
turn = data.turn;
state = data.state;
start_time = new Date(data.start_time);
}
ROOM_ID = rid;
board = data.board.grid;
numberMap = "12345678";
letterMap = "abcdefgh";
setUI();
document.getElementById('join-menu').outerHTML = '';
const urlID = extractIdFromPath();
if (urlID == null) {
@@ -349,19 +470,19 @@
}
const assets = {
p: '/static/black_pawn.png',
r: '/static/black_rook.png',
c: '/static/black_castle.png',
b: '/static/black_bishop.png',
k: '/static/black_king.png',
q: '/static/black_queen.png',
p: '/static/black_pawn.webp',
r: '/static/black_rook.webp',
c: '/static/black_castle.webp',
b: '/static/black_bishop.webp',
k: '/static/black_king.webp',
q: '/static/black_queen.webp',
P: '/static/white_pawn.png',
R: '/static/white_rook.png',
C: '/static/white_castle.png',
B: '/static/white_bishop.png',
K: '/static/white_king.png',
Q: '/static/white_queen.png',
P: '/static/white_pawn.webp',
R: '/static/white_rook.webp',
C: '/static/white_castle.webp',
B: '/static/white_bishop.webp',
K: '/static/white_king.webp',
Q: '/static/white_queen.webp',
};
const loadPromises = Object.entries(assets).map(([key, src]) => {

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>

BIN
icon_default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
icon_update.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

2
libs

Submodule libs updated: 9947e2b429...fe65fafbe0

BIN
static/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 KiB

BIN
static/black_bishop.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

BIN
static/black_castle.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

BIN
static/black_king.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

BIN
static/black_pawn.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

BIN
static/black_queen.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

BIN
static/black_rook.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B

BIN
static/endgame.mp3 Normal file

Binary file not shown.

BIN
static/logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
static/piecemove.mp3 Normal file

Binary file not shown.

BIN
static/white_bishop.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

BIN
static/white_castle.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

BIN
static/white_king.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

BIN
static/white_pawn.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

BIN
static/white_queen.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

BIN
static/white_rook.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B