Files
shatranj/game.html
2026-01-20 19:54:57 +03:30

512 lines
19 KiB
HTML

<!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 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 {
display: flex;
width: min-content;
border-radius: 8px;
overflow: hidden;
border: 1px solid #48517b56;
}
.join-menu * {
border-radius: 0;
}
input,
button {
outline: none;
border: 0;
border-radius: 8px;
}
input {
padding: 8px;
}
button {
padding: 8px 24px;
cursor: pointer;
}
.piece {
width: 100%;
height: 100%;
z-index: 20;
position: relative;
pointer-events: none;
}
.game {
width: 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.webp" alt="">
</header>
<div class="center">
<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>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const canvas = document.getElementById('canvas');
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
const primary = '#f0d9b5';
const accent = '#b58863';
ctx.font = 'bolder 16px Arial';
let mouse = [0, 0];
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;
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').textCotnent = "";
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 == 0) {
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;
const y = ((e.clientY - rect.top) / rect.height) * height;
mouse = [x, y];
i = Math.floor((x / width) * 8);
j = Math.floor((y / height) * 8);
if (ready && state == -1) {
canvas.style.cursor = 'default';
if (board) {
if (color == 0) {
j = 7 - j;
} else {
i = 7 - i;
}
mHouse = [i, j];
if (board[j][i] !== "E" && (board[j][i] === board[j][i].toUpperCase() ? 0 : 1) === color && turn == color) {
canvas.style.cursor = 'pointer';
}
moves.forEach(move => {
if (move[0] == i && move[1] == j) {
canvas.style.cursor = 'pointer';
}
})
}
} else {
canvas.style.cursor = 'not-allowed';
}
});
canvas.addEventListener('mouseup', (e) => {
i = mHouse[0];
j = mHouse[1];
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('/moves/' + ROOM_ID, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ from: "ABCDEFGH"[i] + "12345678"[j] })
})
.then((response) => response.json())
.then((data) => {
moves_str = data.moves.split(",");
console.log(data.moves);
moves_str.forEach(move => {
moves.push(["abcdefgh".indexOf(move[0].toLowerCase()), "12345678".indexOf(move[1])])
});
})
.catch((error) => console.error('Error:', error));
} else if (turn == color && selected) {
moves.forEach(move => {
console.log(move);
if (move[0] == i && move[1] == j) {
fetch('/move/' + ROOM_ID, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ uid: UID, from: "ABCDEFGH"[selected[0]] + "12345678"[selected[1]], to: "ABCDEFGH"[i] + "12345678"[j] })
})
.then((response) => response.json())
.then((data) => {
board = data.board.grid;
boardOnChange();
console.log(data);
selected = undefined;
moves = [];
})
.catch((error) => console.error('Error:', error));
}
});
} else {
moves = [];
selected = undefined;
}
}
});
function update() {
if (ROOM_ID) {
fetch('/poll/' + ROOM_ID, {
method: 'GET',
})
.then((response) => response.json())
.then((data) => {
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));
}
}
setInterval(update, 1500);
function extractIdFromPath() {
const currentPath = window.location.pathname;
const idRegex = /^\/([^/]+)$/;
const match = currentPath.match(idRegex);
if (match) {
const extractedId = match[1];
return extractedId;
} else {
return null;
}
}
function join(rid) {
if (rid.length > 0 && /^[a-zA-Z0-9_-]+$/.test(rid)) {
fetch('/join/' + rid, {
method: 'POST',
})
.then((response) => response.json())
.then((data) => {
if (data.code === "FULL") {
alert("Game is full you can only spectate.");
} else {
UID = data.id;
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) {
history.pushState({ pageId: ROOM_ID }, '', `/${ROOM_ID}`);
}
})
.catch((error) => console.error('Error:', error));
} else {
alert("Invalid Room ID");
}
}
const urlID = extractIdFromPath();
if (urlID != null) {
join(urlID);
}
document.getElementById('join-submit').onclick = () => {
let rid = document.getElementById('room-id').value.trim();
join(rid);
};
function drawCenter(texture, x, y, width, height, padding) {
ctx.drawImage(
texture, x + padding, y + padding, width - padding * 2,
height - padding * 2);
}
function draw() {
ctx.clearRect(0, 0, width, height);
const w = width / 8;
const h = height / 8;
for (let i = 0; i < 8; i++) {
for (let j = 0; j < 8; j++) {
let I = i;
let J = j;
if (color == 0) {
J = 7 - J;
} else {
I = 7 - I;
}
ctx.beginPath();
if ((i * 8 + j) % 2 == i % 2) {
ctx.fillStyle = primary;
} else {
ctx.fillStyle = accent;
}
ctx.rect(i * w, j * h, w, h);
ctx.fill();
if (selected && selected[0] == I && selected[1] == J) {
ctx.fillStyle = "#00f0f0aa"
ctx.rect(i * w, j * h, w, h);
ctx.fill();
}
moves.forEach(move => {
if (move[0] == I && move[1] == J) {
if (board[J][I] !== 'E') {
ctx.fillStyle = "#fa2026aa"
} else {
ctx.fillStyle = "#f0f000aa"
}
ctx.rect(i * w, j * h, w, h);
ctx.fill();
}
});
if ((i * 8 + j) % 2 == i % 2) {
ctx.fillStyle = accent;
} else {
ctx.fillStyle = primary;
}
if (i == 7) {
ctx.fillText(numberMap[J], i * w + w - 16, j * h + 16);
}
if (j == 7) {
ctx.fillText(letterMap[I], i * w + 6, j * h + h - 6);
}
if (board) {
if (board[J][I] !== 'E') {
drawCenter(textures[board[J][I]], i * w, j * h, w, h, 6);
}
}
}
}
// ctx.beginPath();
// ctx.fillStyle = 'red';
// ctx.arc(mouse[0], mouse[1], 6, 0, Math.PI * 2);
// ctx.fill();
}
function loop() {
draw();
requestAnimationFrame(loop);
}
const assets = {
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.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]) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = src;
img.onload = () => resolve({ key, img });
img.onerror = () => reject(new Error(`Failed to load ${src}`));
});
});
Promise.all(loadPromises)
.then((loadedAssets) => {
loadedAssets.forEach(({ key, img }) => {
textures[key] = img;
});
loop();
})
.catch((err) => {
console.error('Failed to load assets:', err);
});
});
</script>
</body>
</html>