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 });
});