ChessNut/public/game.html
2025-11-22 15:25:25 +01:00

356 lines
14 KiB
HTML

<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>ChessNut - Partie</title>
<link rel="stylesheet" href="/styles/style.css">
</head>
<body>
<h2>ChessNut — Partie</h2>
<div>Room: <strong id="roomId">-</strong> &nbsp;&nbsp; Vous jouez: <strong id="myColor">-</strong>
<div class="layout-row">
<div>
<div id="board"></div>
</div>
<aside style="min-width:320px;margin-left:18px">
<section class="card" style="margin-top:12px">
<div><strong>Outils / Logs</strong></div>
<div style="margin-top:8px">
<!-- movement controls removed as requested -->
</div>
<pre id="log" style="margin-top:8px;height:160px;overflow:auto">logs...</pre>
</section>
</aside>
</div>
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<script>
function qs(name){ const url = new URL(window.location.href); return url.searchParams.get(name); }
const roomId = qs('roomId');
const playerId = qs('playerId') || undefined;
const roomIdEl = document.getElementById('roomId');
const playerIdEl = document.getElementById('playerId');
if(roomIdEl) roomIdEl.textContent = roomId || '-';
if(playerIdEl) playerIdEl.textContent = playerId || '-';
// optional UI elements: support removing the sidebar/sections without breaking JS
const logEl = document.getElementById('log');
const playersEl = document.getElementById('players');
const boardEl = document.getElementById('board');
const myColorEl = document.getElementById('myColor');
let socket = null;
let myColor = null;
// selection state for this client
let selectedSquare = null; // algebraic name like 'e2'
let selectedSquareEl = null;
let myPlayerId = playerId || null;
// track remote selections: { playerId: square }
const remoteSelections = {};
// track remote moves: { playerId: [moves] }
const remoteMoves = {};
// safe logger: prefer on-page log if present, otherwise console
function log(...args){
if(logEl){
try{
logEl.textContent += '\n' + args.map(a=>typeof a==='object'?JSON.stringify(a):a).join(' ');
logEl.scrollTop = logEl.scrollHeight;
}catch(e){ console.log(...args); }
} else {
console.log(...args);
}
}
if(!roomId){ alert('roomId manquant dans l\'URL'); }
function connectAndJoin(){
socket = io();
socket.on('connect', ()=>{ log('socket connecté', socket.id); });
socket.on('room:update', (data)=>{
log('room:update', data);
// server now sends `boardState` (JSON). renderBoardFromState will handle it.
if(playersEl){
playersEl.innerHTML = '';
(data.players||[]).forEach(p=>{
const li = document.createElement('li');
li.textContent = p.id + ' (' + p.color + ')';
playersEl.appendChild(li);
});
}
if(data.size){
// ensure grid layout matches requested size and build squares
buildGrid(data.size);
// adjust board width/height responsively (keep squares roughly proportional)
const side = Math.min(800, Math.max(320, data.size * 80));
boardEl.style.width = side + 'px';
boardEl.style.height = side + 'px';
}
if(data.boardState) renderBoardFromState(data.boardState);
});
socket.on('game:over', (data)=>{ log('game:over', data); if(data.boardState) renderBoardFromState(data.boardState); });
socket.on('game:started', (data)=>{ log('game:started', data); if(data.boardState) renderBoardFromState(data.boardState); });
// selection events (include moves) broadcasted by server
socket.on('game:select', (data) => {
try{
const pid = data && data.playerId;
const square = data && data.square;
const moves = data && data.moves;
log('recv game:select', { pid, square, moves: Array.isArray(moves) ? moves.length : moves });
// cleared selection
if(!square){
if(pid === myPlayerId){
// our selection cleared (maybe by reconnection) - clear locally without re-emitting
clearSelection(false);
} else {
if(pid){
clearRemoteSelection(pid);
}
}
return;
}
if(pid === myPlayerId){
// re-apply local selection (don't emit) and show markers
setSelection(square, false);
if(Array.isArray(moves) && moves.length) showLegalMoves(moves);
else clearLegalMarkers();
} else {
// remote player's selection
if(pid) setRemoteSelection(pid, square);
if(Array.isArray(moves) && moves.length){
remoteMoves[pid] = moves;
showRemoteLegalMoves(pid, moves);
} else {
remoteMoves[pid] = [];
if(pid) clearRemoteMoveMarkers(pid);
}
}
}catch(e){ console.warn('game:select handler error', e); }
});
socket.emit('room:join', { roomId, playerId }, (resp)=>{
if(resp && resp.error){ log('join error', resp); alert(resp.error); return; }
log('Rejoint room', resp);
// remember assigned id/color from server
if(resp.playerId){
myPlayerId = resp.playerId;
if(playerIdEl) playerIdEl.textContent = resp.playerId;
}
if(resp.color){
myColor = resp.color;
if(myColorEl) myColorEl.textContent = myColor;
// orient board for black
if(boardEl){
if(myColor === 'black') boardEl.classList.add('flipped');
else boardEl.classList.remove('flipped');
}
}
});
}
connectAndJoin();
// movement UI removed: legalMoves and move markers are disabled
// selection helpers (client-side)
function clearSelection(emit = true){
if(selectedSquareEl){
selectedSquareEl.classList.remove('selected');
selectedSquareEl.classList.remove('hide-selection');
}
selectedSquare = null;
selectedSquareEl = null;
const fromEl = document.getElementById('from');
if(fromEl) fromEl.value = '';
// notify others that we cleared selection
if(emit && socket && myPlayerId){
socket.emit('game:select', { roomId, square: null }, ()=>{});
}
// clear any shown legal-move markers
if(boardEl) clearLegalMarkers();
}
function setSelection(squareName, emit = true){
if(!squareName) return clearSelection(emit);
if(selectedSquareEl) selectedSquareEl.classList.remove('selected');
// clear any existing markers immediately (so selecting a piece with no moves removes old markers)
clearLegalMarkers();
const sqEl = boardEl.querySelector(`.square[data-square="${squareName}"]`);
if(!sqEl){ selectedSquare = null; selectedSquareEl = null; return; }
selectedSquare = squareName;
selectedSquareEl = sqEl;
sqEl.classList.add('selected');
// hide the square selection outline while keeping the piece
sqEl.classList.add('hide-selection');
const fromEl = document.getElementById('from');
if(fromEl) fromEl.value = squareName;
// notify others of our selection
if(emit && socket && myPlayerId){
socket.emit('game:select', { roomId, square: squareName }, ()=>{});
}
// legal moves will be provided by server via the 'game:select' event
}
// show markers for legal moves when clicking any square (any piece color)
function clearLegalMarkers(){
if(!boardEl) return;
const prev = boardEl.querySelectorAll('.move-marker');
prev.forEach(p=>p.remove());
}
function showLegalMoves(moves){
clearLegalMarkers();
(moves||[]).forEach(m => {
const to = m.to || (m.move && m.move.to);
if(!to) return;
const target = boardEl.querySelector(`.square[data-square="${to}"]`);
if(!target) return;
const marker = document.createElement('div');
marker.className = 'move-marker';
// store metadata if needed later
marker.dataset.from = m.from || '';
marker.dataset.to = to;
target.appendChild(marker);
});
}
// remote move markers: attach remoteBy and allow clearing per player
function showRemoteLegalMoves(playerId, moves){
clearRemoteMoveMarkers(playerId);
(moves||[]).forEach(m => {
const to = m.to || (m.move && m.move.to);
if(!to) return;
const target = boardEl.querySelector(`.square[data-square="${to}"]`);
if(!target) return;
const marker = document.createElement('div');
marker.className = 'move-marker';
marker.dataset.from = m.from || '';
marker.dataset.to = to;
marker.dataset.remoteBy = playerId;
target.appendChild(marker);
});
}
function clearRemoteMoveMarkers(playerId){
if(!boardEl) return;
const prev = boardEl.querySelectorAll(`.move-marker[data-remote-by="${playerId}"]`);
prev.forEach(p=>p.remove());
}
// remote selection helpers: mark a square as selected by another player
function setRemoteSelection(playerId, squareName){
// clear previous for that player
clearRemoteSelection(playerId);
const sqEl = boardEl.querySelector(`.square[data-square="${squareName}"]`);
if(!sqEl) return;
remoteSelections[playerId] = squareName;
sqEl.classList.add('selected-remote');
// hide the square selection outline for remote selection as well
sqEl.classList.add('hide-selection');
sqEl.dataset.selectedBy = playerId;
}
function clearRemoteSelection(playerId){
const prev = remoteSelections[playerId];
if(!prev) return;
const prevEl = boardEl.querySelector(`.square[data-square="${prev}"]`);
if(prevEl){
prevEl.classList.remove('selected-remote');
delete prevEl.dataset.selectedBy;
prevEl.classList.remove('hide-selection');
}
delete remoteSelections[playerId];
// also clear remote move markers
clearRemoteMoveMarkers(playerId);
delete remoteMoves[playerId];
}
if(boardEl){
boardEl.addEventListener('click', (ev)=>{
const sqEl = ev.target.closest && ev.target.closest('.square');
if(!sqEl) return;
const square = sqEl.getAttribute('data-square');
if(!square) return;
// clicked a piece -> select it (no legal moves requested)
const pieceImg = ev.target.closest && ev.target.closest('img.piece');
if(pieceImg){
setSelection(square);
return;
}
// default: clicked empty square -> clear selection
clearSelection();
});
}
// move button removed
// Board rendering using provided SVG assets and `boardState` JSON.
const pieceSetPath = '/assets/chess-pieces/chess_maestro_bw';
// board visual defaults are handled by CSS (background, size, border, position)
function renderBoardFromState(boardState){
if(!boardState) return;
// clear existing pieces but keep squares if present
const squares = boardEl.querySelectorAll('.square');
squares.forEach(s=> s.innerHTML = '');
// place pieces according to boardState.pieces (expects algebraic square names)
(boardState.pieces || []).forEach(p => {
const squareName = p.square;
if(!squareName) return;
const sqEl = boardEl.querySelector(`.square[data-square="${squareName}"]`);
if(!sqEl) return;
const letter = p.type ? p.type.toUpperCase() : '?';
const filename = (p.color === 'w' ? 'w' + letter : 'b' + letter) + '.svg';
const img = document.createElement('img');
img.src = `${pieceSetPath}/${filename}`;
img.className = 'piece';
sqEl.appendChild(img);
});
// re-apply selection if the previously selected square still exists
if(selectedSquare){
// re-apply local selection without re-emitting
setSelection(selectedSquare, false);
}
// re-apply remote selections
Object.entries(remoteSelections).forEach(([pid, sq]) => {
const el = boardEl.querySelector(`.square[data-square="${sq}"]`);
if(el){ el.classList.add('selected-remote'); el.classList.add('hide-selection'); }
});
// re-apply remote move markers
Object.entries(remoteMoves).forEach(([pid, moves]) => {
if(Array.isArray(moves) && moves.length) showRemoteLegalMoves(pid, moves);
});
}
// build grid with independent square elements and data-square attributes
function buildGrid(size){
if(!size || size < 1) size = 8;
boardEl.style.gridTemplateColumns = `repeat(${size}, 1fr)`;
boardEl.style.gridTemplateRows = `repeat(${size}, 1fr)`;
// if grid already has correct number of squares, keep them
const existing = boardEl.querySelectorAll('.square');
if(existing.length === size * size) return;
boardEl.innerHTML = '';
// create rows from rank size..1 (so that a1 is bottom-left if CSS doesn't flip)
for(let rank = size; rank >= 1; rank--){
for(let file = 0; file < size; file++){
const fileLetter = String.fromCharCode('a'.charCodeAt(0) + file);
const squareName = `${fileLetter}${rank}`;
const sq = document.createElement('div');
// color squares in checker pattern
const dark = ((file + rank) % 2) === 0;
sq.className = 'square ' + (dark ? 'dark' : 'light');
sq.setAttribute('data-square', squareName);
boardEl.appendChild(sq);
}
}
}
</script>
</body>
</html>