affichage des mouvements
This commit is contained in:
parent
580775e6b1
commit
f2674b0874
3 changed files with 250 additions and 14 deletions
106
public/game.html
106
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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
143
server.js
143
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 });
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue