From f2674b08744afdb7e40fdd1f3c6d9b10f927e066 Mon Sep 17 00:00:00 2001 From: Didictateur Date: Sat, 22 Nov 2025 15:25:25 +0100 Subject: [PATCH] affichage des mouvements --- public/game.html | 106 ++++++++++++++++++++++++++--- public/styles/style.css | 15 ++++- server.js | 143 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 250 insertions(+), 14 deletions(-) diff --git a/public/game.html b/public/game.html index b95565a..7b11c77 100644 --- a/public/game.html +++ b/public/game.html @@ -47,6 +47,8 @@ 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){ @@ -89,20 +91,42 @@ }); 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 broadcasted by other clients + // selection events (include moves) broadcasted by server socket.on('game:select', (data) => { try{ const pid = data && data.playerId; const square = data && data.square; - // ignore our own (some transports might echo) - if(pid === myPlayerId) return; - if(!pid) return; + const moves = data && data.moves; + log('recv game:select', { pid, square, moves: Array.isArray(moves) ? moves.length : moves }); + // cleared selection if(!square){ - // clear remote selection for that player - clearRemoteSelection(pid); + if(pid === myPlayerId){ + // our selection cleared (maybe by reconnection) - clear locally without re-emitting + clearSelection(false); + } else { + if(pid){ + clearRemoteSelection(pid); + } + } 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); } }); @@ -134,6 +158,7 @@ function clearSelection(emit = true){ if(selectedSquareEl){ selectedSquareEl.classList.remove('selected'); + selectedSquareEl.classList.remove('hide-selection'); } selectedSquare = null; selectedSquareEl = null; @@ -143,22 +168,75 @@ 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'); + 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 @@ -169,6 +247,8 @@ 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; } @@ -179,8 +259,12 @@ 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){ @@ -236,7 +320,11 @@ // 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'); + 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); }); } diff --git a/public/styles/style.css b/public/styles/style.css index 4616fde..de01a27 100644 --- a/public/styles/style.css +++ b/public/styles/style.css @@ -49,7 +49,12 @@ a:hover{text-decoration:underline} /* marker shown on target squares for legal moves */ .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 */ .square.selected{ @@ -59,6 +64,14 @@ a:hover{text-decoration:underline} } .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) */ .square.selected-remote{ outline: 3px solid rgba(59,130,246,0.95); /* blue */ diff --git a/server.js b/server.js index 9998197..9d5f794 100644 --- a/server.js +++ b/server.js @@ -21,8 +21,123 @@ const rooms = new Map(); // `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' }, ... ] } function computeLegalMoves(room, square){ - // TODO: implement custom rules engine here. For now return empty list. - return []; + // Basic pseudo-legal move generator for standard chess-like pieces. + // - 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(){ @@ -201,14 +316,34 @@ io.on('connection', (socket) => { // delegates to `computeLegalMoves` which is a stub you should implement. // 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 socket.on('game:select', ({ roomId, square }, cb) => { const room = rooms.get(roomId); if(!room) return cb && cb({ error: 'room not found' }); const playerId = socket.data.playerId || null; - // broadcast to other sockets in the room (exclude sender) - socket.to(roomId).emit('game:select', { playerId, square }); + // compute legal moves for the selected square (allow capturing king) + 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 }); });