diff --git a/public/game.html b/public/game.html index 1c558a1..a2a0401 100644 --- a/public/game.html +++ b/public/game.html @@ -4,6 +4,18 @@ ChessNut - Partie + @@ -71,6 +83,7 @@ // current boardState cached locally let currentBoardState = null; let myMines = []; + let currentVeiledSquares = []; let pendingCard = null; // when set, next square click is used as card target // safe logger: prefer on-page log if present, otherwise console @@ -166,6 +179,8 @@ boardEl.style.width = side + 'px'; boardEl.style.height = side + 'px'; } + // store veiled squares list for brouillard effect (if provided by server) + try{ currentVeiledSquares = Array.isArray(data.veiledSquares) ? data.veiledSquares : (data.boardState && data.boardState.veiledSquares) || []; }catch(_){ currentVeiledSquares = []; } if(data.boardState) { renderBoardFromState(data.boardState); updateTurnBanner(data.boardState); @@ -529,6 +544,11 @@ if(!to) return; const target = boardEl.querySelector(`.square[data-square="${to}"]`); if(!target) return; + // do not show legal-move markers on squares that are veiled/hidden + try{ + if(Array.isArray(currentVeiledSquares) && currentVeiledSquares.indexOf(to) !== -1) return; + if(target.classList && target.classList.contains('veiled')) return; + }catch(_){ } const marker = document.createElement('div'); marker.className = 'move-marker'; // store metadata if needed later @@ -546,6 +566,11 @@ if(!to) return; const target = boardEl.querySelector(`.square[data-square="${to}"]`); if(!target) return; + // do not show remote move markers on veiled/hidden squares + try{ + if(Array.isArray(currentVeiledSquares) && currentVeiledSquares.indexOf(to) !== -1) return; + if(target.classList && target.classList.contains('veiled')) return; + }catch(_){ } const marker = document.createElement('div'); marker.className = 'move-marker'; marker.dataset.from = m.from || ''; @@ -591,10 +616,21 @@ 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; + const sqEl = ev.target.closest && ev.target.closest('.square'); + if(!sqEl) return; + const square = sqEl.getAttribute('data-square'); + if(!square) return; + + // Do not allow interacting with veiled/hidden squares. + // `currentVeiledSquares` is populated from server `room:update` and + // we also add a `.veiled` class during rendering as a fallback. + try{ + if(Array.isArray(currentVeiledSquares) && currentVeiledSquares.indexOf(square) !== -1){ + log('clicked veiled square, ignoring'); + return; + } + if(sqEl.classList && sqEl.classList.contains('veiled')){ log('clicked veiled square (DOM class), ignoring'); return; } + }catch(_){ } // If we're in a pending card-targeting mode, handle target selection and do NOT attempt moves if(pendingCard){ @@ -783,6 +819,31 @@ }); // render any mines owned by this player try{ renderMines(); }catch(e){ } + // render veils for brouillard effect (if any) + try{ + // remove existing veils + const allSq = boardEl.querySelectorAll('.square'); + allSq.forEach(sqEl => { + const v = sqEl.querySelector('.veil'); if(v) v.remove(); + // also remove prior veiled marker class + try{ sqEl.classList.remove('veiled'); }catch(_){ } + }); + if(Array.isArray(currentVeiledSquares) && currentVeiledSquares.length){ + currentVeiledSquares.forEach(sq => { + const el = boardEl.querySelector(`.square[data-square="${sq}"]`); + if(!el) return; + const veil = document.createElement('div'); veil.className = 'veil'; + el.appendChild(veil); + // add a class to hide piece images as a fallback in case z-index/stacking causes overlay issues + try{ + el.classList.add('veiled'); + // remove any move markers that may have been added previously on this square + const prevMarkers = el.querySelectorAll('.move-marker'); + prevMarkers.forEach(m => m.remove()); + }catch(_){ } + }); + } + }catch(_){ } } // draw mine icons on squares for mines owned by this client diff --git a/public/styles/style.css b/public/styles/style.css index 882439d..f499c78 100644 --- a/public/styles/style.css +++ b/public/styles/style.css @@ -92,6 +92,11 @@ a:hover{text-decoration:underline} pointer-events:none; z-index:2; image-rendering: -webkit-optimize-contrast; } +/* When a square is veiled by brouillard, hide its piece images to ensure full concealment + This is a robust fallback in case stacking context / transform interactions make an overlay + not fully cover the image. Hiding the image guarantees the piece is not visible. */ +.square.veiled img, .square.veiled .piece{ visibility: hidden !important; opacity: 0 !important; } + /* highlighted/selected square when a player selects a piece */ .square.selected{ outline: 3px solid rgba(245,158,11,0.95); /* amber */ diff --git a/server.js b/server.js index 3109ecc..2d7d5d4 100644 --- a/server.js +++ b/server.js @@ -77,9 +77,36 @@ function sendRoomUpdate(room){ payload.boardState = base.boardState || null; } }catch(_){ payload.boardState = base.boardState || null; } - // attach visible squares for this recipient (fog of war) + // attach visible squares for this recipient (fog of war) try{ - payload.visibleSquares = Array.from(visibleSquaresForPlayer(room, p.id) || []); + payload.visibleSquares = Array.from(visibleSquaresForPlayer(room, p.id) || []); + // attach veiled squares if a brouillard effect targets this recipient + try{ + const brouillards = (room.activeCardEffects || []).filter(e => e && e.type === 'brouillard'); + if(brouillards && brouillards.length){ + // If any brouillard is active in the room, veil the board for ALL players, + // but exclude the recipient's visible squares (their pieces + adjacent squares). + const state = room.boardState || {}; + const width = state.width || 8; + const height = state.height || 8; + const all = []; + for(let yy = 0; yy < height; yy++){ + for(let xx = 0; xx < width; xx++){ + all.push(String.fromCharCode('a'.charCodeAt(0) + xx) + (yy+1)); + } + } + // compute the set of squares that the recipient can see (their pieces + adjacent squares) + let visibleSet = new Set(); + try{ + const vs = visibleSquaresForPlayer(room, p.id) || new Set(); + visibleSet = new Set(Array.from(vs)); + }catch(_){ visibleSet = new Set(); } + // veiled squares are all minus visibleSet (so own pieces and their neighbors remain visible) + payload.veiledSquares = all.filter(sq => !visibleSet.has(sq)); + } else { + payload.veiledSquares = []; + } + }catch(_){ payload.veiledSquares = []; } // attach any mines owned by this recipient (mines are hidden from other players) try{ const mines = (room.activeCardEffects || []).filter(e => e.type === 'mine' && e.playerId === p.id).map(e => e.square); @@ -225,7 +252,7 @@ function buildDefaultDeck(){ ['folie','La pièce sélectionnée peut maintenant faire les déplacements du fou en plus'], ['fortification','La pièce sélectionnée peut maintenant faire les déplacements de la tour en plus'], ["l'anneau","Le plateau devient un anneau pendant un tour"], - // ['brouillard de guerre','Les joueur ne peuvent voir que au alentour de leurs pièces pendant 4 tours'], + ['brouillard de guerre','Les joueur ne peuvent voir que au alentour de leurs pièces pendant 4 tours'], // ['changer la pièce à capturer','Le joueur choisie la nouvelle pièce jouant le rôle de roi sans la révéler'], // ['trou de ver','Deux cases du plateau deviennent maintenant la même'], ['jouer deux fois','Le joueur peut déplacer deux pièces'], @@ -471,17 +498,7 @@ function computeLegalMoves(room, square){ return null; } - // filter moves according to brouillard (fog of war) if it is active for the owner of this piece - function filterMovesByFog(moves){ - try{ - const owner = ownerPlayer; - if(!owner) return moves; - const hasBrouillard = (room.activeCardEffects || []).some(e => e.type === 'brouillard' && e.playerId === owner.id); - if(!hasBrouillard) return moves; - const vis = visibleSquaresForPlayer(room, owner.id); - return (moves || []).filter(m => vis.has(m.to)); - }catch(e){ return moves; } - } + // brouillard (fog of war) removed — moves are not filtered by fog // helper to add diagonal sliding moves when a piece has been "folié" function addFolieMoves(){ @@ -571,7 +588,7 @@ function computeLegalMoves(room, square){ if(occ && occ.color !== color) moves.push({ from: square, to: tsq }); }); addAdoubementMoves(); addFolieMoves(); addFortificationMoves(); - return filterMovesByFog(moves); + return moves; } if(type === 'N'){ @@ -584,7 +601,7 @@ function computeLegalMoves(room, square){ if(!occ || occ.color !== color) moves.push({ from: square, to: tsq }); }); addAdoubementMoves(); addFolieMoves(); addFortificationMoves(); - return filterMovesByFog(moves); + return moves; } if(type === 'B' || type === 'Q' || type === 'R'){ @@ -658,7 +675,7 @@ function computeLegalMoves(room, square){ } }); addAdoubementMoves(); addFolieMoves(); addFortificationMoves(); - return filterMovesByFog(moves); + return moves; } if(type === 'K'){ @@ -671,11 +688,11 @@ function computeLegalMoves(room, square){ if(!occ || occ.color !== color) moves.push({ from: square, to: tsq }); } addAdoubementMoves(); addFolieMoves(); - return filterMovesByFog(moves); + return moves; } addAdoubementMoves(); addFolieMoves(); addFortificationMoves(); - return filterMovesByFog(moves); + return moves; } function startingBoardState8(){ @@ -894,14 +911,16 @@ io.on('connection', (socket) => { const legal = computeLegalMoves(room, from) || []; const ok = legal.some(m => m.to === to); if(!ok) return cb && cb({ error: 'illegal move' }); - // additionally, do not allow moving into a fogged square if brouillard is active for this player + // If any brouillard is active in the room, disallow moving onto squares that the player cannot see try{ - const hasBrouillardForPlayer = (room.activeCardEffects || []).some(e => e.type === 'brouillard' && e.playerId === senderId); - if(hasBrouillardForPlayer){ - const vis = visibleSquaresForPlayer(room, senderId); - if(!vis.has(to)) return cb && cb({ error: 'destination not visible (fog of war)' }); + const hasBrouillard = (room.activeCardEffects || []).some(e => e && e.type === 'brouillard'); + if(hasBrouillard){ + const visible = visibleSquaresForPlayer(room, senderId) || new Set(); + if(!visible.has(to)){ + return cb && cb({ error: 'destination_not_visible' }); + } } - }catch(e){ /* ignore */ } + }catch(_){ } // apply move: remove any piece on target (capture) const targetIndex = pieces.findIndex(p => p.square === to); @@ -1047,6 +1066,31 @@ io.on('connection', (socket) => { // advance board version board.version = (board.version || 0) + 1; + // Update brouillard play counters: if any brouillard is active, increment the move count for the moving player. + try{ + room.activeCardEffects = room.activeCardEffects || []; + for(let ei = room.activeCardEffects.length - 1; ei >= 0; ei--){ + const ev = room.activeCardEffects[ei]; + if(!ev || ev.type !== 'brouillard') continue; + try{ + ev.playCounts = ev.playCounts || {}; + ev.playCounts[senderId] = (ev.playCounts[senderId] || 0) + 1; + try{ io.to(room.id).emit('card:effect:updated', { roomId: room.id, effect: ev }); }catch(_){ } + // expire the brouillard when ALL players have reached the threshold of plays + const threshold = 2; + const players = room.players || []; + let allReached = true; + for(const pl of players){ if(!pl || !pl.id) continue; if((ev.playCounts[pl.id] || 0) < threshold){ allReached = false; break; } } + if(allReached){ + try{ room.activeCardEffects.splice(ei,1); }catch(_){ } + try{ io.to(room.id).emit('card:effect:removed', { roomId: room.id, effectId: ev.id, type: ev.type, playerId: ev.playerId }); }catch(_){ } + // broadcast updated room state so clients remove veils + try{ sendRoomUpdate(room); }catch(_){ } + } + }catch(_){ } + } + }catch(e){ console.error('brouillard playcount update error', e); } + // Check victory (king capture) before further turn bookkeeping. If game ended, broadcast move and game:over and return. try{ const end = checkAndHandleVictory(room); @@ -2166,29 +2210,40 @@ io.on('connection', (socket) => { played.payload = Object.assign({}, payload, { applied: 'invisible', appliedTo: target }); }catch(e){ console.error('invisible effect error', e); } } - // brouillard de guerre: target a player so their board is fogged (they only see adjacent squares) - else if(cardId === 'brouillard_de_guerre' || (typeof cardId === 'string' && cardId.indexOf('brouillard') !== -1)){ - try{ - const board = room.boardState; - // determine target player id: payload.targetPlayerId or the opponent - let targetPlayerId = payload && payload.targetPlayerId; - if(!targetPlayerId){ - const opp = (room.players || []).find(p => p.id !== senderId); - targetPlayerId = opp && opp.id; - } - const targetPlayer = (room.players || []).find(p => p.id === targetPlayerId); - if(!board || !targetPlayer || targetPlayer.id === senderId){ - // restore removed card to hand and abort - try{ room.hands = room.hands || {}; room.hands[senderId] = room.hands[senderId] || []; if(removed) room.hands[senderId].push(removed); room.discard = room.discard || []; for(let i = room.discard.length-1;i>=0;i--){ if(room.discard[i] && room.discard[i].id === (removed && removed.id)){ room.discard.splice(i,1); break; } } }catch(e){} - return cb && cb({ error: 'no valid target player' }); - } - // record brouillard effect for target player - room.activeCardEffects = room.activeCardEffects || []; - const effect = { id: played.id, type: 'brouillard', playerId: targetPlayer.id, ts: Date.now(), remainingTurns: (payload && payload.turns) || 4 }; - room.activeCardEffects.push(effect); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){} - played.payload = Object.assign({}, payload, { applied: 'brouillard', appliedToPlayer: targetPlayer.id }); - }catch(e){ console.error('brouillard effect error', e); } + else if(cardId === 'brouillard_de_guerre' || (typeof cardId === 'string' && cardId.indexOf('brouillard') !== -1)){ + try{ + const board = room.boardState; + // determine target player id: payload.targetPlayerId or the opponent + let targetPlayerId = payload && payload.targetPlayerId; + if(!targetPlayerId){ + const opp = (room.players || []).find(p => p.id !== senderId); + targetPlayerId = opp && opp.id; + } + const targetPlayer = (room.players || []).find(p => p.id === targetPlayerId); + if(!board || !targetPlayer || targetPlayer.id === senderId){ + // restore removed card to hand and abort + try{ room.hands = room.hands || {}; room.hands[senderId] = room.hands[senderId] || []; if(removed) room.hands[senderId].push(removed); room.discard = room.discard || []; for(let i = room.discard.length-1;i>=0;i--){ if(room.discard[i] && room.discard[i].id === (removed && removed.id)){ room.discard.splice(i,1); break; } } }catch(e){} + return cb && cb({ error: 'no valid target player' }); + } + // compute list of all squares (each square veiled individually) + const width = board.width || 8; + const height = board.height || 8; + const all = []; + for(let yy = 0; yy < height; yy++){ + for(let xx = 0; xx < width; xx++){ + all.push(String.fromCharCode('a'.charCodeAt(0) + xx) + (yy+1)); + } + } + // record brouillard effect for target player with veiledSquares + room.activeCardEffects = room.activeCardEffects || []; + // initialize playCounts so we can expire brouillard after each player played N times + const playCounts = {}; + try{ (room.players || []).forEach(pl => { if(pl && pl.id) playCounts[pl.id] = 0; }); }catch(_){ } + const effect = { id: played.id, type: 'brouillard', playerId: targetPlayer.id, ts: Date.now(), remainingTurns: (payload && payload.turns) || 4, veiledSquares: all, playCounts }; + room.activeCardEffects.push(effect); + try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } + played.payload = Object.assign({}, payload, { applied: 'brouillard', appliedToPlayer: targetPlayer.id }); + }catch(e){ console.error('brouillard effect error', e); } } // anneau: make the board horizontally wrap for the playing player's pieces for this turn else if(cardId === 'anneau' || (typeof cardId === 'string' && cardId.indexOf('anneau') !== -1)){