affichage des mouvements

This commit is contained in:
Didictateur 2025-11-22 15:25:25 +01:00
parent 580775e6b1
commit f2674b0874
3 changed files with 250 additions and 14 deletions

View file

@ -47,6 +47,8 @@
let myPlayerId = playerId || null; let myPlayerId = playerId || null;
// track remote selections: { playerId: square } // track remote selections: { playerId: square }
const remoteSelections = {}; const remoteSelections = {};
// track remote moves: { playerId: [moves] }
const remoteMoves = {};
// safe logger: prefer on-page log if present, otherwise console // safe logger: prefer on-page log if present, otherwise console
function log(...args){ function log(...args){
@ -89,20 +91,42 @@
}); });
socket.on('game:over', (data)=>{ log('game:over', data); 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); }); socket.on('game:started', (data)=>{ log('game:started', data); if(data.boardState) renderBoardFromState(data.boardState); });
// selection events broadcasted by other clients // selection events (include moves) broadcasted by server
socket.on('game:select', (data) => { socket.on('game:select', (data) => {
try{ try{
const pid = data && data.playerId; const pid = data && data.playerId;
const square = data && data.square; const square = data && data.square;
// ignore our own (some transports might echo) const moves = data && data.moves;
if(pid === myPlayerId) return; log('recv game:select', { pid, square, moves: Array.isArray(moves) ? moves.length : moves });
if(!pid) return; // cleared selection
if(!square){ if(!square){
// clear remote selection for that player if(pid === myPlayerId){
clearRemoteSelection(pid); // our selection cleared (maybe by reconnection) - clear locally without re-emitting
clearSelection(false);
} else {
if(pid){
clearRemoteSelection(pid);
}
}
return; return;
} }
setRemoteSelection(pid, square);
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); } }catch(e){ console.warn('game:select handler error', e); }
}); });
@ -134,6 +158,7 @@
function clearSelection(emit = true){ function clearSelection(emit = true){
if(selectedSquareEl){ if(selectedSquareEl){
selectedSquareEl.classList.remove('selected'); selectedSquareEl.classList.remove('selected');
selectedSquareEl.classList.remove('hide-selection');
} }
selectedSquare = null; selectedSquare = null;
selectedSquareEl = null; selectedSquareEl = null;
@ -143,22 +168,75 @@
if(emit && socket && myPlayerId){ if(emit && socket && myPlayerId){
socket.emit('game:select', { roomId, square: null }, ()=>{}); socket.emit('game:select', { roomId, square: null }, ()=>{});
} }
// clear any shown legal-move markers
if(boardEl) clearLegalMarkers();
} }
function setSelection(squareName, emit = true){ function setSelection(squareName, emit = true){
if(!squareName) return clearSelection(emit); if(!squareName) return clearSelection(emit);
if(selectedSquareEl) selectedSquareEl.classList.remove('selected'); 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}"]`); const sqEl = boardEl.querySelector(`.square[data-square="${squareName}"]`);
if(!sqEl){ selectedSquare = null; selectedSquareEl = null; return; } if(!sqEl){ selectedSquare = null; selectedSquareEl = null; return; }
selectedSquare = squareName; selectedSquare = squareName;
selectedSquareEl = sqEl; selectedSquareEl = sqEl;
sqEl.classList.add('selected'); sqEl.classList.add('selected');
// hide the square selection outline while keeping the piece
sqEl.classList.add('hide-selection');
const fromEl = document.getElementById('from'); const fromEl = document.getElementById('from');
if(fromEl) fromEl.value = squareName; if(fromEl) fromEl.value = squareName;
// notify others of our selection // notify others of our selection
if(emit && socket && myPlayerId){ if(emit && socket && myPlayerId){
socket.emit('game:select', { roomId, square: squareName }, ()=>{}); 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 // remote selection helpers: mark a square as selected by another player
@ -169,6 +247,8 @@
if(!sqEl) return; if(!sqEl) return;
remoteSelections[playerId] = squareName; remoteSelections[playerId] = squareName;
sqEl.classList.add('selected-remote'); sqEl.classList.add('selected-remote');
// hide the square selection outline for remote selection as well
sqEl.classList.add('hide-selection');
sqEl.dataset.selectedBy = playerId; sqEl.dataset.selectedBy = playerId;
} }
@ -179,8 +259,12 @@
if(prevEl){ if(prevEl){
prevEl.classList.remove('selected-remote'); prevEl.classList.remove('selected-remote');
delete prevEl.dataset.selectedBy; delete prevEl.dataset.selectedBy;
prevEl.classList.remove('hide-selection');
} }
delete remoteSelections[playerId]; delete remoteSelections[playerId];
// also clear remote move markers
clearRemoteMoveMarkers(playerId);
delete remoteMoves[playerId];
} }
if(boardEl){ if(boardEl){
@ -236,7 +320,11 @@
// re-apply remote selections // re-apply remote selections
Object.entries(remoteSelections).forEach(([pid, sq]) => { Object.entries(remoteSelections).forEach(([pid, sq]) => {
const el = boardEl.querySelector(`.square[data-square="${sq}"]`); const el = boardEl.querySelector(`.square[data-square="${sq}"]`);
if(el) el.classList.add('selected-remote'); 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);
}); });
} }

View file

@ -49,7 +49,12 @@ a:hover{text-decoration:underline}
/* marker shown on target squares for legal moves */ /* marker shown on target squares for legal moves */
.square{position:relative} .square{position:relative}
/* move markers removed */ .square .move-marker{
position:absolute;
left:50%;top:50%;transform:translate(-50%,-50%);
width:18%;height:18%;border-radius:50%;pointer-events:none;background:rgba(96,165,250,0.95);
box-shadow:0 2px 6px rgba(2,6,23,0.6);z-index:3;
}
/* highlighted/selected square when a player selects a piece */ /* highlighted/selected square when a player selects a piece */
.square.selected{ .square.selected{
@ -59,6 +64,14 @@ a:hover{text-decoration:underline}
} }
.square.selected .piece{transform: scale(1.06); transition: transform 140ms ease} .square.selected .piece{transform: scale(1.06); transition: transform 140ms ease}
/* hide the visual selection outline for a square while keeping the piece visible */
.square.hide-selection.selected,
.square.hide-selection.selected-remote{
outline: none !important;
box-shadow: none !important;
}
.square.hide-selection{transition:background 120ms ease}
/* remote selection (other player's selection) */ /* remote selection (other player's selection) */
.square.selected-remote{ .square.selected-remote{
outline: 3px solid rgba(59,130,246,0.95); /* blue */ outline: 3px solid rgba(59,130,246,0.95); /* blue */

143
server.js
View file

@ -21,8 +21,123 @@ const rooms = new Map();
// `boardState` is the canonical JSON representation of the board for this server. // `boardState` is the canonical JSON representation of the board for this server.
// Example boardState: { width:8, height:8, turn:'w', version:1, pieces: [{ square:'e2', type:'P', color:'w', id:'w_p1' }, ... ] } // Example boardState: { width:8, height:8, turn:'w', version:1, pieces: [{ square:'e2', type:'P', color:'w', id:'w_p1' }, ... ] }
function computeLegalMoves(room, square){ function computeLegalMoves(room, square){
// TODO: implement custom rules engine here. For now return empty list. // Basic pseudo-legal move generator for standard chess-like pieces.
return []; // - Does not perform check detection (moves that leave king in check are allowed).
// - Does not implement castling or en-passant.
// - Returns moves as array of { from, to }.
if(!room || !room.boardState || !square) return [];
const state = room.boardState;
const width = state.width || 8;
const height = state.height || 8;
// helpers
function squareToCoord(sq){
if(!/^[a-z][1-9][0-9]*$/i.test(sq)) return null;
const file = sq.charCodeAt(0) - 'a'.charCodeAt(0);
const rank = parseInt(sq.slice(1),10) - 1; // 0-indexed
return { x: file, y: rank };
}
function coordToSquare(x,y){
if(x < 0 || y < 0 || x >= width || y >= height) return null;
return String.fromCharCode('a'.charCodeAt(0) + x) + (y+1);
}
function getPieceAt(sq){
if(!sq) return null;
return (state.pieces || []).find(p => p.square === sq) || null;
}
function isInside(x,y){ return x >= 0 && y >= 0 && x < width && y < height; }
const piece = getPieceAt(square);
if(!piece) return [];
const color = piece.color; // 'w' or 'b'
const fromCoord = squareToCoord(square);
if(!fromCoord) return [];
const moves = [];
// add move helper
function pushIfEmptyOrCapture(tx,ty){
if(!isInside(tx,ty)) return false;
const toSq = coordToSquare(tx,ty);
const occupant = getPieceAt(toSq);
if(!occupant){
moves.push({ from: square, to: toSq });
return true; // can continue sliding
}
// capture allowed if different color
if(occupant.color !== color){
moves.push({ from: square, to: toSq });
}
return false; // blocked
}
const x = fromCoord.x, y = fromCoord.y;
const type = (piece.type || '').toUpperCase();
if(type === 'P'){
// pawn
const forward = (color === 'w') ? 1 : -1;
const startRank = (color === 'w') ? 1 : (height - 2);
const oneY = y + forward;
const oneSq = coordToSquare(x, oneY);
if(isInside(x, oneY) && !getPieceAt(oneSq)){
moves.push({ from: square, to: oneSq });
// double push from starting rank
const twoY = y + forward*2;
const twoSq = coordToSquare(x, twoY);
if(y === startRank && isInside(x, twoY) && !getPieceAt(twoSq)){
moves.push({ from: square, to: twoSq });
}
}
// captures
[[x-1, y+forward],[x+1, y+forward]].forEach(([tx,ty])=>{
if(!isInside(tx,ty)) return;
const tsq = coordToSquare(tx,ty);
const occ = getPieceAt(tsq);
if(occ && occ.color !== color) moves.push({ from: square, to: tsq });
});
return moves;
}
if(type === 'N'){
const deltas = [[1,2],[2,1],[-1,2],[-2,1],[1,-2],[2,-1],[-1,-2],[-2,-1]];
deltas.forEach(([dx,dy])=>{
const tx = x + dx, ty = y + dy;
if(!isInside(tx,ty)) return;
const tsq = coordToSquare(tx,ty);
const occ = getPieceAt(tsq);
if(!occ || occ.color !== color) moves.push({ from: square, to: tsq });
});
return moves;
}
if(type === 'B' || type === 'Q' || type === 'R'){
const directions = [];
if(type === 'B' || type === 'Q') directions.push([1,1],[1,-1],[-1,1],[-1,-1]);
if(type === 'R' || type === 'Q') directions.push([1,0],[-1,0],[0,1],[0,-1]);
directions.forEach(([dx,dy])=>{
let tx = x + dx, ty = y + dy;
while(isInside(tx,ty)){
const cont = pushIfEmptyOrCapture(tx,ty);
if(!cont) break;
tx += dx; ty += dy;
}
});
return moves;
}
if(type === 'K'){
for(let dx=-1; dx<=1; dx++) for(let dy=-1; dy<=1; dy++){
if(dx === 0 && dy === 0) continue;
const tx = x + dx, ty = y + dy;
if(!isInside(tx,ty)) continue;
const tsq = coordToSquare(tx,ty);
const occ = getPieceAt(tsq);
if(!occ || occ.color !== color) moves.push({ from: square, to: tsq });
}
return moves;
}
return moves;
} }
function startingBoardState8(){ function startingBoardState8(){
@ -201,14 +316,34 @@ io.on('connection', (socket) => {
// delegates to `computeLegalMoves` which is a stub you should implement. // delegates to `computeLegalMoves` which is a stub you should implement.
// legalMoves API removed (movement UI disabled) // legalMoves API removed (movement UI disabled)
// Re-add legalMoves API to compute and return pseudo-legal moves for a square.
socket.on('game:legalMoves', ({ roomId, square }, cb) => {
const room = rooms.get(roomId);
if (!room) return cb && cb({ error: 'room not found' });
try{
const moves = computeLegalMoves(room, square) || [];
return cb && cb({ ok: true, moves });
}catch(e){
console.error('game:legalMoves error', e);
return cb && cb({ error: 'invalid square', moves: [] });
}
});
// propagate selection made by one client to the other clients in the same room // propagate selection made by one client to the other clients in the same room
socket.on('game:select', ({ roomId, square }, cb) => { socket.on('game:select', ({ roomId, square }, cb) => {
const room = rooms.get(roomId); const room = rooms.get(roomId);
if(!room) return cb && cb({ error: 'room not found' }); if(!room) return cb && cb({ error: 'room not found' });
const playerId = socket.data.playerId || null; const playerId = socket.data.playerId || null;
// broadcast to other sockets in the room (exclude sender) // compute legal moves for the selected square (allow capturing king)
socket.to(roomId).emit('game:select', { playerId, square }); let moves = [];
try{
if(square) moves = computeLegalMoves(room, square) || [];
}catch(e){
console.error('computeLegalMoves error', e);
moves = [];
}
// broadcast selection + moves to ALL clients in the room (including sender)
io.to(roomId).emit('game:select', { playerId, square, moves });
cb && cb({ ok: true }); cb && cb({ ok: true });
}); });