From 04311503234fb93fd8746d20b0605c3ba063ebc2 Mon Sep 17 00:00:00 2001 From: Didictateur Date: Tue, 2 Dec 2025 21:32:50 +0100 Subject: [PATCH] prettier for java --- package-lock.json | 19 + package.json | 10 +- public/toast.js | 44 +- server.js | 5660 ++++++++++++++++++++++++++++++++------------- 4 files changed, 4097 insertions(+), 1636 deletions(-) diff --git a/package-lock.json b/package-lock.json index f1be53e..9202a95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,9 @@ "express": "^4.18.2", "socket.io": "^4.7.2", "uuid": "^9.0.0" + }, + "devDependencies": { + "prettier": "3.7.3" } }, "node_modules/@socket.io/component-emitter": { @@ -679,6 +682,22 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/prettier": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.3.tgz", + "integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/package.json b/package.json index 844fbaf..cad29b8 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,20 @@ "start": "node server.js", "dev": "NODE_ENV=development node server.js" }, - "keywords": ["chess","socket.io","rooms","docker"], + "keywords": [ + "chess", + "socket.io", + "rooms", + "docker" + ], "author": "Didictateur", "license": "MIT", "dependencies": { "express": "^4.18.2", "socket.io": "^4.7.2", "uuid": "^9.0.0" + }, + "devDependencies": { + "prettier": "3.7.3" } } diff --git a/public/toast.js b/public/toast.js index d304c26..6751b5b 100644 --- a/public/toast.js +++ b/public/toast.js @@ -1,22 +1,32 @@ // Lightweight non-blocking toast used across pages -function showToast(message, opts){ - try{ +function showToast(message, opts) { + try { opts = opts || {}; - const wrap = document.createElement('div'); - wrap.className = 'cn-toast'; - wrap.textContent = message || ''; - wrap.style.position = 'fixed'; - wrap.style.left = '50%'; - wrap.style.transform = 'translateX(-50%)'; - wrap.style.top = opts.top || '18px'; - wrap.style.background = opts.background || 'rgba(0,0,0,0.8)'; - wrap.style.color = opts.color || '#fff'; - wrap.style.padding = '8px 12px'; - wrap.style.borderRadius = '6px'; + const wrap = document.createElement("div"); + wrap.className = "cn-toast"; + wrap.textContent = message || ""; + wrap.style.position = "fixed"; + wrap.style.left = "50%"; + wrap.style.transform = "translateX(-50%)"; + wrap.style.top = opts.top || "18px"; + wrap.style.background = opts.background || "rgba(0,0,0,0.8)"; + wrap.style.color = opts.color || "#fff"; + wrap.style.padding = "8px 12px"; + wrap.style.borderRadius = "6px"; wrap.style.zIndex = 99999; - wrap.style.fontSize = '14px'; - wrap.style.boxShadow = '0 6px 18px rgba(0,0,0,0.25)'; + wrap.style.fontSize = "14px"; + wrap.style.boxShadow = "0 6px 18px rgba(0,0,0,0.25)"; document.body.appendChild(wrap); - setTimeout(()=>{ try{ wrap.style.transition = 'opacity 300ms ease'; wrap.style.opacity = '0'; setTimeout(()=>{ try{ wrap.remove(); }catch(_){ } }, 320); }catch(_){ } }, opts.duration || 3500); - }catch(_){ } + setTimeout(() => { + try { + wrap.style.transition = "opacity 300ms ease"; + wrap.style.opacity = "0"; + setTimeout(() => { + try { + wrap.remove(); + } catch (_) {} + }, 320); + } catch (_) {} + }, opts.duration || 3500); + } catch (_) {} } diff --git a/server.js b/server.js index 40443e9..c0c914d 100644 --- a/server.js +++ b/server.js @@ -1,26 +1,34 @@ -const express = require('express'); -const http = require('http'); -const { Server } = require('socket.io'); -const { v4: uuidv4 } = require('uuid'); +const express = require("express"); +const http = require("http"); +const { Server } = require("socket.io"); +const { v4: uuidv4 } = require("uuid"); const app = express(); -const path = require('path'); +const path = require("path"); const server = http.createServer(app); -const io = new Server(server, { cors: { origin: '*' } }); +const io = new Server(server, { cors: { origin: "*" } }); app.use(express.json()); -app.use(express.static('public')); +app.use(express.static("public")); // Serve provided assets (piece sets, boards, etc.) under /assets -app.use('/assets', express.static(path.join(__dirname, 'assets'))); +app.use("/assets", express.static(path.join(__dirname, "assets"))); // Public API: list available cards (uses the same deck builder as the server) -app.get('/cards', (req, res) => { - try{ +app.get("/cards", (req, res) => { + try { const deck = buildDefaultDeck() || []; // Return a lightweight view (id, cardId, title, description) - const out = deck.map(c => ({ id: c.id, cardId: c.cardId, title: c.title, description: c.description })); + const out = deck.map((c) => ({ + id: c.id, + cardId: c.cardId, + title: c.title, + description: c.description, + })); res.json({ ok: true, cards: out }); - }catch(e){ console.error('GET /cards error', e); res.status(500).json({ error: 'server_error' }); } + } catch (e) { + console.error("GET /cards error", e); + res.status(500).json({ error: "server_error" }); + } }); // In-memory rooms store. For production replace with persistent store. @@ -28,11 +36,11 @@ app.get('/cards', (req, res) => { const rooms = new Map(); // Send a room:update payload to each player individually so hands remain private. -function sendRoomUpdate(room){ - if(!room) return; +function sendRoomUpdate(room) { + if (!room) return; const base = { boardState: room.boardState || null, - players: (room.players || []).map(p => ({ id: p.id, color: p.color })), + players: (room.players || []).map((p) => ({ id: p.id, color: p.color })), status: room.status, hostId: room.hostId, size: (room.boardState && room.boardState.width) || room.size, @@ -46,378 +54,485 @@ function sendRoomUpdate(room){ }; const handCounts = {}; - (room.players || []).forEach(p => { handCounts[p.id] = (room.hands && room.hands[p.id] ? room.hands[p.id].length : 0); }); + (room.players || []).forEach((p) => { + handCounts[p.id] = + room.hands && room.hands[p.id] ? room.hands[p.id].length : 0; + }); - (room.players || []).forEach(p => { + (room.players || []).forEach((p) => { const payload = Object.assign({}, base, { // expose only the recipient's own hand - handsOwn: (room.hands && room.hands[p.id]) ? room.hands[p.id] : [], - handCounts + handsOwn: room.hands && room.hands[p.id] ? room.hands[p.id] : [], + handCounts, }); - // Create a per-recipient boardState view that only hides pieces targeted by an 'invisible' effect - try{ - if(base.boardState && base.boardState.pieces && Array.isArray(base.boardState.pieces)){ + // Create a per-recipient boardState view that only hides pieces targeted by an 'invisible' effect + try { + if ( + base.boardState && + base.boardState.pieces && + Array.isArray(base.boardState.pieces) + ) { const filtered = Object.assign({}, base.boardState); - filtered.pieces = (base.boardState.pieces || []).filter(piece => { - try{ + filtered.pieces = (base.boardState.pieces || []).filter((piece) => { + try { // If a piece has an explicit invisible flag, hide it from recipients who are not the owner - if(piece.invisible){ - const ownerPlayer = (room.players || []).find(pl => (pl.color && pl.color[0]) === piece.color); + if (piece.invisible) { + const ownerPlayer = (room.players || []).find( + (pl) => (pl.color && pl.color[0]) === piece.color, + ); const ownerId = ownerPlayer && ownerPlayer.id; - if(ownerId && ownerId !== p.id) return false; + if (ownerId && ownerId !== p.id) return false; } // If an 'invisible' active effect targets this piece, hide it from recipients who are not the effect owner - const inv = (room.activeCardEffects || []).find(e => e && e.type === 'invisible' && (e.pieceId === piece.id || e.pieceSquare === piece.square)); - if(inv && inv.playerId && inv.playerId !== p.id){ + const inv = (room.activeCardEffects || []).find( + (e) => + e && + e.type === "invisible" && + (e.pieceId === piece.id || e.pieceSquare === piece.square), + ); + if (inv && inv.playerId && inv.playerId !== p.id) { return false; // hide this piece for this recipient } return true; - }catch(_){ return true; } + } catch (_) { + return true; + } }); payload.boardState = filtered; // If a 'tous_memes' effect is active for another player, mask pieces as kings for this recipient - try{ + try { const effects = room.activeCardEffects || []; - const tous = effects.find(e => e && e.type === 'tous_memes' && e.playerId && e.playerId !== p.id); - if(tous && payload.boardState && Array.isArray(payload.boardState.pieces)){ + const tous = effects.find( + (e) => + e && e.type === "tous_memes" && e.playerId && e.playerId !== p.id, + ); + if ( + tous && + payload.boardState && + Array.isArray(payload.boardState.pieces) + ) { const masked = JSON.parse(JSON.stringify(payload.boardState)); - masked.pieces = masked.pieces.map(pc => { const c = Object.assign({}, pc); c.type = 'K'; return c; }); + masked.pieces = masked.pieces.map((pc) => { + const c = Object.assign({}, pc); + c.type = "K"; + return c; + }); payload.boardState = masked; } - }catch(_){ } + } catch (_) {} } else { payload.boardState = base.boardState || null; } - }catch(_){ payload.boardState = base.boardState || null; } - // attach visible squares for this recipient (fog of war) - try{ - 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)); - } + } catch (_) { + payload.boardState = base.boardState || null; + } + // attach visible squares for this recipient (fog of war) + try { + 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 = []; } + // 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); + try { + const mines = (room.activeCardEffects || []) + .filter((e) => e.type === "mine" && e.playerId === p.id) + .map((e) => e.square); payload.minesOwn = mines; - }catch(_){ payload.minesOwn = []; } - }catch(e){ payload.visibleSquares = []; } + } catch (_) { + payload.minesOwn = []; + } + } catch (e) { + payload.visibleSquares = []; + } // attach captured pieces that belong to this recipient so they may choose one for resurrection - try{ + try { const shortColor = (p.color && p.color[0]) || null; - payload.capturedOwn = (room.captured || []).filter(c => c && c.piece && c.piece.color === shortColor).map(c => ({ id: c.id, type: c.piece.type, originalId: c.piece.id, originalSquare: c.piece.square, capturedBy: c.capturedBy, ts: c.ts })); - }catch(_){ payload.capturedOwn = []; } - if(p.socketId){ - io.to(p.socketId).emit('room:update', payload); + payload.capturedOwn = (room.captured || []) + .filter((c) => c && c.piece && c.piece.color === shortColor) + .map((c) => ({ + id: c.id, + type: c.piece.type, + originalId: c.piece.id, + originalSquare: c.piece.square, + capturedBy: c.capturedBy, + ts: c.ts, + })); + } catch (_) { + payload.capturedOwn = []; + } + if (p.socketId) { + io.to(p.socketId).emit("room:update", payload); } }); } // Helper to record whether a player drew during their turn and reset the current-turn marker. -function recordPlayerDrewPrev(room, playerId){ - try{ +function recordPlayerDrewPrev(room, playerId) { + try { room._playerDrewPrev = room._playerDrewPrev || {}; room._playerDrew = room._playerDrew || {}; room._playerDrewPrev[playerId] = !!room._playerDrew[playerId]; room._playerDrew[playerId] = false; - }catch(_){ } + } catch (_) {} } // Helper: at the start of a player's turn, either perform an automatic draw // if room.autoDraw is enabled, or simply broadcast the room state so clients // can update UI. This centralizes the auto-draw toggle behavior. -function maybeDrawAtTurnStart(room, playerId){ - try{ - if(!room) return; - if(room.autoDraw){ +function maybeDrawAtTurnStart(room, playerId) { + try { + if (!room) return; + if (room.autoDraw) { const drawn = drawCardForPlayer(room, playerId); // if nothing was drawn (deck empty, hand full, or noRemise), still push a room update // so clients get the freshest state and don't remain out-of-sync. - if(!drawn){ - try{ sendRoomUpdate(room); }catch(_){ } + if (!drawn) { + try { + sendRoomUpdate(room); + } catch (_) {} } } else { // ensure clients get the updated room state even if no draw happens sendRoomUpdate(room); } - }catch(e){ console.error('maybeDrawAtTurnStart error', e); sendRoomUpdate(room); } + } catch (e) { + console.error("maybeDrawAtTurnStart error", e); + sendRoomUpdate(room); + } } // Compute the set of squares visible to a given player (adjacent to any of their pieces). // Returns a Set of square strings (e.g., 'e4'). -function visibleSquaresForPlayer(room, playerId){ +function visibleSquaresForPlayer(room, playerId) { const out = new Set(); - if(!room || !room.boardState || !playerId) return out; + if (!room || !room.boardState || !playerId) return out; const state = room.boardState; const width = state.width || 8; const height = state.height || 8; - function squareToCoord(sq){ - if(!sq) return null; + function squareToCoord(sq) { + if (!sq) return null; const s = String(sq).trim().toLowerCase(); - if(!/^[a-z][1-9][0-9]*$/.test(s)) return null; - const file = s.charCodeAt(0) - 'a'.charCodeAt(0); - const rank = parseInt(s.slice(1),10) - 1; // 0-indexed + if (!/^[a-z][1-9][0-9]*$/.test(s)) return null; + const file = s.charCodeAt(0) - "a".charCodeAt(0); + const rank = parseInt(s.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 coordToSquare(x, y) { + if (x < 0 || y < 0 || x >= width || y >= height) return null; + return String.fromCharCode("a".charCodeAt(0) + x) + (y + 1); + } // find the player color for playerId - const player = (room.players || []).find(p => p.id === playerId); - if(!player) return out; + const player = (room.players || []).find((p) => p.id === playerId); + if (!player) return out; const colorShort = (player.color && player.color[0]) || null; // iterate pieces belonging to player - (state.pieces || []).forEach(piece => { - if(piece.color !== colorShort) return; + (state.pieces || []).forEach((piece) => { + if (piece.color !== colorShort) return; const c = squareToCoord(piece.square); - if(!c) return; - for(let dx=-1; dx<=1; dx++) for(let dy=-1; dy<=1; dy++){ - const nx = c.x + dx, ny = c.y + dy; - const sq = coordToSquare(nx, ny); - if(sq) out.add(sq); - } + if (!c) return; + for (let dx = -1; dx <= 1; dx++) + for (let dy = -1; dy <= 1; dy++) { + const nx = c.x + dx, + ny = c.y + dy; + const sq = coordToSquare(nx, ny); + if (sq) out.add(sq); + } }); return out; } // Helper to end a player's turn after playing a card: flip turn, decrement per-turn effects and trigger next player's draw -function endTurnAfterCard(room, senderId){ - try{ - if(!room || !room.boardState) return; +function endTurnAfterCard(room, senderId) { + try { + if (!room || !room.boardState) return; const board = room.boardState; // flip turn - if(board.turn) board.turn = (board.turn === 'w') ? 'b' : 'w'; + if (board.turn) board.turn = board.turn === "w" ? "b" : "w"; // decrement remainingTurns for any time-limited effects that belong to the player who just finished their turn - try{ + try { room.activeCardEffects = room.activeCardEffects || []; - for(let i = room.activeCardEffects.length - 1; i >= 0; i--){ + for (let i = room.activeCardEffects.length - 1; i >= 0; i--) { const e = room.activeCardEffects[i]; - if(typeof e.remainingTurns === 'number'){ + if (typeof e.remainingTurns === "number") { let shouldDecrement = false; - if(e.decrementOn === 'opponent'){ - shouldDecrement = (e.playerId !== senderId); - } else if(e.decrementOn === 'owner'){ - shouldDecrement = (e.playerId === senderId); + if (e.decrementOn === "opponent") { + shouldDecrement = e.playerId !== senderId; + } else if (e.decrementOn === "owner") { + shouldDecrement = e.playerId === senderId; } else { - shouldDecrement = (e.playerId === senderId); + shouldDecrement = e.playerId === senderId; } - if(shouldDecrement){ + if (shouldDecrement) { e.remainingTurns = e.remainingTurns - 1; - try{ io.to(room.id).emit('card:effect:updated', { roomId: room.id, effect: e }); }catch(_){ } - if(e.remainingTurns <= 0){ room.activeCardEffects.splice(i,1); try{ io.to(room.id).emit('card:effect:removed', { roomId: room.id, effectId: e.id, type: e.type, playerId: e.playerId }); }catch(_){ } } + try { + io.to(room.id).emit("card:effect:updated", { + roomId: room.id, + effect: e, + }); + } catch (_) {} + if (e.remainingTurns <= 0) { + room.activeCardEffects.splice(i, 1); + try { + io.to(room.id).emit("card:effect:removed", { + roomId: room.id, + effectId: e.id, + type: e.type, + playerId: e.playerId, + }); + } catch (_) {} + } } } } - }catch(e){ console.error('updating temporary effects error', e); } + } catch (e) { + console.error("updating temporary effects error", e); + } // reset per-turn card-play flags - try{ room._cardPlayedThisTurn = {}; }catch(_){ } + try { + room._cardPlayedThisTurn = {}; + } catch (_) {} // Shift the per-player draw marker: record whether the sender drew during their just-finished turn - try{ + try { room._playerDrewPrev = room._playerDrewPrev || {}; room._playerDrew = room._playerDrew || {}; room._playerDrewPrev[senderId] = !!room._playerDrew[senderId]; // clear the current-turn marker for the sender so it will be set anew on their next turn room._playerDrew[senderId] = false; - }catch(_){ } + } catch (_) {} // perform next player's draw (if autoDraw) or at least send room update - try{ + try { const nextColor = board.turn; - const nextPlayer = room.players.find(p => (p.color && p.color[0]) === nextColor); - if(nextPlayer){ maybeDrawAtTurnStart(room, nextPlayer.id); } else { sendRoomUpdate(room); } - }catch(_){ sendRoomUpdate(room); } - }catch(e){ console.error('endTurnAfterCard error', e); } + const nextPlayer = room.players.find( + (p) => (p.color && p.color[0]) === nextColor, + ); + if (nextPlayer) { + maybeDrawAtTurnStart(room, nextPlayer.id); + } else { + sendRoomUpdate(room); + } + } catch (_) { + sendRoomUpdate(room); + } + } catch (e) { + console.error("endTurnAfterCard error", e); + } } // Check if a king is missing and end the game. Returns an object describing the outcome or null if game continues. -function checkAndHandleVictory(room){ - try{ - if(!room || !room.boardState) return null; +function checkAndHandleVictory(room) { + try { + if (!room || !room.boardState) return null; // if the room is already finished, don't re-emit or change state - if(room.status === 'finished') return null; + if (room.status === "finished") return null; const pieces = room.boardState.pieces || []; - const hasWhiteKing = pieces.some(p => p && p.type && String(p.type).toUpperCase() === 'K' && p.color === 'w'); - const hasBlackKing = pieces.some(p => p && p.type && String(p.type).toUpperCase() === 'K' && p.color === 'b'); - if(hasWhiteKing && hasBlackKing) return null; + const hasWhiteKing = pieces.some( + (p) => + p && p.type && String(p.type).toUpperCase() === "K" && p.color === "w", + ); + const hasBlackKing = pieces.some( + (p) => + p && p.type && String(p.type).toUpperCase() === "K" && p.color === "b", + ); + if (hasWhiteKing && hasBlackKing) return null; // both missing -> draw - if(!hasWhiteKing && !hasBlackKing){ - room.status = 'finished'; - try{ io.to(room.id).emit('game:over', { roomId: room.id, draw: true, boardState: room.boardState }); }catch(_){ } + if (!hasWhiteKing && !hasBlackKing) { + room.status = "finished"; + try { + io.to(room.id).emit("game:over", { + roomId: room.id, + draw: true, + boardState: room.boardState, + }); + } catch (_) {} return { over: true, draw: true }; } - const winnerColor = hasWhiteKing ? 'w' : 'b'; - const loserColor = hasWhiteKing ? 'b' : 'w'; - const winner = (room.players || []).find(p => (p.color && p.color[0]) === winnerColor) || null; - const loser = (room.players || []).find(p => (p.color && p.color[0]) === loserColor) || null; - room.status = 'finished'; - try{ io.to(room.id).emit('game:over', { roomId: room.id, winnerId: winner && winner.id, loserId: loser && loser.id, winnerColor, boardState: room.boardState }); }catch(_){ } - return { over: true, winnerId: winner && winner.id, loserId: loser && loser.id, winnerColor }; - }catch(e){ console.error('checkAndHandleVictory error', e); return null; } + const winnerColor = hasWhiteKing ? "w" : "b"; + const loserColor = hasWhiteKing ? "b" : "w"; + const winner = + (room.players || []).find( + (p) => (p.color && p.color[0]) === winnerColor, + ) || null; + const loser = + (room.players || []).find( + (p) => (p.color && p.color[0]) === loserColor, + ) || null; + room.status = "finished"; + try { + io.to(room.id).emit("game:over", { + roomId: room.id, + winnerId: winner && winner.id, + loserId: loser && loser.id, + winnerColor, + boardState: room.boardState, + }); + } catch (_) {} + return { + over: true, + winnerId: winner && winner.id, + loserId: loser && loser.id, + winnerColor, + }; + } catch (e) { + console.error("checkAndHandleVictory error", e); + return null; + } } // Each card has a unique id, cardId (slug), title and description. -function buildDefaultDeck(){ +function buildDefaultDeck() { const cards = [ [ - 'Rebondir sur les bords', - 'Les déplacements en diagonales de la pièce sélectionnée peuvent rebondir une fois sur les bords', - 'rebond' + "Rebondir sur les bords", + "Les déplacements en diagonales de la pièce sélectionnée peuvent rebondir une fois sur les bords", + "rebond", ], [ - 'Adoubement', - 'La pièce sélectionnée peut maintenant faire les déplacements du cavalier en plus', - 'adoubement' + "Adoubement", + "La pièce sélectionnée peut maintenant faire les déplacements du cavalier en plus", + "adoubement", ], [ - 'Folie', - 'La pièce sélectionnée peut maintenant faire les déplacements du fou en plus', - 'folie' + "Folie", + "La pièce sélectionnée peut maintenant faire les déplacements du fou en plus", + "folie", ], [ - 'Fortification', - 'La pièce sélectionnée peut maintenant faire les déplacements de la tour en plus', - 'fortification' + "Fortification", + "La pièce sélectionnée peut maintenant faire les déplacements de la tour en plus", + "fortification", + ], + ["L'anneau", "Le plateau devient un anneau pendant un tour", "anneau"], + [ + "Brouillard de guerre", + "Les joueur ne peuvent voir que au alentour de leurs pièces pendant 4 tours", + "brouillard", ], [ - "L'anneau", - "Le plateau devient un anneau pendant un tour", - "anneau" - ], - [ - 'Brouillard de guerre', - 'Les joueur ne peuvent voir que au alentour de leurs pièces pendant 4 tours', - 'brouillard' - ], - [ - 'Jouer deux fois', - 'Le joueur peut déplacer deux pièces. Ne peut pas capturer pendant son deuxième tour', - 'double' + "Jouer deux fois", + "Le joueur peut déplacer deux pièces. Ne peut pas capturer pendant son deuxième tour", + "double", ], [ "Totem d'immunité", "Annule l'effet de la prochaine carte jouée par l'adversaire", - "totem" + "totem", ], [ - 'Placement de mines', - 'Le joueur place une mine sur une case vide sans la révéler au joueur adverse. Une pièce qui se pose dessus explose et est capturée par le joueur ayant placé la mine', - 'mine' + "Placement de mines", + "Le joueur place une mine sur une case vide sans la révéler au joueur adverse. Une pièce qui se pose dessus explose et est capturée par le joueur ayant placé la mine", + "mine", ], [ "Vole d'une pièce", - 'Désigne une pièce non roi qui change de camp.\n\nCompte comme un mouvement', - 'vole_piece' - ], - [ - 'Promotion', - 'Un pion au choix est promu', - 'promotion' + "Désigne une pièce non roi qui change de camp.\n\nCompte comme un mouvement", + "vole_piece", ], + ["Promotion", "Un pion au choix est promu", "promotion"], [ "Vole d'une carte", - 'Vole une carte aléatoirement au joueur adverse', - 'vole_carte' + "Vole une carte aléatoirement au joueur adverse", + "vole_carte", + ], + ["Resurection", "Ressucite la dernière pièce perdue", "resurection"], + ["Carte sans effet", "N'a aucun effet", "sans_effet"], + [ + "Kamikaze", + "Détruit une de ses pièces, détruisant toutes les pièces adjacentes.\n\nCompte comme un mouvement", + "kamikaze", ], [ - 'Resurection', - 'Ressucite la dernière pièce perdue', - 'resurection' - ], - [ - 'Carte sans effet', - "N'a aucun effet", - 'sans_effet' - ], - [ - 'Kamikaze', - 'Détruit une de ses pièces, détruisant toutes les pièces adjacentes.\n\nCompte comme un mouvement', - 'kamikaze' - ], - [ - 'Invisible', + "Invisible", "Une des pièces devient invisible pour l'adversaire", - 'invisible' + "invisible", ], [ "Coin-Coin", "Possibilité de se téléporter depuis un coin vers n'importe quel autre coin", - 'coincoin' + "coincoin", ], [ - 'Téléportation', + "Téléportation", "Téléporte n'importe quelle pièce de son camp sur une case vide", - 'teleportation' + "teleportation", ], [ "Toucher c'est jouer", "Toucher une pièce adverse qu'il sera obligé de jouer", - 'toucher' + "toucher", ], [ - 'Sniper', - 'Capturer une pièce sans avoir à bouger la pièce capturante', - 'sniper' + "Sniper", + "Capturer une pièce sans avoir à bouger la pièce capturante", + "sniper", ], [ - 'Échange', + "Échange", "Échange la position d'une pièce avec une pièce adverse.\n\nCompte comme un mouvement", - 'inversion' + "inversion", ], [ - 'Mélange', - 'La position de toutes les pièces sont échangées aléatoirement', - 'melange' + "Mélange", + "La position de toutes les pièces sont échangées aléatoirement", + "melange", + ], + ["La parrure", "Une reine est dégradée en pion", "parrure"], + [ + "Tout ou rien", + "Une pièce choisie ne peut maintenant se déplacer que si elle capture.", + "tout", ], [ - 'La parrure', - 'Une reine est dégradée en pion', - 'parrure' + "Tous les mêmes", + "Au yeux de l ennemie, toutes les pièces se ressemblent pendant 2 tours.", + "pareil", ], [ - 'Tout ou rien', - 'Une pièce choisie ne peut maintenant se déplacer que si elle capture.', - 'tout' - ], - [ - 'Tous les mêmes', - 'Au yeux de l ennemie, toutes les pièces se ressemblent pendant 2 tours.', - 'pareil' - ], - [ - 'Révolution', - 'Tous les pions sont aléatoirement changés en Cavalier, Fou ou Tour et les Cavaliers, Fous et Tours sont changés en pions.', - 'revolution' + "Révolution", + "Tous les pions sont aléatoirement changés en Cavalier, Fou ou Tour et les Cavaliers, Fous et Tours sont changés en pions.", + "revolution", ], [ "Doppelganger", "Choisis une pièce. À partir de maintenant, devient chacune des pièces qu'elle capture.", - 'doppelganger' + "doppelganger", ], - // facile et intéresssant + // facile et intéresssant // ["intrication quantique","Deux pièces sont intriquées. Quand l'une bouge, l'autre bouge de la même manière."], // ['kurby','Choisis une pièce. À sa prochaine capture, récupère tous les mouvements de la pièce capturée.'], // ["cachotier", "La prochaine carte jouée ne sera pas révélée à l'adversaire."], @@ -431,31 +546,27 @@ function buildDefaultDeck(){ // ['médusa','La pièce désignée ne peut plus bouger pendant 4 tours'], // ['pièce berserk','Une pièce choisie doit capturer dans les trois prochains tours, sinon elle est capturée'], - // moyen facile mais intéressant + // moyen facile mais intéressant // ['échange de main','Les deux joueurs échangent leurs mains'], // ["pièce fantôme","Choisis une pièce. Tant qu'elle ne capture pas, elle peut traverser les autres pièces comme si elles n'existaient pas."], // ['ça tangue','Toutes les pièces se décale du même côté'], // ['punching ball','Replace le roi dans sa position initiale, et place un nouveau pion à l ancienne position du roi'], // ['petit pion','Le joueur choisit un pion. À partir du prochain tour, il est promu en reine dès qu il capture un pièce non pion.'], - // difficile mais intéressant + // difficile mais intéressant // ['retour à la case départ','Désigne une pièce qui retourne à sa position initiale'], // ['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'], // ['glissade','La pièce désignée ne peut plus s arrêter si elle se déplace en diagonale ou en ligne droite. Soit elle percute une pièce et la capture, soit elle tombe du plateau et est capturée'], // ['réinitialisation','Toutes les pièces reviennent à leur position initiale. S il y a des pièces supplémenaires, se rangent devant les pions'], // ['jeu des 7 différences','Déplace une pièce du plateau pendant que le joueur adverse à les yeux fermés. S il la retrouve, elle est capturée, laissée sinon'], - - // trop désequilibrant ou peu intéressant + + // trop désequilibrant ou peu intéressant // ['épidémie','Toutes les pièces sur le territoire enemie est est capturée'], // ['vacances','Choisie une pièce qui sort du plateau pendant deux tours. Ce après quoi elle tente de revenir: si la case est occupée, alors la pièce vacancière est capturée par la pièce occupant la case.'], - // a reflechir - [ - 'Empathie', - 'On retourne le plateau', - 'empathie' - ], // pendant X tours ? + // a reflechir + ["Empathie", "On retourne le plateau", "empathie"], // pendant X tours ? // ['effet domino', "La pièce désigner peut rejouer tant qu'elle capture"] // ['reversi','Si deux pions encadrent parfaitement une pièce adverse, cette dernière change de camp'], // ['plus on est de fous','Si le joueur possède deux fous dans la même diagonale, alors toutes les pièces adverses encadrées par ces deux fous sont capturés'], @@ -463,165 +574,235 @@ function buildDefaultDeck(){ // ['tronquer le plateau','Tronque au maximum le plateau sans supprimer de pièce'], // ['agrandir le plateau','Rajoute une rangée dans toutes les directions'], ]; - function cap(s){ if(!s) return s; s = String(s).trim(); return s.charAt(0).toUpperCase() + s.slice(1); } - return cards.map(([title,desc,id])=>{ + function cap(s) { + if (!s) return s; + s = String(s).trim(); + return s.charAt(0).toUpperCase() + s.slice(1); + } + return cards.map(([title, desc, id]) => { const cardTitle = title; const cardDesc = desc; const cardId = id; const hidden = /mine|totem|invisible|immun|carte_sans_effet/i.test(cardId); - return { id: cardId, title: cardTitle, description: cardDesc, hidden: !!hidden }; + return { + id: cardId, + title: cardTitle, + description: cardDesc, + hidden: !!hidden, + }; }); } -function drawCardForPlayer(room, playerId){ - if(!room) return null; +function drawCardForPlayer(room, playerId) { + if (!room) return null; room.deck = room.deck || buildDefaultDeck(); room.hands = room.hands || {}; room.deck = room.deck || []; const hand = room.hands[playerId] || []; room._lastDrawForPlayer = room._lastDrawForPlayer || {}; const boardVersion = (room.boardState && room.boardState.version) || null; - if(boardVersion !== null && room._lastDrawForPlayer[playerId] === boardVersion){ + if ( + boardVersion !== null && + room._lastDrawForPlayer[playerId] === boardVersion + ) { return null; } - if(hand.length >= 5) return null; // hand full - if(room.deck.length === 0){ - if(!room.noRemise && room.discard && room.discard.length > 0){ + if (hand.length >= 5) return null; // hand full + if (room.deck.length === 0) { + if (!room.noRemise && room.discard && room.discard.length > 0) { room.deck = room.discard.splice(0).concat(room.deck || []); - for(let i = room.deck.length - 1; i > 0; i--){ + for (let i = room.deck.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); - const tmp = room.deck[i]; room.deck[i] = room.deck[j]; room.deck[j] = tmp; + const tmp = room.deck[i]; + room.deck[i] = room.deck[j]; + room.deck[j] = tmp; } - (room.players || []).forEach(p => { if(p.socketId) io.to(p.socketId).emit('deck:reshuffled', { roomId: room.id, deckCount: room.deck.length }); }); + (room.players || []).forEach((p) => { + if (p.socketId) + io.to(p.socketId).emit("deck:reshuffled", { + roomId: room.id, + deckCount: room.deck.length, + }); + }); } } - if(room.deck.length === 0) return null; - for(let i = room.deck.length - 1; i > 0; i--){ + if (room.deck.length === 0) return null; + for (let i = room.deck.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); - const tmp = room.deck[i]; room.deck[i] = room.deck[j]; room.deck[j] = tmp; + const tmp = room.deck[i]; + room.deck[i] = room.deck[j]; + room.deck[j] = tmp; } const idx = Math.floor(Math.random() * room.deck.length); - const card = room.deck.splice(idx,1)[0]; - if(card && card.title){ + const card = room.deck.splice(idx, 1)[0]; + if (card && card.title) { card.title = String(card.title).trim(); card.title = card.title.charAt(0).toUpperCase() + card.title.slice(1); } room.hands[playerId] = room.hands[playerId] || []; room.hands[playerId].push(card); - if(boardVersion !== null) room._lastDrawForPlayer[playerId] = boardVersion; - const recipient = (room.players || []).find(p => p.id === playerId); - if(recipient && recipient.socketId){ - io.to(recipient.socketId).emit('card:drawn', { playerId, card }); + if (boardVersion !== null) room._lastDrawForPlayer[playerId] = boardVersion; + const recipient = (room.players || []).find((p) => p.id === playerId); + if (recipient && recipient.socketId) { + io.to(recipient.socketId).emit("card:drawn", { playerId, card }); } - try{ if(room.autoDraw === false){ room._playerDrew = room._playerDrew || {}; room._playerDrew[playerId] = true; } }catch(_){ } + try { + if (room.autoDraw === false) { + room._playerDrew = room._playerDrew || {}; + room._playerDrew[playerId] = true; + } + } catch (_) {} sendRoomUpdate(room); return card; } -function computeLegalMoves(room, square){ - if(!room || !room.boardState || !square) return []; +function computeLegalMoves(room, square) { + if (!room || !room.boardState || !square) return []; const state = room.boardState; const width = state.width || 8; const height = state.height || 8; - function squareToCoord(sq){ - if(!sq) return null; + function squareToCoord(sq) { + if (!sq) return null; const s = String(sq).trim().toLowerCase(); - if(!/^[a-z][1-9][0-9]*$/.test(s)) return null; - const file = s.charCodeAt(0) - 'a'.charCodeAt(0); - const rank = parseInt(s.slice(1),10) - 1; + if (!/^[a-z][1-9][0-9]*$/.test(s)) return null; + const file = s.charCodeAt(0) - "a".charCodeAt(0); + const rank = parseInt(s.slice(1), 10) - 1; 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 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 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; } - function isInside(x,y){ return x >= 0 && y >= 0 && x < width && y < height; } const piece = getPieceAt(square); - if(!piece) return []; + if (!piece) return []; const color = piece.color; const fromCoord = squareToCoord(square); - if(!fromCoord) return []; + if (!fromCoord) return []; const moves = []; - const x = fromCoord.x, y = fromCoord.y; - const type = (piece.type || '').toUpperCase(); + const x = fromCoord.x, + y = fromCoord.y; + const type = (piece.type || "").toUpperCase(); // coin coin - try{ - const coin = (room.activeCardEffects || []).find(e => e && e.type === 'coincoin' && ((e.pieceId && e.pieceId === piece.id) || e.pieceSquare === square)); - if(coin && Array.isArray(coin.allowedSquares) && coin.allowedSquares.length > 0){ - coin.allowedSquares.forEach(sq => { - try{ + try { + const coin = (room.activeCardEffects || []).find( + (e) => + e && + e.type === "coincoin" && + ((e.pieceId && e.pieceId === piece.id) || e.pieceSquare === square), + ); + if ( + coin && + Array.isArray(coin.allowedSquares) && + coin.allowedSquares.length > 0 + ) { + coin.allowedSquares.forEach((sq) => { + try { // only add empty corner squares (do not allow capture via coincoin) - if(!moves.some(m => m.to === sq)){ + if (!moves.some((m) => m.to === sq)) { const occ = getPieceAt(sq); - if(!occ) moves.push({ from: square, to: sq }); + if (!occ) moves.push({ from: square, to: sq }); } - }catch(_){ } + } catch (_) {} }); } - }catch(_){ } + } catch (_) {} // detect teleport effect: allow this piece to move to any empty square (one-turn temporary effect) - try{ - const hasTeleport = (room.activeCardEffects || []).some(e => e && e.type === 'teleport' && ((e.pieceId && e.pieceId === piece.id) || e.pieceSquare === square)); - if(hasTeleport){ + try { + const hasTeleport = (room.activeCardEffects || []).some( + (e) => + e && + e.type === "teleport" && + ((e.pieceId && e.pieceId === piece.id) || e.pieceSquare === square), + ); + if (hasTeleport) { // iterate all board squares and add empty ones as legal non-capturing moves - for(let tx = 0; tx < width; tx++){ - for(let ty = 0; ty < height; ty++){ + for (let tx = 0; tx < width; tx++) { + for (let ty = 0; ty < height; ty++) { const tsq = coordToSquare(tx, ty); - if(!tsq) continue; + if (!tsq) continue; // avoid adding the current square - if(tsq === square) continue; + if (tsq === square) continue; const occ = getPieceAt(tsq); - if(!occ && !moves.some(m => m.to === tsq)){ + if (!occ && !moves.some((m) => m.to === tsq)) { moves.push({ from: square, to: tsq }); } } } } - }catch(_){ } + } catch (_) {} // detect if this specific piece has a permanent adoubement effect (grants knight moves) // effects may be bound either to the piece's current square or to the piece id (preferred) - const hasAdoubement = (room.activeCardEffects || []).some(e => e.type === 'adoubement' && ((e.pieceId && e.pieceId === piece.id) || e.pieceSquare === square)); + const hasAdoubement = (room.activeCardEffects || []).some( + (e) => + e.type === "adoubement" && + ((e.pieceId && e.pieceId === piece.id) || e.pieceSquare === square), + ); // helper to add knight (N) deltas when a piece has been adoubé - function addAdoubementMoves(){ - if(!hasAdoubement) return; - 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; + function addAdoubementMoves() { + if (!hasAdoubement) return; + 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; const tsq = resolveSquareWithAnneau(tx, ty); - if(!tsq) return; + if (!tsq) return; // avoid duplicates - if(moves.some(m => m.to === tsq)) return; + if (moves.some((m) => m.to === tsq)) return; const occ = getPieceAt(tsq); - if(!occ || occ.color !== color) moves.push({ from: square, to: tsq }); + if (!occ || occ.color !== color) moves.push({ from: square, to: tsq }); }); } // detect if this specific piece has a permanent folie effect (grants bishop moves) - const hasFolie = (room.activeCardEffects || []).some(e => e.type === 'folie' && ((e.pieceId && e.pieceId === piece.id) || e.pieceSquare === square)); + const hasFolie = (room.activeCardEffects || []).some( + (e) => + e.type === "folie" && + ((e.pieceId && e.pieceId === piece.id) || e.pieceSquare === square), + ); // detect if this specific piece has a permanent fortification effect (grants rook moves) - const hasFortification = (room.activeCardEffects || []).some(e => e.type === 'fortification' && ((e.pieceId && e.pieceId === piece.id) || e.pieceSquare === square)); + const hasFortification = (room.activeCardEffects || []).some( + (e) => + e.type === "fortification" && + ((e.pieceId && e.pieceId === piece.id) || e.pieceSquare === square), + ); // detect if the owner of this piece currently has an active 'anneau' effect - const anneauPlayers = (room.activeCardEffects || []).filter(e => e.type === 'anneau').map(e => e.playerId); - const ownerPlayer = (room.players || []).find(p => (p.color && p.color[0]) === piece.color); + const anneauPlayers = (room.activeCardEffects || []) + .filter((e) => e.type === "anneau") + .map((e) => e.playerId); + const ownerPlayer = (room.players || []).find( + (p) => (p.color && p.color[0]) === piece.color, + ); const hasAnneau = ownerPlayer && anneauPlayers.indexOf(ownerPlayer.id) !== -1; // helper to resolve a target square, allowing horizontal wrap when anneau is active for this piece - function resolveSquareWithAnneau(tx, ty){ + function resolveSquareWithAnneau(tx, ty) { // inside bounds - if(isInside(tx, ty)) return coordToSquare(tx, ty); + if (isInside(tx, ty)) return coordToSquare(tx, ty); // allow only horizontal wrap (left/right) when anneau is active for this piece's owner - if(!hasAnneau) return null; - if(ty < 0 || ty >= height) return null; // no vertical wrapping - if(tx < 0 || tx >= width){ + if (!hasAnneau) return null; + if (ty < 0 || ty >= height) return null; // no vertical wrapping + if (tx < 0 || tx >= width) { const wx = ((tx % width) + width) % width; return coordToSquare(wx, ty); } @@ -631,18 +812,30 @@ function computeLegalMoves(room, square){ // 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(){ - if(!hasFolie) return; - const dirs = [[1,1],[1,-1],[-1,1],[-1,-1]]; - dirs.forEach(([dx,dy])=>{ - let tx = x + dx, ty = y + dy; + function addFolieMoves() { + if (!hasFolie) return; + const dirs = [ + [1, 1], + [1, -1], + [-1, 1], + [-1, -1], + ]; + dirs.forEach(([dx, dy]) => { + let tx = x + dx, + ty = y + dy; let wrapped = false; - while(true){ + while (true) { // resolve target square with anneau let tsq = null; - if(isInside(tx,ty)){ - tsq = coordToSquare(tx,ty); - } else if(hasAnneau && (tx < 0 || tx >= width) && ty >= 0 && ty < height && !wrapped){ + if (isInside(tx, ty)) { + tsq = coordToSquare(tx, ty); + } else if ( + hasAnneau && + (tx < 0 || tx >= width) && + ty >= 0 && + ty < height && + !wrapped + ) { const wx = ((tx % width) + width) % width; tsq = coordToSquare(wx, ty); tx = wx; @@ -651,31 +844,48 @@ function computeLegalMoves(room, square){ break; } // avoid duplicates - if(!moves.some(m => m.to === tsq)){ + if (!moves.some((m) => m.to === tsq)) { const occ = getPieceAt(tsq); - if(!occ){ moves.push({ from: square, to: tsq }); } - else { if(occ.color !== color) moves.push({ from: square, to: tsq }); break; } + if (!occ) { + moves.push({ from: square, to: tsq }); + } else { + if (occ.color !== color) moves.push({ from: square, to: tsq }); + break; + } } else { // if duplicate found, still need to stop sliding if occupied const occ = getPieceAt(tsq); - if(occ) break; + if (occ) break; } - tx += dx; ty += dy; + tx += dx; + ty += dy; } }); } // helper to add orthogonal sliding moves when a piece has been 'fortifié' - function addFortificationMoves(){ - if(!hasFortification) return; - const dirs = [[1,0],[-1,0],[0,1],[0,-1]]; - dirs.forEach(([dx,dy])=>{ - let tx = x + dx, ty = y + dy; + function addFortificationMoves() { + if (!hasFortification) return; + const dirs = [ + [1, 0], + [-1, 0], + [0, 1], + [0, -1], + ]; + dirs.forEach(([dx, dy]) => { + let tx = x + dx, + ty = y + dy; let wrapped = false; - while(true){ + while (true) { let tsq = null; - if(isInside(tx,ty)){ - tsq = coordToSquare(tx,ty); - } else if(hasAnneau && (tx < 0 || tx >= width) && ty >= 0 && ty < height && !wrapped){ + if (isInside(tx, ty)) { + tsq = coordToSquare(tx, ty); + } else if ( + hasAnneau && + (tx < 0 || tx >= width) && + ty >= 0 && + ty < height && + !wrapped + ) { const wx = ((tx % width) + width) % width; tsq = coordToSquare(wx, ty); tx = wx; @@ -683,76 +893,111 @@ function computeLegalMoves(room, square){ } else { break; } - if(!moves.some(m => m.to === tsq)){ + if (!moves.some((m) => m.to === tsq)) { const occ = getPieceAt(tsq); - if(!occ){ moves.push({ from: square, to: tsq }); } - else { if(occ.color !== color) moves.push({ from: square, to: tsq }); break; } + if (!occ) { + moves.push({ from: square, to: tsq }); + } else { + if (occ.color !== color) moves.push({ from: square, to: tsq }); + break; + } } else { const occ = getPieceAt(tsq); - if(occ) break; + if (occ) break; } - tx += dx; ty += dy; + tx += dx; + ty += dy; } }); } - if(type === 'P'){ + if (type === "P") { // pawn - const forward = (color === 'w') ? 1 : -1; - const startRank = (color === 'w') ? 1 : (height - 2); + 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)){ + if (isInside(x, oneY) && !getPieceAt(oneSq)) { moves.push({ from: square, to: oneSq }); // double push from starting rank - const twoY = y + forward*2; + const twoY = y + forward * 2; const twoSq = coordToSquare(x, twoY); - if(y === startRank && isInside(x, twoY) && !getPieceAt(twoSq)){ + 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])=>{ + [ + [x - 1, y + forward], + [x + 1, y + forward], + ].forEach(([tx, ty]) => { const tsq = resolveSquareWithAnneau(tx, ty); - if(!tsq) return; + if (!tsq) return; const occ = getPieceAt(tsq); - if(occ && occ.color !== color) moves.push({ from: square, to: tsq }); + if (occ && occ.color !== color) moves.push({ from: square, to: tsq }); }); - addAdoubementMoves(); addFolieMoves(); addFortificationMoves(); + addAdoubementMoves(); + addFolieMoves(); + addFortificationMoves(); 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 (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; const tsq = resolveSquareWithAnneau(tx, ty); - if(!tsq) return; + if (!tsq) return; const occ = getPieceAt(tsq); - if(!occ || occ.color !== color) moves.push({ from: square, to: tsq }); + if (!occ || occ.color !== color) moves.push({ from: square, to: tsq }); }); - addAdoubementMoves(); addFolieMoves(); addFortificationMoves(); + addAdoubementMoves(); + addFolieMoves(); + addFortificationMoves(); return moves; } - if(type === 'B' || type === 'Q' || type === 'R'){ + 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]); + 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]); - // detect if this specific piece has an active rebondir effect (match by id or square) - const hasRebond = (room.activeCardEffects || []).some(e => e.type === 'rebondir' && ((e.pieceId && e.pieceId === piece.id) || e.pieceSquare === square)); + // detect if this specific piece has an active rebondir effect (match by id or square) + const hasRebond = (room.activeCardEffects || []).some( + (e) => + e.type === "rebondir" && + ((e.pieceId && e.pieceId === piece.id) || e.pieceSquare === square), + ); - directions.forEach(([dx0,dy0])=>{ - if(!hasRebond){ + directions.forEach(([dx0, dy0]) => { + if (!hasRebond) { // standard sliding behavior, with optional single horizontal wrap when anneau is active - let tx = x + dx0, ty = y + dy0; + let tx = x + dx0, + ty = y + dy0; let wrapped = false; - while(true){ + while (true) { // resolve target taking anneau into account let tsq = null; - if(isInside(tx,ty)){ - tsq = coordToSquare(tx,ty); - } else if(hasAnneau && (tx < 0 || tx >= width) && ty >= 0 && ty < height && !wrapped){ + if (isInside(tx, ty)) { + tsq = coordToSquare(tx, ty); + } else if ( + hasAnneau && + (tx < 0 || tx >= width) && + ty >= 0 && + ty < height && + !wrapped + ) { const wx = ((tx % width) + width) % width; tsq = coordToSquare(wx, ty); // set tx to wrapped coordinate so further increments continue correctly @@ -763,501 +1008,923 @@ function computeLegalMoves(room, square){ } const occ = getPieceAt(tsq); - if(!occ){ + if (!occ) { moves.push({ from: square, to: tsq }); // advance current position along the direction - tx += dx0; ty += dy0; + tx += dx0; + ty += dy0; continue; } // occupied - if(occ.color !== color) moves.push({ from: square, to: tsq }); + if (occ.color !== color) moves.push({ from: square, to: tsq }); break; } } else { // sliding with a single bounce on edges (mirror reflection once) - let dx = dx0, dy = dy0; - let cx = x, cy = y; // current position while sliding + let dx = dx0, + dy = dy0; + let cx = x, + cy = y; // current position while sliding let bounced = false; - while(true){ - let tx = cx + dx, ty = cy + dy; - if(!isInside(tx,ty)){ - if(bounced) break; // already used bounce + while (true) { + let tx = cx + dx, + ty = cy + dy; + if (!isInside(tx, ty)) { + if (bounced) break; // already used bounce // reflect the direction components that would go out of bounds - if(tx < 0 || tx >= width) dx = -dx; - if(ty < 0 || ty >= height) dy = -dy; + if (tx < 0 || tx >= width) dx = -dx; + if (ty < 0 || ty >= height) dy = -dy; bounced = true; // recompute the next square after reflection from current position - tx = cx + dx; ty = cy + dy; - if(!isInside(tx,ty)) break; // still invalid after reflection + tx = cx + dx; + ty = cy + dy; + if (!isInside(tx, ty)) break; // still invalid after reflection } - const tsq = coordToSquare(tx,ty); + const tsq = coordToSquare(tx, ty); const occ = getPieceAt(tsq); - if(!occ){ + if (!occ) { moves.push({ from: square, to: tsq }); // advance current position along the (possibly reflected) direction - cx = tx; cy = ty; + cx = tx; + cy = ty; continue; } // occupied - if(occ.color !== color) moves.push({ from: square, to: tsq }); + if (occ.color !== color) moves.push({ from: square, to: tsq }); break; } } }); - addAdoubementMoves(); addFolieMoves(); addFortificationMoves(); + addAdoubementMoves(); + addFolieMoves(); + addFortificationMoves(); 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; - const tsq = resolveSquareWithAnneau(tx, ty); - if(!tsq) continue; - const occ = getPieceAt(tsq); - if(!occ || occ.color !== color) moves.push({ from: square, to: tsq }); - } - addAdoubementMoves(); addFolieMoves(); + 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; + const tsq = resolveSquareWithAnneau(tx, ty); + if (!tsq) continue; + const occ = getPieceAt(tsq); + if (!occ || occ.color !== color) moves.push({ from: square, to: tsq }); + } + addAdoubementMoves(); + addFolieMoves(); return moves; } - addAdoubementMoves(); addFolieMoves(); addFortificationMoves(); + addAdoubementMoves(); + addFolieMoves(); + addFortificationMoves(); return moves; } -function startingBoardState8(){ +function startingBoardState8() { // returns a simple pieces array with square names for standard chess starting position const rows = [ - 'rnbqkbnr', - 'pppppppp', - '........', - '........', - '........', - '........', - 'PPPPPPPP', - 'RNBQKBNR' + "rnbqkbnr", + "pppppppp", + "........", + "........", + "........", + "........", + "PPPPPPPP", + "RNBQKBNR", ]; const pieces = []; - for(let r=0;r<8;r++){ + for (let r = 0; r < 8; r++) { const row = rows[r]; - for(let f=0;f<8;f++){ + for (let f = 0; f < 8; f++) { const ch = row[f]; - if(ch === '.') continue; - const fileLetter = String.fromCharCode('a'.charCodeAt(0) + f); + if (ch === ".") continue; + const fileLetter = String.fromCharCode("a".charCodeAt(0) + f); const rank = 8 - r; const square = `${fileLetter}${rank}`; - const isWhite = (ch === ch.toUpperCase()); + const isWhite = ch === ch.toUpperCase(); const type = ch.toUpperCase(); - const color = isWhite ? 'w' : 'b'; - const id = (color === 'w' ? 'w_' : 'b_') + type + '_' + pieces.length; + const color = isWhite ? "w" : "b"; + const id = (color === "w" ? "w_" : "b_") + type + "_" + pieces.length; pieces.push({ square, type, color, id }); } } - return { width: 8, height: 8, turn: 'w', version: 1, pieces }; + return { width: 8, height: 8, turn: "w", version: 1, pieces }; } -app.post('/rooms', (req, res) => { +app.post("/rooms", (req, res) => { const id = uuidv4().slice(0, 8); const size = (req.body && parseInt(req.body.size, 10)) || 8; const boardState = size === 8 ? startingBoardState8() : null; const deck = buildDefaultDeck(); - rooms.set(id, { id, boardState, players: [], status: 'waiting', hostId: null, size, cards: {}, playedCards: [], removalTimers: new Map(), deck, hands: {}, autoDraw: false, noRemise: false }); + rooms.set(id, { + id, + boardState, + players: [], + status: "waiting", + hostId: null, + size, + cards: {}, + playedCards: [], + removalTimers: new Map(), + deck, + hands: {}, + autoDraw: false, + noRemise: false, + }); res.json({ roomId: id, size }); }); -app.get('/rooms/:id', (req, res) => { +app.get("/rooms/:id", (req, res) => { const room = rooms.get(req.params.id); - if (!room) return res.status(404).json({ error: 'room not found', message: "Aucune salle trouvée." }); - const boardState = room.boardState || null; - const size = (room.boardState && room.boardState.width) || room.size; - res.json({ id: room.id, boardState, size: size, players: room.players.map(p => ({ id: p.id, color: p.color })), status: room.status, hostId: room.hostId, cards: Object.keys(room.cards || {}), autoDraw: !!room.autoDraw, noRemise: !!room.noRemise }); + if (!room) + return res + .status(404) + .json({ error: "room not found", message: "Aucune salle trouvée." }); + const boardState = room.boardState || null; + const size = (room.boardState && room.boardState.width) || room.size; + res.json({ + id: room.id, + boardState, + size: size, + players: room.players.map((p) => ({ id: p.id, color: p.color })), + status: room.status, + hostId: room.hostId, + cards: Object.keys(room.cards || {}), + autoDraw: !!room.autoDraw, + noRemise: !!room.noRemise, + }); }); -io.on('connection', (socket) => { - console.log('socket connected', socket.id); +io.on("connection", (socket) => { + console.log("socket connected", socket.id); - socket.on('room:join', ({ roomId, playerId }, cb) => { + socket.on("room:join", ({ roomId, playerId }, cb) => { const room = rooms.get(roomId); - if (!room) return cb && cb({ error: 'room not found', message: "Aucune salle trouvée." }); - const assignedId = playerId || uuidv4(); + if (!room) + return ( + cb && cb({ error: "room not found", message: "Aucune salle trouvée." }) + ); + const assignedId = playerId || uuidv4(); - if(room.removalTimers && room.removalTimers.has(assignedId)){ + if (room.removalTimers && room.removalTimers.has(assignedId)) { clearTimeout(room.removalTimers.get(assignedId)); room.removalTimers.delete(assignedId); } - let existing = room.players.find(p => p.id === assignedId); + let existing = room.players.find((p) => p.id === assignedId); let color; - if(existing){ + if (existing) { existing.socketId = socket.id; color = existing.color; } else { - color = room.players.length === 0 ? 'white' : 'black'; + color = room.players.length === 0 ? "white" : "black"; room.players.push({ id: assignedId, socketId: socket.id, color }); } room.hands = room.hands || {}; - if(!room.deck) room.deck = buildDefaultDeck(); + if (!room.deck) room.deck = buildDefaultDeck(); socket.join(roomId); socket.data.roomId = roomId; socket.data.playerId = assignedId; if (!room.hostId) room.hostId = assignedId; - sendRoomUpdate(room); - try{ - if(room.status === 'playing' && room.boardState && room.boardState.turn){ - const myPlayer = room.players.find(p => p.id === assignedId); - const myShort = (myPlayer && myPlayer.color || '')[0]; - if(myShort === room.boardState.turn){ - const hasHand = room.hands && room.hands[assignedId] && room.hands[assignedId].length > 0; - if(!hasHand) maybeDrawAtTurnStart(room, assignedId); + try { + if ( + room.status === "playing" && + room.boardState && + room.boardState.turn + ) { + const myPlayer = room.players.find((p) => p.id === assignedId); + const myShort = ((myPlayer && myPlayer.color) || "")[0]; + if (myShort === room.boardState.turn) { + const hasHand = + room.hands && + room.hands[assignedId] && + room.hands[assignedId].length > 0; + if (!hasHand) maybeDrawAtTurnStart(room, assignedId); } } - }catch(e){ console.error('post-join draw error', e); } - cb && cb({ ok: true, color, roomId, playerId: assignedId, hostId: room.hostId }); + } catch (e) { + console.error("post-join draw error", e); + } + cb && + cb({ + ok: true, + color, + roomId, + playerId: assignedId, + hostId: room.hostId, + }); }); - socket.on('room:leave', ({ roomId }, cb) => { - try{ + socket.on("room:leave", ({ roomId }, cb) => { + try { const room = rooms.get(roomId || socket.data.roomId); const playerId = socket.data.playerId; - if(!room) return cb && cb({ error: 'room not found', message: "Aucune salle trouvée." }); - if(!playerId) return cb && cb({ error: 'not joined', message: "Vous n'avez pas rejoint la salle." }); - room.players = (room.players || []).filter(p => p.id !== playerId); - if(room.hostId && !room.players.find(p => p.id === room.hostId)){ + if (!room) + return ( + cb && + cb({ error: "room not found", message: "Aucune salle trouvée." }) + ); + if (!playerId) + return ( + cb && + cb({ + error: "not joined", + message: "Vous n'avez pas rejoint la salle.", + }) + ); + room.players = (room.players || []).filter((p) => p.id !== playerId); + if (room.hostId && !room.players.find((p) => p.id === room.hostId)) { room.hostId = room.players[0] ? room.players[0].id : null; } - if((room.players || []).length < 2 && room.status === 'playing') room.status = 'waiting'; - try{ socket.leave(room.id); }catch(_){ } - try{ socket.data.roomId = null; }catch(_){ } - try{ sendRoomUpdate(room); }catch(_){ } + if ((room.players || []).length < 2 && room.status === "playing") + room.status = "waiting"; + try { + socket.leave(room.id); + } catch (_) {} + try { + socket.data.roomId = null; + } catch (_) {} + try { + sendRoomUpdate(room); + } catch (_) {} return cb && cb({ ok: true }); - }catch(e){ console.error('room:leave error', e); return cb && cb({ error: 'server_error' }); } + } catch (e) { + console.error("room:leave error", e); + return cb && cb({ error: "server_error" }); + } }); - socket.on('room:auto_draw:set', ({ roomId, enabled }, cb) => { + socket.on("room:auto_draw:set", ({ roomId, enabled }, cb) => { const room = rooms.get(roomId); - if(!room) return cb && cb({ error: 'room not found', message: "Aucune salle trouvée." }); + if (!room) + return ( + cb && cb({ error: "room not found", message: "Aucune salle trouvée." }) + ); const sender = socket.data.playerId; - if(!sender) return cb && cb({ error: 'not joined', message: "Vous n'avez pas rejoint la salle." }); - if(room.hostId !== sender) return cb && cb({ error: 'only the host can change auto-draw', message: "Seul l'hôte peut changer le dessin automatique." }); + if (!sender) + return ( + cb && + cb({ + error: "not joined", + message: "Vous n'avez pas rejoint la salle.", + }) + ); + if (room.hostId !== sender) + return ( + cb && + cb({ + error: "only the host can change auto-draw", + message: "Seul l'hôte peut changer le dessin automatique.", + }) + ); room.autoDraw = !!enabled; - try{ sendRoomUpdate(room); }catch(_){ } - try{ io.to(room.id).emit('room:auto_draw:changed', { roomId: room.id, enabled: room.autoDraw }); }catch(_){ } + try { + sendRoomUpdate(room); + } catch (_) {} + try { + io.to(room.id).emit("room:auto_draw:changed", { + roomId: room.id, + enabled: room.autoDraw, + }); + } catch (_) {} return cb && cb({ ok: true, autoDraw: room.autoDraw }); }); - socket.on('room:no_remise:set', ({ roomId, enabled }, cb) => { + socket.on("room:no_remise:set", ({ roomId, enabled }, cb) => { const room = rooms.get(roomId); - if(!room) return cb && cb({ error: 'room not found', message: "Aucune salle trouvée." }); + if (!room) + return ( + cb && cb({ error: "room not found", message: "Aucune salle trouvée." }) + ); const sender = socket.data.playerId; - if(!sender) return cb && cb({ error: 'not joined', message: "Vous n'avez pas rejoint la salle." }); - if(room.hostId !== sender) return cb && cb({ error: 'only the host can change no-remise', message: "Seul l'hôte peut changer le no-remise." }); + if (!sender) + return ( + cb && + cb({ + error: "not joined", + message: "Vous n'avez pas rejoint la salle.", + }) + ); + if (room.hostId !== sender) + return ( + cb && + cb({ + error: "only the host can change no-remise", + message: "Seul l'hôte peut changer le no-remise.", + }) + ); room.noRemise = !!enabled; - try{ sendRoomUpdate(room); }catch(_){ } - try{ io.to(room.id).emit('room:no_remise:changed', { roomId: room.id, enabled: room.noRemise }); }catch(_){ } + try { + sendRoomUpdate(room); + } catch (_) {} + try { + io.to(room.id).emit("room:no_remise:changed", { + roomId: room.id, + enabled: room.noRemise, + }); + } catch (_) {} return cb && cb({ ok: true, noRemise: room.noRemise }); }); - socket.on('game:move', ({ roomId, from, to, promotion }, cb) => { + socket.on("game:move", ({ roomId, from, to, promotion }, cb) => { const room = rooms.get(roomId); - if (!room) return cb && cb({ error: 'room not found', message: "Aucune salle trouvée." }); - if(room.status === 'finished') return cb && cb({ error: 'game_over', message: "La partie est terminée." }); - try{ + if (!room) + return ( + cb && cb({ error: "room not found", message: "Aucune salle trouvée." }) + ); + if (room.status === "finished") + return ( + cb && cb({ error: "game_over", message: "La partie est terminée." }) + ); + try { const senderId = socket.data.playerId; - if(!senderId) return cb && cb({ error: 'not joined', message: "Vous n'avez pas rejoint la salle." }); - const roomPlayer = room.players.find(p => p.id === senderId); - if(!roomPlayer) return cb && cb({ error: 'player not in room', message: "Vous n'êtes pas dans cette salle." }); - if(!room.boardState) return cb && cb({ error: 'no board state', message: "Aucun état de plateau disponible." }); + if (!senderId) + return ( + cb && + cb({ + error: "not joined", + message: "Vous n'avez pas rejoint la salle.", + }) + ); + const roomPlayer = room.players.find((p) => p.id === senderId); + if (!roomPlayer) + return ( + cb && + cb({ + error: "player not in room", + message: "Vous n'êtes pas dans cette salle.", + }) + ); + if (!room.boardState) + return ( + cb && + cb({ + error: "no board state", + message: "Aucun état de plateau disponible.", + }) + ); const board = room.boardState; - const playerColorShort = (roomPlayer.color && roomPlayer.color[0]) || null; - if(!playerColorShort) return cb && cb({ error: 'invalid player color' }); + const playerColorShort = + (roomPlayer.color && roomPlayer.color[0]) || null; + if (!playerColorShort) return cb && cb({ error: "invalid player color" }); - if(board.turn !== playerColorShort) return cb && cb({ error: 'not your turn', message: "Ce n'est pas à votre tour de jouer." }); + if (board.turn !== playerColorShort) + return ( + cb && + cb({ + error: "not your turn", + message: "Ce n'est pas à votre tour de jouer.", + }) + ); const pieces = board.pieces || []; - const moving = pieces.find(p => p.square === from); - if(!moving) return cb && cb({ error: 'no piece at source' }); - if(moving.color !== playerColorShort) return cb && cb({ error: 'not your piece', message: "Vous ne pouvez pas déplacer une pièce adverse." }); + const moving = pieces.find((p) => p.square === from); + if (!moving) return cb && cb({ error: "no piece at source" }); + if (moving.color !== playerColorShort) + return ( + cb && + cb({ + error: "not your piece", + message: "Vous ne pouvez pas déplacer une pièce adverse.", + }) + ); - try{ + try { const effects = room.activeCardEffects || []; - const toucher = effects.find(e => e && e.type === 'toucher' && e.playerId === senderId); - if(toucher && toucher.pieceId && moving.id !== toucher.pieceId){ - return cb && cb({ error: 'must_move_restricted_piece', message: "Vous devez déplacer la pièce restreinte." }); + const toucher = effects.find( + (e) => e && e.type === "toucher" && e.playerId === senderId, + ); + if (toucher && toucher.pieceId && moving.id !== toucher.pieceId) { + return ( + cb && + cb({ + error: "must_move_restricted_piece", + message: "Vous devez déplacer la pièce restreinte.", + }) + ); } - }catch(_){ } + } catch (_) {} // tout ou rien - try{ + try { const effects2 = room.activeCardEffects || []; - const tout = effects2.find(e => e && e.type === 'tout_ou_rien' && e.pieceId === moving.id); - if(tout){ - const targetIndexCheck = pieces.findIndex(p => p.square === to); - if(targetIndexCheck === -1){ - return cb && cb({ error: 'must_capture_to_move', message: "Mouvement impossible : cette pièce ne peut se déplacer que pour capturer une pièce adverse." }); + const tout = effects2.find( + (e) => e && e.type === "tout_ou_rien" && e.pieceId === moving.id, + ); + if (tout) { + const targetIndexCheck = pieces.findIndex((p) => p.square === to); + if (targetIndexCheck === -1) { + return ( + cb && + cb({ + error: "must_capture_to_move", + message: + "Mouvement impossible : cette pièce ne peut se déplacer que pour capturer une pièce adverse.", + }) + ); } } - }catch(_){ } + } catch (_) {} const legal = computeLegalMoves(room, from) || []; - const ok = legal.some(m => m.to === to); - if(!ok) return cb && cb({ error: 'illegal move', message: "Mouvement illégal." }); - try{ - const hasBrouillard = (room.activeCardEffects || []).some(e => e && e.type === 'brouillard'); - if(hasBrouillard){ + const ok = legal.some((m) => m.to === to); + if (!ok) + return ( + cb && cb({ error: "illegal move", message: "Mouvement illégal." }) + ); + try { + 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', message: "La destination n'est pas visible." }); + if (!visible.has(to)) { + return ( + cb && + cb({ + error: "destination_not_visible", + message: "La destination n'est pas visible.", + }) + ); } } - }catch(_){ } + } catch (_) {} // Second move of "double" - try{ + try { const effects = room.activeCardEffects || []; - const dbl = effects.find(e => e && (e.type === 'double') && e.playerId === senderId); - if(dbl && typeof dbl.remainingMoves === 'number' && dbl.remainingMoves === 1){ - const occupant = (pieces || []).find(p => p.square === to); - if(occupant){ - return cb && cb({ error: 'capture_forbidden_double', message: "Vous ne pouvez pas capturer lors du deuxième mouvement de 'Jouer deux fois'." }); + const dbl = effects.find( + (e) => e && e.type === "double" && e.playerId === senderId, + ); + if ( + dbl && + typeof dbl.remainingMoves === "number" && + dbl.remainingMoves === 1 + ) { + const occupant = (pieces || []).find((p) => p.square === to); + if (occupant) { + return ( + cb && + cb({ + error: "capture_forbidden_double", + message: + "Vous ne pouvez pas capturer lors du deuxième mouvement de 'Jouer deux fois'.", + }) + ); } } - }catch(_){ } + } catch (_) {} - const targetIndex = pieces.findIndex(p => p.square === to); + const targetIndex = pieces.findIndex((p) => p.square === to); let sniperTriggered = false; let capturedPieceForReactions = null; // sniper - if(targetIndex >= 0){ - try{ + if (targetIndex >= 0) { + try { room.activeCardEffects = room.activeCardEffects || []; - const sniperIdx = room.activeCardEffects.findIndex(e => e && e.type === 'sniper' && e.pieceId === moving.id && e.playerId === senderId); - if(sniperIdx !== -1){ + const sniperIdx = room.activeCardEffects.findIndex( + (e) => + e && + e.type === "sniper" && + e.pieceId === moving.id && + e.playerId === senderId, + ); + if (sniperIdx !== -1) { const capturedPiece = pieces.splice(targetIndex, 1)[0]; - capturedPieceForReactions = capturedPiece; - try{ - room.captured = room.captured || []; - const originalOwner = (room.players || []).find(p => (p.color && p.color[0]) === capturedPiece.color); - room.captured.push({ id: uuidv4().slice(0,8), piece: capturedPiece, originalOwnerId: (originalOwner && originalOwner.id) || null, capturedBy: senderId, ts: Date.now() }); - try{ if(capturedPiece && capturedPiece.invisible) delete capturedPiece.invisible; }catch(_){ } - }catch(_){ /* ignore bookkeeping errors */ } - try{ - for(let ei = room.activeCardEffects.length - 1; ei >= 0; ei--){ - const ev = room.activeCardEffects[ei]; - if(!ev) continue; - if(ev.type === 'invisible' && (ev.pieceId === capturedPiece.id || ev.pieceSquare === capturedPiece.square)){ - 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(_){ } - } - } - }catch(_){ } - try{ - const removedEffect = room.activeCardEffects.splice(sniperIdx, 1)[0]; - try{ io.to(room.id).emit('card:effect:removed', { roomId: room.id, effectId: removedEffect && removedEffect.id, type: 'sniper', playerId: removedEffect && removedEffect.playerId }); }catch(_){ } - }catch(_){ } - sniperTriggered = true; - } else { - const capturedPiece = pieces.splice(targetIndex, 1)[0]; - capturedPieceForReactions = capturedPiece; - try{ - room.captured = room.captured || []; - const originalOwner = (room.players || []).find(p => (p.color && p.color[0]) === capturedPiece.color); - room.captured.push({ id: uuidv4().slice(0,8), piece: capturedPiece, originalOwnerId: (originalOwner && originalOwner.id) || null, capturedBy: senderId, ts: Date.now() }); - try{ if(capturedPiece && capturedPiece.invisible) delete capturedPiece.invisible; }catch(_){ } - }catch(_){ /* ignore bookkeeping errors */ } - try{ - for(let ei = room.activeCardEffects.length - 1; ei >= 0; ei--){ - const ev = room.activeCardEffects[ei]; - if(!ev) continue; - if(ev.type === 'invisible' && (ev.pieceId === capturedPiece.id || ev.pieceSquare === capturedPiece.square)){ - 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(_){ } - } - } - }catch(_){ } - } - }catch(_){ - const capturedPiece = pieces.splice(targetIndex, 1)[0]; capturedPieceForReactions = capturedPiece; - try{ room.captured = room.captured || []; const originalOwner = (room.players || []).find(p => (p.color && p.color[0]) === capturedPiece.color); room.captured.push({ id: uuidv4().slice(0,8), piece: capturedPiece, originalOwnerId: (originalOwner && originalOwner.id) || null, capturedBy: senderId, ts: Date.now() }); }catch(_){ } + try { + room.captured = room.captured || []; + const originalOwner = (room.players || []).find( + (p) => (p.color && p.color[0]) === capturedPiece.color, + ); + room.captured.push({ + id: uuidv4().slice(0, 8), + piece: capturedPiece, + originalOwnerId: (originalOwner && originalOwner.id) || null, + capturedBy: senderId, + ts: Date.now(), + }); + try { + if (capturedPiece && capturedPiece.invisible) + delete capturedPiece.invisible; + } catch (_) {} + } catch (_) { + /* ignore bookkeeping errors */ + } + try { + for (let ei = room.activeCardEffects.length - 1; ei >= 0; ei--) { + const ev = room.activeCardEffects[ei]; + if (!ev) continue; + if ( + ev.type === "invisible" && + (ev.pieceId === capturedPiece.id || + ev.pieceSquare === capturedPiece.square) + ) { + 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 (_) {} + } + } + } catch (_) {} + try { + const removedEffect = room.activeCardEffects.splice( + sniperIdx, + 1, + )[0]; + try { + io.to(room.id).emit("card:effect:removed", { + roomId: room.id, + effectId: removedEffect && removedEffect.id, + type: "sniper", + playerId: removedEffect && removedEffect.playerId, + }); + } catch (_) {} + } catch (_) {} + sniperTriggered = true; + } else { + const capturedPiece = pieces.splice(targetIndex, 1)[0]; + capturedPieceForReactions = capturedPiece; + try { + room.captured = room.captured || []; + const originalOwner = (room.players || []).find( + (p) => (p.color && p.color[0]) === capturedPiece.color, + ); + room.captured.push({ + id: uuidv4().slice(0, 8), + piece: capturedPiece, + originalOwnerId: (originalOwner && originalOwner.id) || null, + capturedBy: senderId, + ts: Date.now(), + }); + try { + if (capturedPiece && capturedPiece.invisible) + delete capturedPiece.invisible; + } catch (_) {} + } catch (_) { + /* ignore bookkeeping errors */ + } + try { + for (let ei = room.activeCardEffects.length - 1; ei >= 0; ei--) { + const ev = room.activeCardEffects[ei]; + if (!ev) continue; + if ( + ev.type === "invisible" && + (ev.pieceId === capturedPiece.id || + ev.pieceSquare === capturedPiece.square) + ) { + 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 (_) {} + } + } + } catch (_) {} + } + } catch (_) { + const capturedPiece = pieces.splice(targetIndex, 1)[0]; + capturedPieceForReactions = capturedPiece; + try { + room.captured = room.captured || []; + const originalOwner = (room.players || []).find( + (p) => (p.color && p.color[0]) === capturedPiece.color, + ); + room.captured.push({ + id: uuidv4().slice(0, 8), + piece: capturedPiece, + originalOwnerId: (originalOwner && originalOwner.id) || null, + capturedBy: senderId, + ts: Date.now(), + }); + } catch (_) {} } - } - - // doppelganger - if(capturedPieceForReactions){ - try{ - room.activeCardEffects = room.activeCardEffects || []; - const doppel = room.activeCardEffects.find(e => e && e.type === 'doppelganger' && e.pieceId === moving.id); - if(doppel && capturedPieceForReactions && typeof capturedPieceForReactions.type !== 'undefined'){ - moving.type = capturedPieceForReactions.type; - if(capturedPieceForReactions.promoted) { - moving.promoted = true; - } else if(moving.promoted){ - try{ delete moving.promoted; }catch(_){ } - } - doppel.pieceSquare = to; - try{ io.to(room.id).emit('card:effect:updated', { roomId: room.id, effect: doppel }); }catch(_){ } - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect: Object.assign({}, doppel, { appliedType: capturedPieceForReactions.type }) }); }catch(_){ } } - }catch(_){ } - } - if(!sniperTriggered){ moving.square = to; } + // doppelganger + if (capturedPieceForReactions) { + try { + room.activeCardEffects = room.activeCardEffects || []; + const doppel = room.activeCardEffects.find( + (e) => e && e.type === "doppelganger" && e.pieceId === moving.id, + ); + if ( + doppel && + capturedPieceForReactions && + typeof capturedPieceForReactions.type !== "undefined" + ) { + moving.type = capturedPieceForReactions.type; + if (capturedPieceForReactions.promoted) { + moving.promoted = true; + } else if (moving.promoted) { + try { + delete moving.promoted; + } catch (_) {} + } + doppel.pieceSquare = to; + try { + io.to(room.id).emit("card:effect:updated", { + roomId: room.id, + effect: doppel, + }); + } catch (_) {} + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect: Object.assign({}, doppel, { + appliedType: capturedPieceForReactions.type, + }), + }); + } catch (_) {} + } + } catch (_) {} + } - try{ + if (!sniperTriggered) { + moving.square = to; + } + + try { room.activeCardEffects = room.activeCardEffects || []; - for(let i = room.activeCardEffects.length - 1; i >= 0; i--){ + for (let i = room.activeCardEffects.length - 1; i >= 0; i--) { const e = room.activeCardEffects[i]; - if(e.type === 'rebond' && (e.pieceSquare === from || (e.pieceId && e.pieceId === moving.id))){ - room.activeCardEffects.splice(i,1); + if ( + e.type === "rebond" && + (e.pieceSquare === from || (e.pieceId && e.pieceId === moving.id)) + ) { + room.activeCardEffects.splice(i, 1); continue; } - if(e.pieceId && e.pieceId === moving.id){ + if (e.pieceId && e.pieceId === moving.id) { e.pieceSquare = to; } } - }catch(e){ console.error('consuming card effects error', e); } + } catch (e) { + console.error("consuming card effects error", e); + } // mine - try{ + try { room.activeCardEffects = room.activeCardEffects || []; - for(let i = room.activeCardEffects.length - 1; i >= 0; i--){ + for (let i = room.activeCardEffects.length - 1; i >= 0; i--) { const e = room.activeCardEffects[i]; - if(e && e.type === 'mine' && e.square === to){ - const rmIdx = pieces.findIndex(p => p.id === moving.id); - if(rmIdx >= 0){ + if (e && e.type === "mine" && e.square === to) { + const rmIdx = pieces.findIndex((p) => p.id === moving.id); + if (rmIdx >= 0) { const capturedPiece = pieces.splice(rmIdx, 1)[0]; - try{ + try { room.captured = room.captured || []; - const originalOwner = (room.players || []).find(p => (p.color && p.color[0]) === capturedPiece.color); - room.captured.push({ id: uuidv4().slice(0,8), piece: capturedPiece, originalOwnerId: (originalOwner && originalOwner.id) || null, capturedBy: e.playerId, ts: Date.now() }); - }catch(_){ } + const originalOwner = (room.players || []).find( + (p) => (p.color && p.color[0]) === capturedPiece.color, + ); + room.captured.push({ + id: uuidv4().slice(0, 8), + piece: capturedPiece, + originalOwnerId: (originalOwner && originalOwner.id) || null, + capturedBy: e.playerId, + ts: Date.now(), + }); + } catch (_) {} } - try{ room.activeCardEffects.splice(i,1); }catch(_){ } - try{ io.to(roomId).emit('mine:detonated', { roomId: room.id, ownerId: e.playerId, detonatorId: senderId, square: to, piece: moving }); }catch(_){ } - try{ const owner = (room.players||[]).find(p => p.id === e.playerId); if(owner && owner.socketId) io.to(owner.socketId).emit('mine:detonated:private', { roomId: room.id, effectId: e.id, square: to, piece: moving }); }catch(_){ } + try { + room.activeCardEffects.splice(i, 1); + } catch (_) {} + try { + io.to(roomId).emit("mine:detonated", { + roomId: room.id, + ownerId: e.playerId, + detonatorId: senderId, + square: to, + piece: moving, + }); + } catch (_) {} + try { + const owner = (room.players || []).find( + (p) => p.id === e.playerId, + ); + if (owner && owner.socketId) + io.to(owner.socketId).emit("mine:detonated:private", { + roomId: room.id, + effectId: e.id, + square: to, + piece: moving, + }); + } catch (_) {} break; } } - }catch(err){ console.error('mine detonation error', err); } + } catch (err) { + console.error("mine detonation error", err); + } board.version = (board.version || 0) + 1; // Update brouillard play counts - try{ + try { room.activeCardEffects = room.activeCardEffects || []; - for(let ei = room.activeCardEffects.length - 1; ei >= 0; ei--){ + for (let ei = room.activeCardEffects.length - 1; ei >= 0; ei--) { const ev = room.activeCardEffects[ei]; - if(!ev || ev.type !== 'brouillard') continue; - try{ + 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(_){ } + try { + io.to(room.id).emit("card:effect:updated", { + roomId: room.id, + effect: ev, + }); + } catch (_) {} 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(_){ } - try{ sendRoomUpdate(room); }catch(_){ } + for (const pl of players) { + if (!pl || !pl.id) continue; + if ((ev.playCounts[pl.id] || 0) < threshold) { + allReached = false; + break; + } } - }catch(_){ } + 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 (_) {} + try { + sendRoomUpdate(room); + } catch (_) {} + } + } catch (_) {} } - }catch(e){ console.error('brouillard playcount update error', e); } + } catch (e) { + console.error("brouillard playcount update error", e); + } // victory check - try{ + try { const end = checkAndHandleVictory(room); - if(end && end.over){ + if (end && end.over) { const moved = { playerId: senderId, from, to }; - try{ io.to(roomId).emit('move:moved', moved); }catch(_){ } - try{ sendRoomUpdate(room); }catch(_){ } + try { + io.to(roomId).emit("move:moved", moved); + } catch (_) {} + try { + sendRoomUpdate(room); + } catch (_) {} return cb && cb({ ok: true, moved, gameOver: end }); } - }catch(_){ } + } catch (_) {} // double move let consumedDoubleMove = false; let freeMoveConsumed = false; - try{ - if(room && room._freeMoveFor && room._freeMoveFor === senderId){ + try { + if (room && room._freeMoveFor && room._freeMoveFor === senderId) { freeMoveConsumed = true; - try{ delete room._freeMoveFor; }catch(_){ room._freeMoveFor = null; } - try{ io.to(room.id).emit('card:free_move_consumed', { roomId: room.id, playerId: senderId }); }catch(_){ } + try { + delete room._freeMoveFor; + } catch (_) { + room._freeMoveFor = null; + } + try { + io.to(room.id).emit("card:free_move_consumed", { + roomId: room.id, + playerId: senderId, + }); + } catch (_) {} } - }catch(e){ /* ignore */ } + } catch (e) { + /* ignore */ + } - try{ + try { room.activeCardEffects = room.activeCardEffects || []; - for(let i = room.activeCardEffects.length - 1; i >= 0; i--){ + for (let i = room.activeCardEffects.length - 1; i >= 0; i--) { const e = room.activeCardEffects[i]; - if((e.type === 'double') && e.playerId === senderId){ - const newRemaining = (typeof e.remainingMoves === 'number') ? (e.remainingMoves - 1) : ((e.remainingMoves || 2) - 1); - if(newRemaining > 0){ + if (e.type === "double" && e.playerId === senderId) { + const newRemaining = + typeof e.remainingMoves === "number" + ? e.remainingMoves - 1 + : (e.remainingMoves || 2) - 1; + if (newRemaining > 0) { e.remainingMoves = newRemaining; consumedDoubleMove = true; - try{ io.to(room.id).emit('card:effect:updated', { roomId: room.id, effect: e }); }catch(_){ } + try { + io.to(room.id).emit("card:effect:updated", { + roomId: room.id, + effect: e, + }); + } catch (_) {} } else { - try{ - room.activeCardEffects.splice(i,1); - }catch(_){ } - try{ io.to(room.id).emit('card:effect:removed', { roomId: room.id, effectId: e.id, type: e.type, playerId: e.playerId }); }catch(_){ } + try { + room.activeCardEffects.splice(i, 1); + } catch (_) {} + try { + io.to(room.id).emit("card:effect:removed", { + roomId: room.id, + effectId: e.id, + type: e.type, + playerId: e.playerId, + }); + } catch (_) {} } break; } } - }catch(err){ console.error('double move consume error', err); } + } catch (err) { + console.error("double move consume error", err); + } - if(!consumedDoubleMove){ - board.turn = (board.turn === 'w') ? 'b' : 'w'; + if (!consumedDoubleMove) { + board.turn = board.turn === "w" ? "b" : "w"; - try{ + try { room.activeCardEffects = room.activeCardEffects || []; - for(let i = room.activeCardEffects.length - 1; i >= 0; i--){ + for (let i = room.activeCardEffects.length - 1; i >= 0; i--) { const e = room.activeCardEffects[i]; - if(typeof e.remainingTurns === 'number'){ + if (typeof e.remainingTurns === "number") { let shouldDecrement = false; - if(e.decrementOn === 'opponent'){ - shouldDecrement = (e.playerId !== senderId); - } else if(e.decrementOn === 'owner'){ - shouldDecrement = (e.playerId === senderId); + if (e.decrementOn === "opponent") { + shouldDecrement = e.playerId !== senderId; + } else if (e.decrementOn === "owner") { + shouldDecrement = e.playerId === senderId; } else { - shouldDecrement = (e.playerId === senderId); + shouldDecrement = e.playerId === senderId; } - if(shouldDecrement){ + if (shouldDecrement) { e.remainingTurns = e.remainingTurns - 1; - try{ io.to(room.id).emit('card:effect:updated', { roomId: room.id, effect: e }); }catch(_){ } - if(e.remainingTurns <= 0){ - room.activeCardEffects.splice(i,1); - try{ io.to(room.id).emit('card:effect:removed', { roomId: room.id, effectId: e.id, type: e.type, playerId: e.playerId }); }catch(_){ } + try { + io.to(room.id).emit("card:effect:updated", { + roomId: room.id, + effect: e, + }); + } catch (_) {} + if (e.remainingTurns <= 0) { + room.activeCardEffects.splice(i, 1); + try { + io.to(room.id).emit("card:effect:removed", { + roomId: room.id, + effectId: e.id, + type: e.type, + playerId: e.playerId, + }); + } catch (_) {} } } } } - }catch(e){ console.error('updating temporary effects error', e); } + } catch (e) { + console.error("updating temporary effects error", e); + } - try{ room._cardPlayedThisTurn = {}; }catch(_){ } - // Record whether the sender drew during this turn so it will be available as "previous turn" for their next turn - try{ - room._playerDrewPrev = room._playerDrewPrev || {}; - room._playerDrew = room._playerDrew || {}; - room._playerDrewPrev[senderId] = !!room._playerDrew[senderId]; - room._playerDrew[senderId] = false; - }catch(_){ } + try { + room._cardPlayedThisTurn = {}; + } catch (_) {} + // Record whether the sender drew during this turn so it will be available as "previous turn" for their next turn + try { + room._playerDrewPrev = room._playerDrewPrev || {}; + room._playerDrew = room._playerDrew || {}; + room._playerDrewPrev[senderId] = !!room._playerDrew[senderId]; + room._playerDrew[senderId] = false; + } catch (_) {} } const moved = { playerId: senderId, from, to }; - try{ - if(!consumedDoubleMove){ + try { + if (!consumedDoubleMove) { const nextColor = board.turn; - const nextPlayer = room.players.find(p => (p.color && p.color[0]) === nextColor); - if(nextPlayer){ + const nextPlayer = room.players.find( + (p) => (p.color && p.color[0]) === nextColor, + ); + if (nextPlayer) { maybeDrawAtTurnStart(room, nextPlayer.id); } else { sendRoomUpdate(room); @@ -1265,63 +1932,122 @@ io.on('connection', (socket) => { } else { sendRoomUpdate(room); } - }catch(e){ - console.error('draw-at-start-of-turn error', e); + } catch (e) { + console.error("draw-at-start-of-turn error", e); } - io.to(roomId).emit('move:moved', moved); + io.to(roomId).emit("move:moved", moved); return cb && cb({ ok: true, moved }); - }catch(err){ - console.error('game:move error', err); - return cb && cb({ error: 'server error' }); + } catch (err) { + console.error("game:move error", err); + return cb && cb({ error: "server error" }); } }); - socket.on('game:start', ({ roomId }, cb) => { + socket.on("game:start", ({ roomId }, cb) => { const room = rooms.get(roomId); - if (!room) return cb && cb({ error: 'room not found', message: "La partie n'a pas été trouvée." }); + if (!room) + return ( + cb && + cb({ + error: "room not found", + message: "La partie n'a pas été trouvée.", + }) + ); const senderId = socket.data.playerId; - if (!senderId) return cb && cb({ error: 'not joined', message: "Vous n'avez pas rejoint la partie." }); - if (room.players.length < 2) return cb && cb({ error: 'need 2 players to start', message: "Il faut 2 joueurs pour commencer." }); - if (!room.hostId || room.hostId !== senderId) return cb && cb({ error: 'only host can start', message: "Seul l'hôte peut commencer la partie." }); + if (!senderId) + return ( + cb && + cb({ + error: "not joined", + message: "Vous n'avez pas rejoint la partie.", + }) + ); + if (room.players.length < 2) + return ( + cb && + cb({ + error: "need 2 players to start", + message: "Il faut 2 joueurs pour commencer.", + }) + ); + if (!room.hostId || room.hostId !== senderId) + return ( + cb && + cb({ + error: "only host can start", + message: "Seul l'hôte peut commencer la partie.", + }) + ); - room.status = 'playing'; - io.to(roomId).emit('game:started', { roomId }); + room.status = "playing"; + io.to(roomId).emit("game:started", { roomId }); sendRoomUpdate(room); - try{ - if(room.boardState && room.boardState.turn){ + try { + if (room.boardState && room.boardState.turn) { const firstColor = room.boardState.turn; - const firstPlayer = room.players.find(p => (p.color && p.color[0]) === firstColor); - if(firstPlayer){ - const hasHand = room.hands && room.hands[firstPlayer.id] && room.hands[firstPlayer.id].length > 0; - if(!hasHand) maybeDrawAtTurnStart(room, firstPlayer.id); + const firstPlayer = room.players.find( + (p) => (p.color && p.color[0]) === firstColor, + ); + if (firstPlayer) { + const hasHand = + room.hands && + room.hands[firstPlayer.id] && + room.hands[firstPlayer.id].length > 0; + if (!hasHand) maybeDrawAtTurnStart(room, firstPlayer.id); } } - }catch(e){ - console.error('initial draw error', e); + } catch (e) { + console.error("initial draw error", e); } cb && cb({ ok: true }); }); - socket.on('room:deck:set', ({ roomId, selected }, cb) => { + socket.on("room:deck:set", ({ roomId, selected }, cb) => { const room = rooms.get(roomId); - if(!room) return cb && cb({ error: 'room not found', message: "Aucune salle trouvée." }); + if (!room) + return ( + cb && cb({ error: "room not found", message: "Aucune salle trouvée." }) + ); const sender = socket.data.playerId; - if(!sender) return cb && cb({ error: 'not joined', message: "Vous n'avez pas rejoint la salle." }); - if(room.hostId !== sender) return cb && cb({ error: 'only host can set deck', message: "Seul l'hôte peut définir la pioche." }); - try{ + if (!sender) + return ( + cb && + cb({ + error: "not joined", + message: "Vous n'avez pas rejoint la salle.", + }) + ); + if (room.hostId !== sender) + return ( + cb && + cb({ + error: "only host can set deck", + message: "Seul l'hôte peut définir la pioche.", + }) + ); + try { const sel = Array.isArray(selected) ? selected : []; const master = buildDefaultDeck(); - const pool = (room.deck && Array.isArray(room.deck) && room.deck.length) ? room.deck.concat(master) : master; + const pool = + room.deck && Array.isArray(room.deck) && room.deck.length + ? room.deck.concat(master) + : master; const byId = {}; - pool.forEach(c => { if(c && c.id) byId[c.id] = c; if(c && c.cardId) byId[c.cardId] = c; }); + pool.forEach((c) => { + if (c && c.id) byId[c.id] = c; + if (c && c.cardId) byId[c.cardId] = c; + }); const newDeck = []; - sel.forEach(sid => { const c = byId[sid]; if(c) newDeck.push(Object.assign({}, c)); }); - if(newDeck.length === 0){ + sel.forEach((sid) => { + const c = byId[sid]; + if (c) newDeck.push(Object.assign({}, c)); + }); + if (newDeck.length === 0) { room.deck = buildDefaultDeck(); } else { room.deck = newDeck; @@ -1329,242 +2055,490 @@ io.on('connection', (socket) => { room.discard = room.discard || []; sendRoomUpdate(room); return cb && cb({ ok: true, deckCount: room.deck.length }); - }catch(err){ console.error('room:deck:set error', err); return cb && cb({ error: 'server_error' }); } + } catch (err) { + console.error("room:deck:set error", err); + return cb && cb({ error: "server_error" }); + } }); - socket.on('room:refresh', ({ roomId }, cb) => { - try{ + socket.on("room:refresh", ({ roomId }, cb) => { + try { const room = rooms.get(roomId); - if(!room) return cb && cb({ error: 'room not found' }); + if (!room) return cb && cb({ error: "room not found" }); sendRoomUpdate(room); return cb && cb({ ok: true }); - }catch(e){ console.error('room:refresh error', e); return cb && cb({ error: 'server_error' }); } + } catch (e) { + console.error("room:refresh error", e); + return cb && cb({ error: "server_error" }); + } }); - socket.on('disconnect', () => { + socket.on("disconnect", () => { const roomId = socket.data.roomId; if (!roomId) return; const room = rooms.get(roomId); if (!room) return; const playerId = socket.data.playerId; - if(playerId && room.removalTimers){ - const t = setTimeout(()=>{ - room.players = room.players.filter(p => p.id !== playerId); - if (room.hostId && !room.players.find(p => p.id === room.hostId)) { + if (playerId && room.removalTimers) { + const t = setTimeout(() => { + room.players = room.players.filter((p) => p.id !== playerId); + if (room.hostId && !room.players.find((p) => p.id === room.hostId)) { room.hostId = room.players[0] ? room.players[0].id : null; } - if (room.players.length < 2 && room.status === 'playing') room.status = 'waiting'; + if (room.players.length < 2 && room.status === "playing") + room.status = "waiting"; sendRoomUpdate(room); room.removalTimers.delete(playerId); }, 5000); room.removalTimers.set(playerId, t); } else { - room.players = room.players.filter(p => p.socketId !== socket.id); - if (room.players.length < 2 && room.status === 'playing') room.status = 'waiting'; - if (room.hostId && !room.players.find(p => p.id === room.hostId)) { + room.players = room.players.filter((p) => p.socketId !== socket.id); + if (room.players.length < 2 && room.status === "playing") + room.status = "waiting"; + if (room.hostId && !room.players.find((p) => p.id === room.hostId)) { room.hostId = room.players[0] ? room.players[0].id : null; } sendRoomUpdate(room); } }); - socket.on('game:legalMoves', ({ roomId, square }, cb) => { + socket.on("game:legalMoves", ({ roomId, square }, cb) => { const room = rooms.get(roomId); - if (!room) return cb && cb({ error: 'room not found', message: "La partie n'a pas été trouvée." }); - try{ + if (!room) + return ( + cb && + cb({ + error: "room not found", + message: "La partie n'a pas été trouvée.", + }) + ); + 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: [], message: "La case sélectionnée est invalide." }); + } catch (e) { + console.error("game:legalMoves error", e); + return ( + cb && + cb({ + error: "invalid square", + moves: [], + message: "La case sélectionnée est invalide.", + }) + ); } }); - socket.on('game:select', ({ roomId, square }, cb) => { + socket.on("game:select", ({ roomId, square }, cb) => { const room = rooms.get(roomId); - if(!room) return cb && cb({ error: 'room not found', message: "La partie n'a pas été trouvée." }); + if (!room) + return ( + cb && + cb({ + error: "room not found", + message: "La partie n'a pas été trouvée.", + }) + ); const playerId = socket.data.playerId || null; - try{ socket.data.lastSelectedSquare = square || null; }catch(e){} + try { + socket.data.lastSelectedSquare = square || null; + } catch (e) {} let moves = []; - try{ - if(square){ + try { + if (square) { const effects = room.activeCardEffects || []; - const isAffectedByTous = effects.some(e => e && e.type === 'pareil' && e.playerId && e.playerId !== playerId); - if(isAffectedByTous){ + const isAffectedByTous = effects.some( + (e) => + e && e.type === "pareil" && e.playerId && e.playerId !== playerId, + ); + if (isAffectedByTous) { const tempRoom = Object.assign({}, room); - tempRoom.boardState = JSON.parse(JSON.stringify(room.boardState || {})); - if(Array.isArray(tempRoom.boardState.pieces)){ - tempRoom.boardState.pieces = tempRoom.boardState.pieces.map(pc => { const c = Object.assign({}, pc); c.type = 'K'; return c; }); + tempRoom.boardState = JSON.parse( + JSON.stringify(room.boardState || {}), + ); + if (Array.isArray(tempRoom.boardState.pieces)) { + tempRoom.boardState.pieces = tempRoom.boardState.pieces.map( + (pc) => { + const c = Object.assign({}, pc); + c.type = "K"; + return c; + }, + ); } moves = computeLegalMoves(tempRoom, square) || []; } else { moves = computeLegalMoves(room, square) || []; } } - }catch(e){ - console.error('computeLegalMoves error', e); + } catch (e) { + console.error("computeLegalMoves error", e); moves = []; } // toucher - try{ + try { const effects = room.activeCardEffects || []; - const toucherEffects = effects.filter(e => e && e.type === 'toucher' && e.playerId === playerId); - if(toucherEffects && toucherEffects.length > 0){ - const selectedPiece = (room.boardState && room.boardState.pieces || []).find(p => p.square === square); - const allowedPieceIds = toucherEffects.map(e => e.pieceId).filter(Boolean); - if(!selectedPiece || allowedPieceIds.indexOf(selectedPiece.id) === -1){ + const toucherEffects = effects.filter( + (e) => e && e.type === "toucher" && e.playerId === playerId, + ); + if (toucherEffects && toucherEffects.length > 0) { + const selectedPiece = ( + (room.boardState && room.boardState.pieces) || + [] + ).find((p) => p.square === square); + const allowedPieceIds = toucherEffects + .map((e) => e.pieceId) + .filter(Boolean); + if ( + !selectedPiece || + allowedPieceIds.indexOf(selectedPiece.id) === -1 + ) { moves = []; } } - }catch(_){ /* ignore enforcement errors */ } - try{ - socket.emit('game:select', { playerId, square, moves }); - socket.to(roomId).emit('game:select', { playerId, square, moves: [] }); - }catch(e){ - io.to(roomId).emit('game:select', { playerId, square, moves: [] }); + } catch (_) { + /* ignore enforcement errors */ + } + try { + socket.emit("game:select", { playerId, square, moves }); + socket.to(roomId).emit("game:select", { playerId, square, moves: [] }); + } catch (e) { + io.to(roomId).emit("game:select", { playerId, square, moves: [] }); } cb && cb({ ok: true }); }); // manual draw - socket.on('player:draw', ({ roomId }, cb) => { + socket.on("player:draw", ({ roomId }, cb) => { const room = rooms.get(roomId); - if(!room) return cb && cb({ error: 'room not found', message: "La partie n'a pas été trouvée." }); - if(room.status === 'finished') return cb && cb({ error: 'game_over', message: "La partie est terminée." }); + if (!room) + return ( + cb && + cb({ + error: "room not found", + message: "La partie n'a pas été trouvée.", + }) + ); + if (room.status === "finished") + return ( + cb && cb({ error: "game_over", message: "La partie est terminée." }) + ); const senderId = socket.data.playerId; - if(!senderId) return cb && cb({ error: 'not joined', message: "Vous n'avez pas rejoint la partie." }); + if (!senderId) + return ( + cb && + cb({ + error: "not joined", + message: "Vous n'avez pas rejoint la partie.", + }) + ); const board = room.boardState; - if(!board) return cb && cb({ error: 'no board state', message: "L'état du plateau est introuvable." }); - const roomPlayer = room.players.find(p => p.id === senderId); - if(!roomPlayer) return cb && cb({ error: 'player not in room', message: "Vous n'êtes pas dans cette partie." }); + if (!board) + return ( + cb && + cb({ + error: "no board state", + message: "L'état du plateau est introuvable.", + }) + ); + const roomPlayer = room.players.find((p) => p.id === senderId); + if (!roomPlayer) + return ( + cb && + cb({ + error: "player not in room", + message: "Vous n'êtes pas dans cette partie.", + }) + ); const playerColorShort = (roomPlayer.color && roomPlayer.color[0]) || null; - if(board.turn !== playerColorShort) return cb && cb({ error: 'not your turn', message: "Ce n'est pas à votre tour de jouer." }); - if(room.autoDraw) return cb && cb({ error: 'auto_draw_enabled', message: "Le tirage manuel n'est pas autorisé." }); + if (board.turn !== playerColorShort) + return ( + cb && + cb({ + error: "not your turn", + message: "Ce n'est pas à votre tour de jouer.", + }) + ); + if (room.autoDraw) + return ( + cb && + cb({ + error: "auto_draw_enabled", + message: "Le tirage manuel n'est pas autorisé.", + }) + ); room._cardPlayedThisTurn = room._cardPlayedThisTurn || {}; - if(room._cardPlayedThisTurn[senderId]) return cb && cb({ error: 'card_already_played_this_turn', message: "Vous avez déjà joué une carte ce tour." }); + if (room._cardPlayedThisTurn[senderId]) + return ( + cb && + cb({ + error: "card_already_played_this_turn", + message: "Vous avez déjà joué une carte ce tour.", + }) + ); - try{ + try { // If the player had drawn on their previous turn, disallow drawing now - try{ - if(room._playerDrewPrev && room._playerDrewPrev[senderId]){ - try{ console.log('player:draw: rejected because player drew on previous turn (prev marker true) for', senderId); }catch(_){ } - return cb && cb({ error: 'drew_last_turn', message: "Vous avez pioché lors de votre précédent tour et ne pouvez pas piocher maintenant." }); + try { + if (room._playerDrewPrev && room._playerDrewPrev[senderId]) { + try { + console.log( + "player:draw: rejected because player drew on previous turn (prev marker true) for", + senderId, + ); + } catch (_) {} + return ( + cb && + cb({ + error: "drew_last_turn", + message: + "Vous avez pioché lors de votre précédent tour et ne pouvez pas piocher maintenant.", + }) + ); } - }catch(_){ } + } catch (_) {} const drawn = drawCardForPlayer(room, senderId); - if(!drawn){ - return cb && cb({ error: 'no_card_drawn', message: "Aucune carte n'a été tirée." }); + if (!drawn) { + return ( + cb && + cb({ error: "no_card_drawn", message: "Aucune carte n'a été tirée." }) + ); } - board.version = (board.version || 0) + 1; - room._cardPlayedThisTurn = room._cardPlayedThisTurn || {}; - room._cardPlayedThisTurn[senderId] = true; - board.turn = (board.turn === 'w') ? 'b' : 'w'; - try{ room._cardPlayedThisTurn = {}; }catch(_){ } - // Record whether the sender drew during this turn so it is available as "previous turn" for their next turn - try{ - room._playerDrewPrev = room._playerDrewPrev || {}; - room._playerDrew = room._playerDrew || {}; - room._playerDrewPrev[senderId] = !!room._playerDrew[senderId]; - room._playerDrew[senderId] = false; - }catch(_){ } - try{ + board.version = (board.version || 0) + 1; + room._cardPlayedThisTurn = room._cardPlayedThisTurn || {}; + room._cardPlayedThisTurn[senderId] = true; + board.turn = board.turn === "w" ? "b" : "w"; + try { + room._cardPlayedThisTurn = {}; + } catch (_) {} + // Record whether the sender drew during this turn so it is available as "previous turn" for their next turn + try { + room._playerDrewPrev = room._playerDrewPrev || {}; + room._playerDrew = room._playerDrew || {}; + room._playerDrewPrev[senderId] = !!room._playerDrew[senderId]; + room._playerDrew[senderId] = false; + } catch (_) {} + try { room.activeCardEffects = room.activeCardEffects || []; - for(let i = room.activeCardEffects.length - 1; i >= 0; i--){ + for (let i = room.activeCardEffects.length - 1; i >= 0; i--) { const e = room.activeCardEffects[i]; - if(typeof e.remainingTurns === 'number'){ + if (typeof e.remainingTurns === "number") { let shouldDecrement = false; - if(e.decrementOn === 'opponent'){ - shouldDecrement = (e.playerId !== senderId); - } else if(e.decrementOn === 'owner'){ - shouldDecrement = (e.playerId === senderId); + if (e.decrementOn === "opponent") { + shouldDecrement = e.playerId !== senderId; + } else if (e.decrementOn === "owner") { + shouldDecrement = e.playerId === senderId; } else { - shouldDecrement = (e.playerId === senderId); + shouldDecrement = e.playerId === senderId; } - if(shouldDecrement){ + if (shouldDecrement) { e.remainingTurns = e.remainingTurns - 1; - try{ io.to(room.id).emit('card:effect:updated', { roomId: room.id, effect: e }); }catch(_){ } - if(e.remainingTurns <= 0){ room.activeCardEffects.splice(i,1); try{ io.to(room.id).emit('card:effect:removed', { roomId: room.id, effectId: e.id, type: e.type, playerId: e.playerId }); }catch(_){ } } + try { + io.to(room.id).emit("card:effect:updated", { + roomId: room.id, + effect: e, + }); + } catch (_) {} + if (e.remainingTurns <= 0) { + room.activeCardEffects.splice(i, 1); + try { + io.to(room.id).emit("card:effect:removed", { + roomId: room.id, + effectId: e.id, + type: e.type, + playerId: e.playerId, + }); + } catch (_) {} + } } } } - }catch(e){ console.error('decrement-after-draw error', e); } + } catch (e) { + console.error("decrement-after-draw error", e); + } const nextColor = board.turn; - const nextPlayer = (room.players || []).find(p => (p.color && p.color[0]) === nextColor); - if(nextPlayer){ try{ maybeDrawAtTurnStart(room, nextPlayer.id); }catch(_){ sendRoomUpdate(room); } } - else { sendRoomUpdate(room); } + const nextPlayer = (room.players || []).find( + (p) => (p.color && p.color[0]) === nextColor, + ); + if (nextPlayer) { + try { + maybeDrawAtTurnStart(room, nextPlayer.id); + } catch (_) { + sendRoomUpdate(room); + } + } else { + sendRoomUpdate(room); + } - try{ io.to(room.id).emit('player:drew', { roomId: room.id, playerId: senderId }); }catch(_){ } + try { + io.to(room.id).emit("player:drew", { + roomId: room.id, + playerId: senderId, + }); + } catch (_) {} return cb && cb({ ok: true, card: drawn }); - }catch(err){ console.error('player:draw error', err); return cb && cb({ error: 'server_error' }); } + } catch (err) { + console.error("player:draw error", err); + return cb && cb({ error: "server_error" }); + } }); - socket.on('card:list', ({ roomId }, cb) => { + socket.on("card:list", ({ roomId }, cb) => { const room = rooms.get(roomId); - if(!room) return cb && cb({ error: 'room not found', message: "La partie n'a pas été trouvée." }); + if (!room) + return ( + cb && + cb({ + error: "room not found", + message: "La partie n'a pas été trouvée.", + }) + ); const available = [ - { id: 'invert', name: 'Invert Turn', description: 'Swap movement directions for one move' }, - { id: 'teleport', name: 'Teleport', description: 'Move one piece to any empty square' } + { + id: "invert", + name: "Invert Turn", + description: "Swap movement directions for one move", + }, + { + id: "teleport", + name: "Teleport", + description: "Move one piece to any empty square", + }, ]; cb && cb({ ok: true, cards: available }); }); - socket.on('card:play', ({ roomId, playerId, cardId, payload }, cb) => { + socket.on("card:play", ({ roomId, playerId, cardId, payload }, cb) => { const room = rooms.get(roomId); - if(!room) return cb && cb({ error: 'room not found', message: "La partie n'a pas été trouvée." }); - if(room.status === 'finished') return cb && cb({ error: 'game_over', message: "La partie est terminée." }); + if (!room) + return ( + cb && + cb({ + error: "room not found", + message: "La partie n'a pas été trouvée.", + }) + ); + if (room.status === "finished") + return ( + cb && cb({ error: "game_over", message: "La partie est terminée." }) + ); const senderId = socket.data.playerId; - if(!senderId) return cb && cb({ error: 'not joined', message: "Vous n'avez pas rejoint la partie." }); - try{ + if (!senderId) + return ( + cb && + cb({ + error: "not joined", + message: "Vous n'avez pas rejoint la partie.", + }) + ); + try { const board = room.boardState; - if(room.status === 'playing' && board){ - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - if(board.turn !== playerColorShort) return cb && cb({ error: 'not your turn', message: "Ce n'est pas à votre tour de jouer." }); + if (room.status === "playing" && board) { + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + if (board.turn !== playerColorShort) + return ( + cb && + cb({ + error: "not your turn", + message: "Ce n'est pas à votre tour de jouer.", + }) + ); room._cardPlayedThisTurn = room._cardPlayedThisTurn || {}; - if(room._cardPlayedThisTurn[senderId]) return cb && cb({ error: 'card_already_played_this_turn', message: "Vous avez déjà joué une carte ce tour." }); + if (room._cardPlayedThisTurn[senderId]) + return ( + cb && + cb({ + error: "card_already_played_this_turn", + message: "Vous avez déjà joué une carte ce tour.", + }) + ); } - - }catch(e){ console.error('card play pre-check error', e); } - const played = { id: uuidv4().slice(0,8), playerId: senderId, cardId, payload, ts: Date.now() }; + } catch (e) { + console.error("card play pre-check error", e); + } + const played = { + id: uuidv4().slice(0, 8), + playerId: senderId, + cardId, + payload, + ts: Date.now(), + }; room.playedCards = room.playedCards || []; // cards needing target - try{ - const isRebond = (typeof cardId === 'string') && (cardId.indexOf('rebond') !== -1); - const isAdoub = (typeof cardId === 'string') && (cardId.indexOf('adoubement') !== -1); - const isFolie = (typeof cardId === 'string') && (cardId.indexOf('folie') !== -1); - const isFort = (typeof cardId === 'string') && (cardId.indexOf('fortification') !== -1); + try { + const isRebond = + typeof cardId === "string" && cardId.indexOf("rebond") !== -1; + const isAdoub = + typeof cardId === "string" && cardId.indexOf("adoubement") !== -1; + const isFolie = + typeof cardId === "string" && cardId.indexOf("folie") !== -1; + const isFort = + typeof cardId === "string" && cardId.indexOf("fortification") !== -1; const isTargetCard = isRebond || isAdoub || isFolie || isFort; - if(isTargetCard){ + if (isTargetCard) { const board = room.boardState; let targetCandidate = payload && payload.targetSquare; - if(!targetCandidate){ try{ targetCandidate = socket.data && socket.data.lastSelectedSquare; }catch(e){ targetCandidate = null; } } - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const targetPiece = (board && board.pieces || []).find(p => p.square === targetCandidate); - if(!board || !targetCandidate || !targetPiece || targetPiece.color !== playerColorShort){ - return cb && cb({ error: 'no valid target', message: "Aucune cible valide n'a été sélectionnée." }); + if (!targetCandidate) { + try { + targetCandidate = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + targetCandidate = null; + } + } + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const targetPiece = ((board && board.pieces) || []).find( + (p) => p.square === targetCandidate, + ); + if ( + !board || + !targetCandidate || + !targetPiece || + targetPiece.color !== playerColorShort + ) { + return ( + cb && + cb({ + error: "no valid target", + message: "Aucune cible valide n'a été sélectionnée.", + }) + ); } payload = payload || {}; payload.targetSquare = targetCandidate; played.payload = Object.assign({}, payload); } - }catch(e){ console.error('target card pre-check error', e); } + } catch (e) { + console.error("target card pre-check error", e); + } - try{ + try { room.hands = room.hands || {}; const hand = room.hands[senderId] || []; - const idx = hand.findIndex(c => (c.id && c.id === (payload && payload.id)) || (c.cardId && c.cardId === cardId) || (c.id && c.id === cardId)); - if(idx === -1){ - return cb && cb({ error: 'you do not have that card', message: "Vous ne possedez pas cette carte." }); + const idx = hand.findIndex( + (c) => + (c.id && c.id === (payload && payload.id)) || + (c.cardId && c.cardId === cardId) || + (c.id && c.id === cardId), + ); + if (idx === -1) { + return ( + cb && + cb({ + error: "you do not have that card", + message: "Vous ne possedez pas cette carte.", + }) + ); } - const removed = hand.splice(idx,1)[0]; + const removed = hand.splice(idx, 1)[0]; room.hands[senderId] = hand; room.discard = room.discard || []; - if(!room.noRemise){ + if (!room.noRemise) { room.discard.push(removed); played.card = removed; played._discarded = true; @@ -1572,951 +2546,2395 @@ io.on('connection', (socket) => { played.card = removed; played._discarded = false; } - - cardId = (removed && removed.cardId) || cardId; - }catch(e){ - console.error('card removal error', e); + + cardId = (removed && removed.cardId) || cardId; + } catch (e) { + console.error("card removal error", e); } - // totem - try{ - const active = room.activeCardEffects || []; - const totems = active.filter(e => e && (e.type === 'totem')); - if(totems && totems.length){ - const blocking = totems.find(t => t && t.playerId && t.playerId !== senderId); - if(blocking){ - try{ - room.activeCardEffects = (room.activeCardEffects || []).filter(e => !(e && e.id === blocking.id)); - }catch(_){ } - try{ io.to(room.id).emit('card:effect:removed', { roomId: room.id, effectId: blocking.id, type: 'totem', playerId: blocking.playerId }); }catch(_){ } - try{ io.to(room.id).emit('card:play_blocked', { roomId: room.id, played, blockedBy: 'totem', protectedPlayerId: blocking.playerId }); }catch(_){ } - try{ socket.emit('card:play_blocked:private', { ok: true, blocked: true, reason: 'totem', protectedPlayerId: blocking.playerId, message: 'Votre carte a été annulée par un totem d\'immunité.' }); }catch(_){ } - try{ - const protectedPlayer = (room.players || []).find(p => p && p.id === blocking.playerId); - if(protectedPlayer && protectedPlayer.socketId){ - io.to(protectedPlayer.socketId).emit('notification', { type: 'totem_consumed', roomId: room.id, message: 'Votre totem d\'immunité a annulé la dernière carte jouée par l\'adversaire.' }); - } - }catch(_){ } - try{ sendRoomUpdate(room); }catch(_){ } - return cb && cb({ ok: true, blocked: true, reason: 'totem', protectedPlayerId: blocking.playerId }); + // totem + try { + const active = room.activeCardEffects || []; + const totems = active.filter((e) => e && e.type === "totem"); + if (totems && totems.length) { + const blocking = totems.find( + (t) => t && t.playerId && t.playerId !== senderId, + ); + if (blocking) { + try { + room.activeCardEffects = (room.activeCardEffects || []).filter( + (e) => !(e && e.id === blocking.id), + ); + } catch (_) {} + try { + io.to(room.id).emit("card:effect:removed", { + roomId: room.id, + effectId: blocking.id, + type: "totem", + playerId: blocking.playerId, + }); + } catch (_) {} + try { + io.to(room.id).emit("card:play_blocked", { + roomId: room.id, + played, + blockedBy: "totem", + protectedPlayerId: blocking.playerId, + }); + } catch (_) {} + try { + socket.emit("card:play_blocked:private", { + ok: true, + blocked: true, + reason: "totem", + protectedPlayerId: blocking.playerId, + message: "Votre carte a été annulée par un totem d'immunité.", + }); + } catch (_) {} + try { + const protectedPlayer = (room.players || []).find( + (p) => p && p.id === blocking.playerId, + ); + if (protectedPlayer && protectedPlayer.socketId) { + io.to(protectedPlayer.socketId).emit("notification", { + type: "totem_consumed", + roomId: room.id, + message: + "Votre totem d'immunité a annulé la dernière carte jouée par l'adversaire.", + }); + } + } catch (_) {} + try { + sendRoomUpdate(room); + } catch (_) {} + return ( + cb && + cb({ + ok: true, + blocked: true, + reason: "totem", + protectedPlayerId: blocking.playerId, + }) + ); + } } + } catch (e) { + console.error("totem immunity check error", e); } - }catch(e){ console.error('totem immunity check error', e); } - // board modification cards - try{ + // board modification cards + try { // adoubement - if(cardId === 'adoubement'){ - try{ + if (cardId === "adoubement") { + try { const board = room.boardState; let target = payload && payload.targetSquare; - if(!target){ try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } } - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const targetPiece = (board && board.pieces || []).find(p => p.square === target); - if(!board || !target || !targetPiece || targetPiece.color !== playerColorShort){ - try{ + if (!target) { + try { + target = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + target = null; + } + } + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const targetPiece = ((board && board.pieces) || []).find( + (p) => p.square === target, + ); + if ( + !board || + !target || + !targetPiece || + targetPiece.color !== playerColorShort + ) { + try { room.hands = room.hands || {}; room.hands[senderId] = room.hands[senderId] || []; - if(removed) room.hands[senderId].push(removed); + 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){ console.error('restore removed card error', e); } - return cb && cb({ error: 'no valid target', message: "Aucune cible valide n'a été sélectionnée." }); + 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) { + console.error("restore removed card error", e); + } + return ( + cb && + cb({ + error: "no valid target", + message: "Aucune cible valide n'a été sélectionnée.", + }) + ); } room.activeCardEffects = room.activeCardEffects || []; - room.activeCardEffects.push({ id: played.id, type: 'adoubement', pieceId: targetPiece.id, pieceSquare: target, playerId: senderId }); - played.payload = Object.assign({}, payload, { applied: 'adoubement', appliedTo: target }); - }catch(e){ console.error('adoubement effect error', e); } - } - + room.activeCardEffects.push({ + id: played.id, + type: "adoubement", + pieceId: targetPiece.id, + pieceSquare: target, + playerId: senderId, + }); + played.payload = Object.assign({}, payload, { + applied: "adoubement", + appliedTo: target, + }); + } catch (e) { + console.error("adoubement effect error", e); + } + } + // doppelganger - else if(cardId === 'doppelganger'){ - try{ + else if (cardId === "doppelganger") { + try { const board = room.boardState; let target = payload && payload.targetSquare; - if(!target){ try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } } - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const targetPiece = (board && board.pieces || []).find(p => p.square === target); - if(!board || !target || !targetPiece || targetPiece.color !== playerColorShort){ - try{ + if (!target) { + try { + target = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + target = null; + } + } + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const targetPiece = ((board && board.pieces) || []).find( + (p) => p.square === target, + ); + if ( + !board || + !target || + !targetPiece || + targetPiece.color !== playerColorShort + ) { + try { room.hands = room.hands || {}; room.hands[senderId] = room.hands[senderId] || []; - if(removed) room.hands[senderId].push(removed); + 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){ console.error('restore removed card error', e); } - return cb && cb({ error: 'no valid target', message: "Aucune cible valide n'a été sélectionnée." }); - } - try{ - room.activeCardEffects = room.activeCardEffects || []; - const effect = { id: played.id, type: 'doppelganger', pieceId: targetPiece.id, pieceSquare: target, playerId: senderId }; - room.activeCardEffects.push(effect); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } - played.payload = Object.assign({}, payload, { applied: 'doppelganger', appliedTo: target }); - }catch(e){ console.error('apply persistent doppelganger error', e); played.payload = Object.assign({}, payload, { applied: 'doppelganger', appliedTo: target }); } - }catch(e){ console.error('doppelganger minimal apply error', e); } - } - else if(cardId === 'folie' || (typeof cardId === 'string' && cardId.indexOf('folie') !== -1)){ - try{ - const board = room.boardState; - let target = payload && payload.targetSquare; - if(!target){ try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } } - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const targetPiece = (board && board.pieces || []).find(p => p.square === target); - if(!board || !target || !targetPiece || targetPiece.color !== playerColorShort){ - 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){ console.error('restore removed card error', e); } - return cb && cb({ error: 'no valid target', message: "Aucune cible valide n'a été sélectionnée." }); + 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; + } } - room.activeCardEffects = room.activeCardEffects || []; - room.activeCardEffects.push({ id: played.id, type: 'folie', pieceId: targetPiece.id, pieceSquare: target, playerId: senderId }); - played.payload = Object.assign({}, payload, { applied: 'folie', appliedTo: target }); - }catch(e){ console.error('folie effect error', e); } + } catch (e) { + console.error("restore removed card error", e); + } + return ( + cb && + cb({ + error: "no valid target", + message: "Aucune cible valide n'a été sélectionnée.", + }) + ); + } + try { + room.activeCardEffects = room.activeCardEffects || []; + const effect = { + id: played.id, + type: "doppelganger", + pieceId: targetPiece.id, + pieceSquare: target, + playerId: senderId, + }; + room.activeCardEffects.push(effect); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + played.payload = Object.assign({}, payload, { + applied: "doppelganger", + appliedTo: target, + }); + } catch (e) { + console.error("apply persistent doppelganger error", e); + played.payload = Object.assign({}, payload, { + applied: "doppelganger", + appliedTo: target, + }); + } + } catch (e) { + console.error("doppelganger minimal apply error", e); + } + } else if ( + cardId === "folie" || + (typeof cardId === "string" && cardId.indexOf("folie") !== -1) + ) { + try { + const board = room.boardState; + let target = payload && payload.targetSquare; + if (!target) { + try { + target = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + target = null; + } + } + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const targetPiece = ((board && board.pieces) || []).find( + (p) => p.square === target, + ); + if ( + !board || + !target || + !targetPiece || + targetPiece.color !== playerColorShort + ) { + 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) { + console.error("restore removed card error", e); + } + return ( + cb && + cb({ + error: "no valid target", + message: "Aucune cible valide n'a été sélectionnée.", + }) + ); + } + room.activeCardEffects = room.activeCardEffects || []; + room.activeCardEffects.push({ + id: played.id, + type: "folie", + pieceId: targetPiece.id, + pieceSquare: target, + playerId: senderId, + }); + played.payload = Object.assign({}, payload, { + applied: "folie", + appliedTo: target, + }); + } catch (e) { + console.error("folie effect error", e); + } } // fortification - else if(cardId === 'fortification'){ - try{ + else if (cardId === "fortification") { + try { const board = room.boardState; let target = payload && payload.targetSquare; - if(!target){ try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } } - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const targetPiece = (board && board.pieces || []).find(p => p.square === target); - if(!board || !target || !targetPiece || targetPiece.color !== playerColorShort){ - try{ + if (!target) { + try { + target = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + target = null; + } + } + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const targetPiece = ((board && board.pieces) || []).find( + (p) => p.square === target, + ); + if ( + !board || + !target || + !targetPiece || + targetPiece.color !== playerColorShort + ) { + try { room.hands = room.hands || {}; room.hands[senderId] = room.hands[senderId] || []; - if(removed) room.hands[senderId].push(removed); + 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){ console.error('restore removed card error', e); } - return cb && cb({ error: 'no valid target', message: "Aucune cible valide n'a été sélectionnée." }); + 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) { + console.error("restore removed card error", e); + } + return ( + cb && + cb({ + error: "no valid target", + message: "Aucune cible valide n'a été sélectionnée.", + }) + ); } room.activeCardEffects = room.activeCardEffects || []; - room.activeCardEffects.push({ id: played.id, type: 'fortification', pieceId: targetPiece.id, pieceSquare: target, playerId: senderId }); - played.payload = Object.assign({}, payload, { applied: 'fortification', appliedTo: target }); - }catch(e){ console.error('fortification effect error', e); } + room.activeCardEffects.push({ + id: played.id, + type: "fortification", + pieceId: targetPiece.id, + pieceSquare: target, + playerId: senderId, + }); + played.payload = Object.assign({}, payload, { + applied: "fortification", + appliedTo: target, + }); + } catch (e) { + console.error("fortification effect error", e); + } } // toucher - else if(cardId === 'toucher'){ - try{ + else if (cardId === "toucher") { + try { const board = room.boardState; let target = payload && payload.targetSquare; - if(!target){ try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } } - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const targetPiece = (board && board.pieces || []).find(p => p.square === target); - if(!board || !target || !targetPiece || targetPiece.color === playerColorShort){ - try{ + if (!target) { + try { + target = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + target = null; + } + } + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const targetPiece = ((board && board.pieces) || []).find( + (p) => p.square === target, + ); + if ( + !board || + !target || + !targetPiece || + targetPiece.color === playerColorShort + ) { + try { room.hands = room.hands || {}; room.hands[senderId] = room.hands[senderId] || []; - if(removed) room.hands[senderId].push(removed); + 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){ console.error('restore removed card error', e); } - return cb && cb({ error: 'no valid target', message: "Aucune cible valide n'a été sélectionnée." }); + 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) { + console.error("restore removed card error", e); + } + return ( + cb && + cb({ + error: "no valid target", + message: "Aucune cible valide n'a été sélectionnée.", + }) + ); } - const targetOwner = (room.players || []).find(p => (p.color && p.color[0]) === targetPiece.color) || null; + const targetOwner = + (room.players || []).find( + (p) => (p.color && p.color[0]) === targetPiece.color, + ) || null; room.activeCardEffects = room.activeCardEffects || []; - const effect = { id: played.id, type: 'toucher', playerId: (targetOwner && targetOwner.id) || null, pieceId: targetPiece.id, pieceSquare: targetPiece.square, remainingTurns: 1, decrementOn: 'owner', imposedBy: senderId, ts: Date.now() }; + const effect = { + id: played.id, + type: "toucher", + playerId: (targetOwner && targetOwner.id) || null, + pieceId: targetPiece.id, + pieceSquare: targetPiece.square, + remainingTurns: 1, + decrementOn: "owner", + imposedBy: senderId, + ts: Date.now(), + }; room.activeCardEffects.push(effect); - played.payload = Object.assign({}, payload, { applied: 'toucher', appliedTo: target }); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } - }catch(e){ console.error('toucher effect error', e); } + played.payload = Object.assign({}, payload, { + applied: "toucher", + appliedTo: target, + }); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + } catch (e) { + console.error("toucher effect error", e); } + } - // parrure - else if(cardId === 'parrure'){ - try{ - const board = room.boardState; - let target = payload && payload.targetSquare; - if(!target){ try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } } - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const targetPiece = (board && board.pieces || []).find(p => p.square === target); - if(!board || !target || !targetPiece || targetPiece.color === playerColorShort || !targetPiece.type || targetPiece.type.toLowerCase() !== 'q'){ - return cb && cb({ error: 'no valid target', message: "Aucune cible valide n'a été sélectionnée." }); + // parrure + else if (cardId === "parrure") { + try { + const board = room.boardState; + let target = payload && payload.targetSquare; + if (!target) { + try { + target = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + target = null; } - try{ - targetPiece.type = 'p'; - if(targetPiece.promoted) delete targetPiece.promoted; - try{ board.version = (board.version || 0) + 1; }catch(_){ } - played.payload = Object.assign({}, payload, { applied: 'parrure', appliedTo: target }); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect: { id: played.id, type: 'parrure', pieceId: targetPiece.id, pieceSquare: targetPiece.square, playerId: senderId } }); }catch(_){ } - }catch(e){ console.error('parrure apply error', e); } - }catch(e){ console.error('parrure effect error', e); } + } + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const targetPiece = ((board && board.pieces) || []).find( + (p) => p.square === target, + ); + if ( + !board || + !target || + !targetPiece || + targetPiece.color === playerColorShort || + !targetPiece.type || + targetPiece.type.toLowerCase() !== "q" + ) { + return ( + cb && + cb({ + error: "no valid target", + message: "Aucune cible valide n'a été sélectionnée.", + }) + ); + } + try { + targetPiece.type = "p"; + if (targetPiece.promoted) delete targetPiece.promoted; + try { + board.version = (board.version || 0) + 1; + } catch (_) {} + played.payload = Object.assign({}, payload, { + applied: "parrure", + appliedTo: target, + }); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect: { + id: played.id, + type: "parrure", + pieceId: targetPiece.id, + pieceSquare: targetPiece.square, + playerId: senderId, + }, + }); + } catch (_) {} + } catch (e) { + console.error("parrure apply error", e); + } + } catch (e) { + console.error("parrure effect error", e); } + } - // sniper - else if(cardId === 'sniper'){ - try{ - const board = room.boardState; - let source = (payload && payload.sourceSquare) || (payload && payload.targetSquare) || null; - if(!source){ try{ source = socket.data && socket.data.lastSelectedSquare; }catch(e){ source = null; } } - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const srcPiece = (board && board.pieces || []).find(p => p.square === source); - if(!board || !source || !srcPiece || srcPiece.color !== playerColorShort){ - 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(_){ } - return cb && cb({ error: 'no valid source selected', message: "Aucune source valide n'a été sélectionnée." }); + // sniper + else if (cardId === "sniper") { + try { + const board = room.boardState; + let source = + (payload && payload.sourceSquare) || + (payload && payload.targetSquare) || + null; + if (!source) { + try { + source = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + source = null; } - room.activeCardEffects = room.activeCardEffects || []; - const effect = { id: played.id, type: 'sniper', playerId: senderId, pieceId: srcPiece.id, pieceSquare: srcPiece.square, remainingUses: 1, imposedBy: senderId, ts: Date.now() }; - room.activeCardEffects.push(effect); - played.payload = Object.assign({}, payload, { applied: 'sniper_bound', appliedTo: source }); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } - }catch(e){ console.error('sniper binding error', e); } + } + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const srcPiece = ((board && board.pieces) || []).find( + (p) => p.square === source, + ); + if ( + !board || + !source || + !srcPiece || + srcPiece.color !== playerColorShort + ) { + 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 (_) {} + return ( + cb && + cb({ + error: "no valid source selected", + message: "Aucune source valide n'a été sélectionnée.", + }) + ); + } + room.activeCardEffects = room.activeCardEffects || []; + const effect = { + id: played.id, + type: "sniper", + playerId: senderId, + pieceId: srcPiece.id, + pieceSquare: srcPiece.square, + remainingUses: 1, + imposedBy: senderId, + ts: Date.now(), + }; + room.activeCardEffects.push(effect); + played.payload = Object.assign({}, payload, { + applied: "sniper_bound", + appliedTo: source, + }); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + } catch (e) { + console.error("sniper binding error", e); } + } - // tout ou rien - else if(cardId === 'tout'){ - try{ - const board = room.boardState; - let target = payload && payload.targetSquare; - if(!target){ try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } } - const targetPiece = (board && board.pieces || []).find(p => p.square === target); - if(!board || !target || !targetPiece){ - return cb && cb({ error: 'no valid target', message: "Aucune cible valide n'a été sélectionnée." }); + // tout ou rien + else if (cardId === "tout") { + try { + const board = room.boardState; + let target = payload && payload.targetSquare; + if (!target) { + try { + target = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + target = null; } - if(targetPiece.type && targetPiece.type.toLowerCase() === 'k'){ - return cb && cb({ error: 'cannot_target_king', message: "Vous ne pouvez pas cibler un roi." }); - } - const targetOwner = (room.players || []).find(p => (p.color && p.color[0]) === targetPiece.color) || null; - room.activeCardEffects = room.activeCardEffects || []; - const effect = { id: played.id, type: 'tout_ou_rien', playerId: (targetOwner && targetOwner.id) || null, pieceId: targetPiece.id, pieceSquare: targetPiece.square, imposedBy: senderId, ts: Date.now() }; - room.activeCardEffects.push(effect); - played.payload = Object.assign({}, payload, { applied: 'tout_ou_rien', appliedTo: target }); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } - }catch(e){ console.error('tout_ou_rien effect error', e); } + } + const targetPiece = ((board && board.pieces) || []).find( + (p) => p.square === target, + ); + if (!board || !target || !targetPiece) { + return ( + cb && + cb({ + error: "no valid target", + message: "Aucune cible valide n'a été sélectionnée.", + }) + ); + } + if (targetPiece.type && targetPiece.type.toLowerCase() === "k") { + return ( + cb && + cb({ + error: "cannot_target_king", + message: "Vous ne pouvez pas cibler un roi.", + }) + ); + } + const targetOwner = + (room.players || []).find( + (p) => (p.color && p.color[0]) === targetPiece.color, + ) || null; + room.activeCardEffects = room.activeCardEffects || []; + const effect = { + id: played.id, + type: "tout_ou_rien", + playerId: (targetOwner && targetOwner.id) || null, + pieceId: targetPiece.id, + pieceSquare: targetPiece.square, + imposedBy: senderId, + ts: Date.now(), + }; + room.activeCardEffects.push(effect); + played.payload = Object.assign({}, payload, { + applied: "tout_ou_rien", + appliedTo: target, + }); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + } catch (e) { + console.error("tout_ou_rien effect error", e); } + } - // inversion - else if(cardId === 'inversion'){ - try{ - const board = room.boardState; - const payloadSrc = payload && payload.sourceSquare; - const payloadTgt = payload && payload.targetSquare; - let source = payloadSrc || null; - let target = payloadTgt || null; - if(!source){ try{ source = socket.data && socket.data.lastSelectedSquare; }catch(_){ source = null; } } - if(!target){ /* nothing */ } - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const srcPiece = (board && board.pieces || []).find(p => p.square === source); - const tgtPiece = (board && board.pieces || []).find(p => p.square === target); - if(!board || !source || !target || !srcPiece || !tgtPiece || srcPiece.color !== playerColorShort || tgtPiece.color === playerColorShort){ - 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){ console.error('restore removed card error', e); } - return cb && cb({ error: 'no valid targets', message: "Aucune cible valide n'a été sélectionnée." }); + // inversion + else if (cardId === "inversion") { + try { + const board = room.boardState; + const payloadSrc = payload && payload.sourceSquare; + const payloadTgt = payload && payload.targetSquare; + let source = payloadSrc || null; + let target = payloadTgt || null; + if (!source) { + try { + source = socket.data && socket.data.lastSelectedSquare; + } catch (_) { + source = null; } - try{ - const sSquare = srcPiece.square; - const tSquare = tgtPiece.square; - srcPiece.square = tSquare; - tgtPiece.square = sSquare; - try{ - room.activeCardEffects = room.activeCardEffects || []; - room.activeCardEffects.forEach(e => { - try{ - if(e && e.pieceSquare && e.pieceSquare === sSquare) e.pieceSquare = tSquare; - else if(e && e.pieceSquare && e.pieceSquare === tSquare) e.pieceSquare = sSquare; - }catch(_){ } - }); - }catch(_){ } - try{ board.version = (board.version || 0) + 1; }catch(_){ } - played.payload = Object.assign({}, payload, { applied: 'inversion', from: source, to: target }); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect: { id: played.id, type: 'inversion', swapped: [srcPiece.id, tgtPiece.id], playerId: senderId } }); }catch(_){ } - }catch(e){ console.error('inversion apply error', e); } - }catch(e){ console.error('inversion effect error', e); } + } + if (!target) { + /* nothing */ + } + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const srcPiece = ((board && board.pieces) || []).find( + (p) => p.square === source, + ); + const tgtPiece = ((board && board.pieces) || []).find( + (p) => p.square === target, + ); + if ( + !board || + !source || + !target || + !srcPiece || + !tgtPiece || + srcPiece.color !== playerColorShort || + tgtPiece.color === playerColorShort + ) { + 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) { + console.error("restore removed card error", e); + } + return ( + cb && + cb({ + error: "no valid targets", + message: "Aucune cible valide n'a été sélectionnée.", + }) + ); + } + try { + const sSquare = srcPiece.square; + const tSquare = tgtPiece.square; + srcPiece.square = tSquare; + tgtPiece.square = sSquare; + try { + room.activeCardEffects = room.activeCardEffects || []; + room.activeCardEffects.forEach((e) => { + try { + if (e && e.pieceSquare && e.pieceSquare === sSquare) + e.pieceSquare = tSquare; + else if (e && e.pieceSquare && e.pieceSquare === tSquare) + e.pieceSquare = sSquare; + } catch (_) {} + }); + } catch (_) {} + try { + board.version = (board.version || 0) + 1; + } catch (_) {} + played.payload = Object.assign({}, payload, { + applied: "inversion", + from: source, + to: target, + }); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect: { + id: played.id, + type: "inversion", + swapped: [srcPiece.id, tgtPiece.id], + playerId: senderId, + }, + }); + } catch (_) {} + } catch (e) { + console.error("inversion apply error", e); + } + } catch (e) { + console.error("inversion effect error", e); + } } // teleportation - else if(cardId === 'teleportation'){ - try{ + else if (cardId === "teleportation") { + try { const board = room.boardState; let target = payload && payload.targetSquare; - if(!target){ try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } } - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const targetPiece = (board && board.pieces || []).find(p => p.square === target); - if(!board || !target || !targetPiece || targetPiece.color !== playerColorShort){ - try{ + if (!target) { + try { + target = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + target = null; + } + } + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const targetPiece = ((board && board.pieces) || []).find( + (p) => p.square === target, + ); + if ( + !board || + !target || + !targetPiece || + targetPiece.color !== playerColorShort + ) { + try { room.hands = room.hands || {}; room.hands[senderId] = room.hands[senderId] || []; - if(removed) room.hands[senderId].push(removed); + 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){ console.error('restore removed card error', e); } - return cb && cb({ error: 'no valid target', message: "Aucune cible valide n'a été sélectionnée." }); + 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) { + console.error("restore removed card error", e); + } + return ( + cb && + cb({ + error: "no valid target", + message: "Aucune cible valide n'a été sélectionnée.", + }) + ); } room.activeCardEffects = room.activeCardEffects || []; - const effect = { id: played.id, type: 'teleport', pieceId: targetPiece.id, pieceSquare: target, playerId: senderId, remainingTurns: 1, decrementOn: 'owner' }; + const effect = { + id: played.id, + type: "teleport", + pieceId: targetPiece.id, + pieceSquare: target, + playerId: senderId, + remainingTurns: 1, + decrementOn: "owner", + }; room.activeCardEffects.push(effect); - played.payload = Object.assign({}, payload, { applied: 'teleport', appliedTo: target }); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } - }catch(e){ console.error('teleport effect error', e); } + played.payload = Object.assign({}, payload, { + applied: "teleport", + appliedTo: target, + }); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + } catch (e) { + console.error("teleport effect error", e); } + } // empathie - else if(cardId === 'empathie'){ - try{ + else if (cardId === "empathie") { + try { const board = room.boardState; - if(!board || !Array.isArray(board.pieces)){ - try{ + if (!board || !Array.isArray(board.pieces)) { + try { room.hands = room.hands || {}; room.hands[senderId] = room.hands[senderId] || []; - if(removed) room.hands[senderId].push(removed); + 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 board to flip', message: "Aucun plateau à retourner." }); - } - function squareToCoord(sq){ if(!sq) return null; const s = String(sq).trim().toLowerCase(); if(!/^[a-z][1-9][0-9]*$/.test(s)) return null; const file = s.charCodeAt(0) - 'a'.charCodeAt(0); const rank = parseInt(s.slice(1),10) - 1; return { x: file, y: rank }; } - function coordToSquare(x,y){ if(x<0||y<0||!board.width||!board.height) return null; if(x<0||y<0||x>=board.width||y>=board.height) return null; return String.fromCharCode('a'.charCodeAt(0) + x) + (y+1); } - const w = board.width || 8; const h = board.height || 8; - (board.pieces || []).forEach(p => { - try{ - const c = squareToCoord(p.square); - if(c){ const nx = (w - 1) - c.x; const ny = (h - 1) - c.y; const ns = coordToSquare(nx, ny); if(ns) p.square = ns; } - p.color = (p.color === 'w' ? 'b' : (p.color === 'b' ? 'w' : p.color)); - }catch(_){ } - }); - try{ - room.activeCardEffects = room.activeCardEffects || []; - room.activeCardEffects.forEach(e => { - if(!e) return; - try{ - if(e.pieceSquare){ const c = squareToCoord(e.pieceSquare); if(c){ const nx = (w-1)-c.x; const ny = (h-1)-c.y; const ns = coordToSquare(nx,ny); if(ns) e.pieceSquare = ns; } } - if(e.square){ const c2 = squareToCoord(e.square); if(c2){ const nx = (w-1)-c2.x; const ny = (h-1)-c2.y; const ns2 = coordToSquare(nx,ny); if(ns2) e.square = ns2; } } - if(Array.isArray(e.allowedSquares)){ - e.allowedSquares = e.allowedSquares.map(sq => { const cc = squareToCoord(sq); if(!cc) return sq; const nx = (w-1)-cc.x; const ny = (h-1)-cc.y; return coordToSquare(nx,ny) || sq; }); + 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(_){ } + } + } catch (e) {} + return ( + cb && + cb({ + error: "no board to flip", + message: "Aucun plateau à retourner.", + }) + ); + } + function squareToCoord(sq) { + if (!sq) return null; + const s = String(sq).trim().toLowerCase(); + if (!/^[a-z][1-9][0-9]*$/.test(s)) return null; + const file = s.charCodeAt(0) - "a".charCodeAt(0); + const rank = parseInt(s.slice(1), 10) - 1; + return { x: file, y: rank }; + } + function coordToSquare(x, y) { + if (x < 0 || y < 0 || !board.width || !board.height) return null; + if (x < 0 || y < 0 || x >= board.width || y >= board.height) + return null; + return String.fromCharCode("a".charCodeAt(0) + x) + (y + 1); + } + const w = board.width || 8; + const h = board.height || 8; + (board.pieces || []).forEach((p) => { + try { + const c = squareToCoord(p.square); + if (c) { + const nx = w - 1 - c.x; + const ny = h - 1 - c.y; + const ns = coordToSquare(nx, ny); + if (ns) p.square = ns; + } + p.color = p.color === "w" ? "b" : p.color === "b" ? "w" : p.color; + } catch (_) {} + }); + try { + room.activeCardEffects = room.activeCardEffects || []; + room.activeCardEffects.forEach((e) => { + if (!e) return; + try { + if (e.pieceSquare) { + const c = squareToCoord(e.pieceSquare); + if (c) { + const nx = w - 1 - c.x; + const ny = h - 1 - c.y; + const ns = coordToSquare(nx, ny); + if (ns) e.pieceSquare = ns; + } + } + if (e.square) { + const c2 = squareToCoord(e.square); + if (c2) { + const nx = w - 1 - c2.x; + const ny = h - 1 - c2.y; + const ns2 = coordToSquare(nx, ny); + if (ns2) e.square = ns2; + } + } + if (Array.isArray(e.allowedSquares)) { + e.allowedSquares = e.allowedSquares.map((sq) => { + const cc = squareToCoord(sq); + if (!cc) return sq; + const nx = w - 1 - cc.x; + const ny = h - 1 - cc.y; + return coordToSquare(nx, ny) || sq; + }); + } + } catch (_) {} }); - }catch(_){ } - (room.players || []).forEach(pl => { try{ pl.color = (pl.color === 'white' ? 'black' : (pl.color === 'black' ? 'white' : pl.color)); }catch(_){ } }); - if(board.turn) board.turn = (board.turn === 'w' ? 'b' : (board.turn === 'b' ? 'w' : board.turn)); + } catch (_) {} + (room.players || []).forEach((pl) => { + try { + pl.color = + pl.color === "white" + ? "black" + : pl.color === "black" + ? "white" + : pl.color; + } catch (_) {} + }); + if (board.turn) + board.turn = + board.turn === "w" ? "b" : board.turn === "b" ? "w" : board.turn; board.version = (board.version || 0) + 1; - const effect = { id: played.id, type: 'empathie', playerId: senderId, ts: Date.now() }; + const effect = { + id: played.id, + type: "empathie", + playerId: senderId, + ts: Date.now(), + }; room.activeCardEffects = room.activeCardEffects || []; room.activeCardEffects.push(effect); - played.payload = Object.assign({}, payload, { applied: 'empathie' }); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } - try{ recordPlayerDrewPrev(room, senderId); }catch(_){ } - }catch(e){ console.error('empathie / changement de camp error', e); } + played.payload = Object.assign({}, payload, { applied: "empathie" }); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + try { + recordPlayerDrewPrev(room, senderId); + } catch (_) {} + } catch (e) { + console.error("empathie / changement de camp error", e); + } } - + // promotion - else if(cardId === 'promotion'){ - try{ - const board = room.boardState; - let target = payload && payload.targetSquare; - if(!target){ try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } } - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const targetPiece = (board && board.pieces || []).find(p => p.square === target); - if(!board || !target || !targetPiece || targetPiece.color !== playerColorShort || (targetPiece.type && String(targetPiece.type).toLowerCase() !== 'p')){ - played.payload = Object.assign({}, payload, { applied: 'promotion_failed', attemptedTo: target }); - try{ const owner = (room.players || []).find(p => p.id === senderId); if(owner && owner.socketId) io.to(owner.socketId).emit('card:effect:applied', { roomId: room.id, effect: { id: played.id, type: 'promotion_failed', playerId: senderId, square: target, ts: Date.now() } }); }catch(_){ } - } else { - const oldType = targetPiece.type; - const chosen = (payload && (payload.promotion || payload.targetPromotion || payload.promoteTo)) || 'q'; - const mapping = { q: 'q', r: 'r', b: 'b', n: 'n' }; - const toType = mapping[String(chosen).toLowerCase()] || 'q'; - targetPiece.type = toType; - targetPiece.promoted = true; - try{ - if(board){ - board.turn = (board.turn === 'w') ? 'b' : 'w'; - try{ recordPlayerDrewPrev(room, senderId); }catch(_){ } - const nextColor = board.turn; - const nextPlayer = (room.players || []).find(p => (p.color && p.color[0]) === nextColor); - if(nextPlayer){ try{ maybeDrawAtTurnStart(room, nextPlayer.id); }catch(_){ } } - } - const effect = { id: played.id, type: 'promotion', pieceId: targetPiece.id, pieceSquare: target, playerId: senderId, ts: Date.now() }; - room.activeCardEffects = room.activeCardEffects || []; - room.activeCardEffects.push(effect); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } - }catch(_){ } - played.payload = Object.assign({}, payload, { applied: 'promotion', appliedTo: target, fromType: oldType, toType: targetPiece.type }); + else if (cardId === "promotion") { + try { + const board = room.boardState; + let target = payload && payload.targetSquare; + if (!target) { + try { + target = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + target = null; } - }catch(e){ console.error('promotion effect error', e); } - } - - // kamikaze - else if(cardId === 'kamikaze'){ - try{ - const board = room.boardState; - let target = payload && payload.targetSquare; - if(!target){ try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } } - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const targetPiece = (board && board.pieces || []).find(p => p.square === target); - if(!board || !target || !targetPiece || targetPiece.color !== playerColorShort){ - 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){ console.error('restore removed card error', e); } - return cb && cb({ error: 'no valid target', message: "Aucune cible valide n'a été sélectionnée." }); - } - function neighbors(sq){ - if(!sq) return []; - const m = String(sq).toLowerCase().match(/^([a-z])([0-9]+)$/i); - if(!m) return []; - const file = m[1].charCodeAt(0) - 'a'.charCodeAt(0); - const rank = parseInt(m[2],10) - 1; - const w = (board.width || 8), h = (board.height || 8); - const out = []; - for(let dx=-1; dx<=1; dx++) for(let dy=-1; dy<=1; dy++){ - const nx = file + dx, ny = rank + dy; - if(nx<0||ny<0||nx>=w||ny>=h) continue; - out.push(String.fromCharCode('a'.charCodeAt(0)+nx) + (ny+1)); - } - return out; - } - const affected = neighbors(target); - const removedPieces = []; - for(let i = (board.pieces || []).length - 1; i >= 0; i--){ - const p = board.pieces[i]; - if(p && affected.indexOf(p.square) !== -1){ - const cp = board.pieces.splice(i,1)[0]; - try{ - room.captured = room.captured || []; - const originalOwner = (room.players || []).find(pl => (pl.color && pl.color[0]) === cp.color); - room.captured.push({ id: uuidv4().slice(0,8), piece: cp, originalOwnerId: (originalOwner && originalOwner.id) || null, capturedBy: senderId, ts: Date.now() }); - }catch(_){ } - removedPieces.push({ id: cp.id, square: cp.square, type: cp.type, color: cp.color }); - } - } - board.version = (board.version || 0) + 1; - const effect = { id: played.id, type: 'kamikaz', playerId: senderId, targetSquare: target, affectedSquares: affected, removed: removedPieces, ts: Date.now() }; - room.activeCardEffects = room.activeCardEffects || []; - room.activeCardEffects.push(effect); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } - try{ io.to(room.id).emit('mine:detonated', { roomId: room.id, square: target }); }catch(_){ } - played.payload = Object.assign({}, payload, { applied: 'kamikaz', appliedTo: target, affected: affected, removedCount: removedPieces.length }); - try{ - const end = checkAndHandleVictory(room); - if(end && end.over){ - try{ sendRoomUpdate(room); }catch(_){ } - } else { - if(board){ - board.turn = (board.turn === 'w') ? 'b' : 'w'; - try{ recordPlayerDrewPrev(room, senderId); }catch(_){ } - const nextColor = board.turn; - const nextPlayer = (room.players || []).find(p => (p.color && p.color[0]) === nextColor); - if(nextPlayer){ try{ maybeDrawAtTurnStart(room, nextPlayer.id); }catch(_){ } } + } + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const targetPiece = ((board && board.pieces) || []).find( + (p) => p.square === target, + ); + if ( + !board || + !target || + !targetPiece || + targetPiece.color !== playerColorShort || + (targetPiece.type && String(targetPiece.type).toLowerCase() !== "p") + ) { + played.payload = Object.assign({}, payload, { + applied: "promotion_failed", + attemptedTo: target, + }); + try { + const owner = (room.players || []).find((p) => p.id === senderId); + if (owner && owner.socketId) + io.to(owner.socketId).emit("card:effect:applied", { + roomId: room.id, + effect: { + id: played.id, + type: "promotion_failed", + playerId: senderId, + square: target, + ts: Date.now(), + }, + }); + } catch (_) {} + } else { + const oldType = targetPiece.type; + const chosen = + (payload && + (payload.promotion || + payload.targetPromotion || + payload.promoteTo)) || + "q"; + const mapping = { q: "q", r: "r", b: "b", n: "n" }; + const toType = mapping[String(chosen).toLowerCase()] || "q"; + targetPiece.type = toType; + targetPiece.promoted = true; + try { + if (board) { + board.turn = board.turn === "w" ? "b" : "w"; + try { + recordPlayerDrewPrev(room, senderId); + } catch (_) {} + const nextColor = board.turn; + const nextPlayer = (room.players || []).find( + (p) => (p.color && p.color[0]) === nextColor, + ); + if (nextPlayer) { + try { + maybeDrawAtTurnStart(room, nextPlayer.id); + } catch (_) {} } } - }catch(_){ } - }catch(e){ console.error('kamikaz effect error', e); } - } - - // coin coin - else if(cardId === 'coincoin'){ - try{ - const board = room.boardState; - let source = payload && payload.targetSquare; - if(!source){ try{ source = socket.data && socket.data.lastSelectedSquare; }catch(e){ source = null; } } - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const piece = (board && board.pieces || []).find(p => p.square === source); - const w = (board && board.width) || 8; - const h = (board && board.height) || 8; - const left = 'a'; - const right = String.fromCharCode('a'.charCodeAt(0) + (w - 1)); - const corners = [ left + '1', left + String(h), right + '1', right + String(h) ]; - if(!board || !source || !piece || piece.color !== playerColorShort || corners.indexOf(source) === -1){ - 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){ console.error('restore removed card error', e); } - return cb && cb({ error: 'no valid corner piece selected', message: "Aucun coin valide n'a été sélectionné." }); - } - const emptyCorners = corners.filter(c => { return !(board.pieces || []).some(p => p.square === c); }); - const destChoices = emptyCorners.filter(c => c !== source); - if(!destChoices || destChoices.length === 0){ - 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){ console.error('restore removed card error', e); } - return cb && cb({ error: 'no empty destination corner available', message: "Aucun coin de destination vide n'est disponible." }); - } - const effect = { id: played.id, type: 'coincoin', playerId: senderId, pieceId: piece.id, pieceSquare: source, allowedSquares: destChoices.slice(0), remainingTurns: 1, ts: Date.now() }; - room.activeCardEffects = room.activeCardEffects || []; - room.activeCardEffects.push(effect); - board.version = (board.version || 0) + 1; - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } - played.payload = Object.assign({}, payload, { applied: 'coincoin', from: source, allowed: destChoices.slice(0) }); - }catch(e){ console.error('coincoin effect error', e); } - } - - // mélange - else if(cardId === 'melange'){ - try{ - const board = room.boardState; - if(!board || !Array.isArray(board.pieces) || board.pieces.length === 0){ - 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(_){ } - return cb && cb({ error: 'no pieces to shuffle', message: "Aucune pièce à mélanger." }); - } - const pieces = board.pieces; - const w = board.width || 8; const h = board.height || 8; - const allSquares = []; - for(let yy = 0; yy < h; yy++){ - for(let xx = 0; xx < w; xx++){ - allSquares.push(String.fromCharCode('a'.charCodeAt(0) + xx) + (yy + 1)); - } - } - if(allSquares.length < pieces.length){ - played.payload = Object.assign({}, payload, { applied: 'melange_failed', reason: 'board_too_small' }); - } else { - const occupiedByBlack = {}; - pieces.forEach(p => { try{ if(p && p.color === 'b' && p.square){ occupiedByBlack[p.square] = true; } }catch(_){ } }); - const occupiedByWhite = {}; - pieces.forEach(p => { try{ if(p && p.color === 'w' && p.square){ occupiedByWhite[p.square] = true; } }catch(_){ } }); - const blackSquares = Object.keys(occupiedByBlack); - for(let i = blackSquares.length - 1; i > 0; i--){ const j = Math.floor(Math.random() * (i + 1)); const tmp = blackSquares[i]; blackSquares[i] = blackSquares[j]; blackSquares[j] = tmp; } - const newBlackSquareByPieceId = {}; - let blackIndex = 0; - pieces.forEach(p => { try{ if(p && p.color === 'b'){ newBlackSquareByPieceId[p.id] = blackSquares[blackIndex] || p.square; blackIndex++; } }catch(_){ } }); - const whiteSquares = Object.keys(occupiedByWhite); - for(let i = whiteSquares.length - 1; i > 0; i--){ const j = Math.floor(Math.random() * (i + 1)); const tmp = whiteSquares[i]; whiteSquares[i] = whiteSquares[j]; whiteSquares[j] = tmp; } - const newWhiteSquareByPieceId = {}; - let whiteIndex = 0; - pieces.forEach(p => { try{ if(p && p.color === 'w'){ newWhiteSquareByPieceId[p.id] = whiteSquares[whiteIndex] || p.square; whiteIndex++; } }catch(_){ } }); - pieces.forEach(p => { - try{ - if(p && p.color === 'b' && newBlackSquareByPieceId[p.id]){ - p.square = newBlackSquareByPieceId[p.id]; - } else if(p && p.color === 'w' && newWhiteSquareByPieceId[p.id]){ - p.square = newWhiteSquareByPieceId[p.id]; - } - }catch(_){ } - }); - board.version = (board.version || 0) + 1; - const effect = { id: played.id, type: 'melange', playerId: senderId, ts: Date.now() }; + const effect = { + id: played.id, + type: "promotion", + pieceId: targetPiece.id, + pieceSquare: target, + playerId: senderId, + ts: Date.now(), + }; room.activeCardEffects = room.activeCardEffects || []; room.activeCardEffects.push(effect); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } - played.payload = Object.assign({}, payload, { applied: 'melange' }); - } - }catch(e){ console.error('melange error', e); } + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + } catch (_) {} + played.payload = Object.assign({}, payload, { + applied: "promotion", + appliedTo: target, + fromType: oldType, + toType: targetPiece.type, + }); + } + } catch (e) { + console.error("promotion effect error", e); } + } - // révolution - else if(cardId === 'revolution'){ - try{ - const board = room.boardState; - if(!board || !Array.isArray(board.pieces) || board.pieces.length === 0){ - 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(_){ } - return cb && cb({ error: 'no pieces to transform', message: "Aucune pièce à transformer." }); + // kamikaze + else if (cardId === "kamikaze") { + try { + const board = room.boardState; + let target = payload && payload.targetSquare; + if (!target) { + try { + target = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + target = null; } - const pieces = board.pieces; - const transformChoices = ['N','B','R']; - const mapping = []; - for(let i = 0; i < pieces.length; i++){ - const p = pieces[i]; - if(!p || !p.type) continue; - const t = ('' + p.type).toUpperCase(); - if(t === 'P'){ - const choice = transformChoices[Math.floor(Math.random() * transformChoices.length)]; - p.type = choice; - if(p.promoted) try{ delete p.promoted; }catch(_){ } - mapping.push({ id: p.id, from: 'P', to: choice }); - } else if(t === 'N' || t === 'B' || t === 'R'){ - p.type = 'P'; - if(p.promoted) try{ delete p.promoted; }catch(_){ } - mapping.push({ id: p.id, from: t, to: 'P' }); + } + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const targetPiece = ((board && board.pieces) || []).find( + (p) => p.square === target, + ); + if ( + !board || + !target || + !targetPiece || + targetPiece.color !== playerColorShort + ) { + 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) { + console.error("restore removed card error", e); + } + return ( + cb && + cb({ + error: "no valid target", + message: "Aucune cible valide n'a été sélectionnée.", + }) + ); + } + function neighbors(sq) { + if (!sq) return []; + const m = String(sq) + .toLowerCase() + .match(/^([a-z])([0-9]+)$/i); + if (!m) return []; + const file = m[1].charCodeAt(0) - "a".charCodeAt(0); + const rank = parseInt(m[2], 10) - 1; + const w = board.width || 8, + h = board.height || 8; + const out = []; + for (let dx = -1; dx <= 1; dx++) + for (let dy = -1; dy <= 1; dy++) { + const nx = file + dx, + ny = rank + dy; + if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue; + out.push( + String.fromCharCode("a".charCodeAt(0) + nx) + (ny + 1), + ); + } + return out; + } + const affected = neighbors(target); + const removedPieces = []; + for (let i = (board.pieces || []).length - 1; i >= 0; i--) { + const p = board.pieces[i]; + if (p && affected.indexOf(p.square) !== -1) { + const cp = board.pieces.splice(i, 1)[0]; + try { + room.captured = room.captured || []; + const originalOwner = (room.players || []).find( + (pl) => (pl.color && pl.color[0]) === cp.color, + ); + room.captured.push({ + id: uuidv4().slice(0, 8), + piece: cp, + originalOwnerId: (originalOwner && originalOwner.id) || null, + capturedBy: senderId, + ts: Date.now(), + }); + } catch (_) {} + removedPieces.push({ + id: cp.id, + square: cp.square, + type: cp.type, + color: cp.color, + }); + } + } + board.version = (board.version || 0) + 1; + const effect = { + id: played.id, + type: "kamikaz", + playerId: senderId, + targetSquare: target, + affectedSquares: affected, + removed: removedPieces, + ts: Date.now(), + }; + room.activeCardEffects = room.activeCardEffects || []; + room.activeCardEffects.push(effect); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + try { + io.to(room.id).emit("mine:detonated", { + roomId: room.id, + square: target, + }); + } catch (_) {} + played.payload = Object.assign({}, payload, { + applied: "kamikaz", + appliedTo: target, + affected: affected, + removedCount: removedPieces.length, + }); + try { + const end = checkAndHandleVictory(room); + if (end && end.over) { + try { + sendRoomUpdate(room); + } catch (_) {} + } else { + if (board) { + board.turn = board.turn === "w" ? "b" : "w"; + try { + recordPlayerDrewPrev(room, senderId); + } catch (_) {} + const nextColor = board.turn; + const nextPlayer = (room.players || []).find( + (p) => (p.color && p.color[0]) === nextColor, + ); + if (nextPlayer) { + try { + maybeDrawAtTurnStart(room, nextPlayer.id); + } catch (_) {} + } } } - board.version = (board.version || 0) + 1; - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect: { id: played.id, type: 'revolution', mapping } }); }catch(_){ } - played.payload = Object.assign({}, payload, { applied: 'revolution', mapping }); - }catch(e){ console.error('revolution effect error', e); } + } catch (_) {} + } catch (e) { + console.error("kamikaz effect error", e); } + } - // invisible - else if(cardId === 'invisible'){ - try{ - const board = room.boardState; - let target = payload && payload.targetSquare; - if(!target){ try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } } - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const targetPiece = (board && board.pieces || []).find(p => p.square === target); - if(!board || !target || !targetPiece || targetPiece.color !== playerColorShort){ - 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){ console.error('restore removed card error', e); } - return cb && cb({ error: 'no valid target', message: "Aucune cible valide n'a été sélectionnée." }); + // coin coin + else if (cardId === "coincoin") { + try { + const board = room.boardState; + let source = payload && payload.targetSquare; + if (!source) { + try { + source = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + source = null; } - room.activeCardEffects = room.activeCardEffects || []; - try{ targetPiece.invisible = true; }catch(_){ } - const effect = { id: played.id, type: 'invisible', pieceId: targetPiece.id, pieceSquare: target, playerId: senderId, ts: Date.now() }; - room.activeCardEffects.push(effect); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } - played.payload = Object.assign({}, payload, { applied: 'invisible', appliedTo: target }); - }catch(e){ console.error('invisible effect error', e); } + } + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const piece = ((board && board.pieces) || []).find( + (p) => p.square === source, + ); + const w = (board && board.width) || 8; + const h = (board && board.height) || 8; + const left = "a"; + const right = String.fromCharCode("a".charCodeAt(0) + (w - 1)); + const corners = [ + left + "1", + left + String(h), + right + "1", + right + String(h), + ]; + if ( + !board || + !source || + !piece || + piece.color !== playerColorShort || + corners.indexOf(source) === -1 + ) { + 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) { + console.error("restore removed card error", e); + } + return ( + cb && + cb({ + error: "no valid corner piece selected", + message: "Aucun coin valide n'a été sélectionné.", + }) + ); + } + const emptyCorners = corners.filter((c) => { + return !(board.pieces || []).some((p) => p.square === c); + }); + const destChoices = emptyCorners.filter((c) => c !== source); + if (!destChoices || destChoices.length === 0) { + 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) { + console.error("restore removed card error", e); + } + return ( + cb && + cb({ + error: "no empty destination corner available", + message: "Aucun coin de destination vide n'est disponible.", + }) + ); + } + const effect = { + id: played.id, + type: "coincoin", + playerId: senderId, + pieceId: piece.id, + pieceSquare: source, + allowedSquares: destChoices.slice(0), + remainingTurns: 1, + ts: Date.now(), + }; + room.activeCardEffects = room.activeCardEffects || []; + room.activeCardEffects.push(effect); + board.version = (board.version || 0) + 1; + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + played.payload = Object.assign({}, payload, { + applied: "coincoin", + from: source, + allowed: destChoices.slice(0), + }); + } catch (e) { + console.error("coincoin effect error", e); } + } + + // mélange + else if (cardId === "melange") { + try { + const board = room.boardState; + if ( + !board || + !Array.isArray(board.pieces) || + board.pieces.length === 0 + ) { + 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 (_) {} + return ( + cb && + cb({ + error: "no pieces to shuffle", + message: "Aucune pièce à mélanger.", + }) + ); + } + const pieces = board.pieces; + const w = board.width || 8; + const h = board.height || 8; + const allSquares = []; + for (let yy = 0; yy < h; yy++) { + for (let xx = 0; xx < w; xx++) { + allSquares.push( + String.fromCharCode("a".charCodeAt(0) + xx) + (yy + 1), + ); + } + } + if (allSquares.length < pieces.length) { + played.payload = Object.assign({}, payload, { + applied: "melange_failed", + reason: "board_too_small", + }); + } else { + const occupiedByBlack = {}; + pieces.forEach((p) => { + try { + if (p && p.color === "b" && p.square) { + occupiedByBlack[p.square] = true; + } + } catch (_) {} + }); + const occupiedByWhite = {}; + pieces.forEach((p) => { + try { + if (p && p.color === "w" && p.square) { + occupiedByWhite[p.square] = true; + } + } catch (_) {} + }); + const blackSquares = Object.keys(occupiedByBlack); + for (let i = blackSquares.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const tmp = blackSquares[i]; + blackSquares[i] = blackSquares[j]; + blackSquares[j] = tmp; + } + const newBlackSquareByPieceId = {}; + let blackIndex = 0; + pieces.forEach((p) => { + try { + if (p && p.color === "b") { + newBlackSquareByPieceId[p.id] = + blackSquares[blackIndex] || p.square; + blackIndex++; + } + } catch (_) {} + }); + const whiteSquares = Object.keys(occupiedByWhite); + for (let i = whiteSquares.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const tmp = whiteSquares[i]; + whiteSquares[i] = whiteSquares[j]; + whiteSquares[j] = tmp; + } + const newWhiteSquareByPieceId = {}; + let whiteIndex = 0; + pieces.forEach((p) => { + try { + if (p && p.color === "w") { + newWhiteSquareByPieceId[p.id] = + whiteSquares[whiteIndex] || p.square; + whiteIndex++; + } + } catch (_) {} + }); + pieces.forEach((p) => { + try { + if (p && p.color === "b" && newBlackSquareByPieceId[p.id]) { + p.square = newBlackSquareByPieceId[p.id]; + } else if ( + p && + p.color === "w" && + newWhiteSquareByPieceId[p.id] + ) { + p.square = newWhiteSquareByPieceId[p.id]; + } + } catch (_) {} + }); + board.version = (board.version || 0) + 1; + const effect = { + id: played.id, + type: "melange", + playerId: senderId, + ts: Date.now(), + }; + room.activeCardEffects = room.activeCardEffects || []; + room.activeCardEffects.push(effect); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + played.payload = Object.assign({}, payload, { applied: "melange" }); + } + } catch (e) { + console.error("melange error", e); + } + } + + // révolution + else if (cardId === "revolution") { + try { + const board = room.boardState; + if ( + !board || + !Array.isArray(board.pieces) || + board.pieces.length === 0 + ) { + 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 (_) {} + return ( + cb && + cb({ + error: "no pieces to transform", + message: "Aucune pièce à transformer.", + }) + ); + } + const pieces = board.pieces; + const transformChoices = ["N", "B", "R"]; + const mapping = []; + for (let i = 0; i < pieces.length; i++) { + const p = pieces[i]; + if (!p || !p.type) continue; + const t = ("" + p.type).toUpperCase(); + if (t === "P") { + const choice = + transformChoices[ + Math.floor(Math.random() * transformChoices.length) + ]; + p.type = choice; + if (p.promoted) + try { + delete p.promoted; + } catch (_) {} + mapping.push({ id: p.id, from: "P", to: choice }); + } else if (t === "N" || t === "B" || t === "R") { + p.type = "P"; + if (p.promoted) + try { + delete p.promoted; + } catch (_) {} + mapping.push({ id: p.id, from: t, to: "P" }); + } + } + board.version = (board.version || 0) + 1; + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect: { id: played.id, type: "revolution", mapping }, + }); + } catch (_) {} + played.payload = Object.assign({}, payload, { + applied: "revolution", + mapping, + }); + } catch (e) { + console.error("revolution effect error", e); + } + } + + // invisible + else if (cardId === "invisible") { + try { + const board = room.boardState; + let target = payload && payload.targetSquare; + if (!target) { + try { + target = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + target = null; + } + } + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const targetPiece = ((board && board.pieces) || []).find( + (p) => p.square === target, + ); + if ( + !board || + !target || + !targetPiece || + targetPiece.color !== playerColorShort + ) { + 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) { + console.error("restore removed card error", e); + } + return ( + cb && + cb({ + error: "no valid target", + message: "Aucune cible valide n'a été sélectionnée.", + }) + ); + } + room.activeCardEffects = room.activeCardEffects || []; + try { + targetPiece.invisible = true; + } catch (_) {} + const effect = { + id: played.id, + type: "invisible", + pieceId: targetPiece.id, + pieceSquare: target, + playerId: senderId, + ts: Date.now(), + }; + room.activeCardEffects.push(effect); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + played.payload = Object.assign({}, payload, { + applied: "invisible", + appliedTo: target, + }); + } catch (e) { + console.error("invisible effect error", e); + } + } // brouillard - else if(cardId === 'brouillard'){ - try{ - const board = room.boardState; - 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){ - 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', message: "Aucun joueur cible valide n'a été sélectionné." }); - } - 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)); + else if (cardId === "brouillard") { + try { + const board = room.boardState; + 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) { + 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; } } - room.activeCardEffects = room.activeCardEffects || []; - 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 - else if(cardId === 'anneau'){ - try{ - room.activeCardEffects = room.activeCardEffects || []; - const effect = { id: played.id, type: 'anneau', playerId: senderId, ts: Date.now(), remainingTurns: (payload && payload.turns) || 1 }; - room.activeCardEffects.push(effect); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){} - played.payload = Object.assign({}, payload, { applied: 'anneau' }); - }catch(e){ console.error('anneau effect error', e); } - } - - // totem - else if(cardId === 'totem'){ - try{ - room.activeCardEffects = room.activeCardEffects || []; - const effect = { id: played.id, type: 'totem', playerId: senderId, ts: Date.now() }; - room.activeCardEffects.push(effect); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } - played.payload = Object.assign({}, payload, { applied: 'totem', appliedToPlayer: senderId }); - }catch(e){ console.error('totem effect error', e); } - } - - //mine - else if(cardId === 'mine'){ - try{ - const board = room.boardState; - let target = payload && payload.targetSquare; - if(!target){ try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } } - const targetOccupied = (board && board.pieces || []).find(p => p.square === target); - if(!board || !target || targetOccupied){ - played.payload = Object.assign({}, payload, { applied: 'mine_failed', attemptedTo: target }); - try{ const owner = (room.players || []).find(p => p.id === senderId); if(owner && owner.socketId) io.to(owner.socketId).emit('card:effect:applied', { roomId: room.id, effect: { id: played.id, type: 'mine_failed', playerId: senderId, square: target, ts: Date.now() } }); }catch(_){ } - } else { - room.activeCardEffects = room.activeCardEffects || []; - const effect = { id: played.id, type: 'mine', playerId: senderId, square: target, ts: Date.now() }; - room.activeCardEffects.push(effect); - try{ - const owner = (room.players || []).find(p => p.id === senderId); - if(owner && owner.socketId) io.to(owner.socketId).emit('card:effect:applied', { roomId: room.id, effect }); - }catch(_){ } - played.payload = Object.assign({}, payload, { applied: 'mine' }); + } catch (e) {} + return ( + cb && + cb({ + error: "no valid target player", + message: "Aucun joueur cible valide n'a été sélectionné.", + }) + ); } - }catch(e){ console.error('mine placement error', e); } + 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)); + } + } + room.activeCardEffects = room.activeCardEffects || []; + 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); } + } - // jouer deux fois - else if(cardId === 'double'){ - try{ + // anneau + else if (cardId === "anneau") { + try { + room.activeCardEffects = room.activeCardEffects || []; + const effect = { + id: played.id, + type: "anneau", + playerId: senderId, + ts: Date.now(), + remainingTurns: (payload && payload.turns) || 1, + }; + room.activeCardEffects.push(effect); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + played.payload = Object.assign({}, payload, { applied: "anneau" }); + } catch (e) { + console.error("anneau effect error", e); + } + } + + // totem + else if (cardId === "totem") { + try { + room.activeCardEffects = room.activeCardEffects || []; + const effect = { + id: played.id, + type: "totem", + playerId: senderId, + ts: Date.now(), + }; + room.activeCardEffects.push(effect); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + played.payload = Object.assign({}, payload, { + applied: "totem", + appliedToPlayer: senderId, + }); + } catch (e) { + console.error("totem effect error", e); + } + } + + //mine + else if (cardId === "mine") { + try { + const board = room.boardState; + let target = payload && payload.targetSquare; + if (!target) { + try { + target = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + target = null; + } + } + const targetOccupied = ((board && board.pieces) || []).find( + (p) => p.square === target, + ); + if (!board || !target || targetOccupied) { + played.payload = Object.assign({}, payload, { + applied: "mine_failed", + attemptedTo: target, + }); + try { + const owner = (room.players || []).find((p) => p.id === senderId); + if (owner && owner.socketId) + io.to(owner.socketId).emit("card:effect:applied", { + roomId: room.id, + effect: { + id: played.id, + type: "mine_failed", + playerId: senderId, + square: target, + ts: Date.now(), + }, + }); + } catch (_) {} + } else { room.activeCardEffects = room.activeCardEffects || []; - const effect = { id: played.id, type: 'double', playerId: senderId, ts: Date.now(), remainingMoves: (payload && payload.moves) || 2 }; + const effect = { + id: played.id, + type: "mine", + playerId: senderId, + square: target, + ts: Date.now(), + }; room.activeCardEffects.push(effect); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } - played.payload = Object.assign({}, payload, { applied: 'double', moves: effect.remainingMoves }); - }catch(e){ console.error('double effect error', e); } + try { + const owner = (room.players || []).find((p) => p.id === senderId); + if (owner && owner.socketId) + io.to(owner.socketId).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + played.payload = Object.assign({}, payload, { applied: "mine" }); + } + } catch (e) { + console.error("mine placement error", e); } + } - // tous les même - else if(cardId === 'pareil'){ - try{ - room.activeCardEffects = room.activeCardEffects || []; - const effect = { id: played.id, type: 'tous_memes', playerId: senderId, ts: Date.now(), remainingTurns: (payload && payload.turns) || 2, decrementOn: 'opponent' }; - room.activeCardEffects.push(effect); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } - played.payload = Object.assign({}, payload, { applied: 'tous_memes', appliedToPlayer: senderId }); - }catch(e){ console.error('tous_memes effect error', e); } + // jouer deux fois + else if (cardId === "double") { + try { + room.activeCardEffects = room.activeCardEffects || []; + const effect = { + id: played.id, + type: "double", + playerId: senderId, + ts: Date.now(), + remainingMoves: (payload && payload.moves) || 2, + }; + room.activeCardEffects.push(effect); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + played.payload = Object.assign({}, payload, { + applied: "double", + moves: effect.remainingMoves, + }); + } catch (e) { + console.error("double effect error", e); } + } - // vole de pièce - else if(cardId === 'vole_piece'){ - try{ - const board = room.boardState; - let target = payload && payload.targetSquare; - if(!target){ try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } } - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const targetPiece = (board && board.pieces || []).find(p => p.square === target); - if(!board || !target || !targetPiece || targetPiece.color === playerColorShort || (targetPiece.type && String(targetPiece.type).toLowerCase() === 'k')){ - played.payload = Object.assign({}, payload, { applied: 'steal_failed', attemptedTo: target }); - try{ const owner = (room.players || []).find(p => p.id === senderId); if(owner && owner.socketId) io.to(owner.socketId).emit('card:effect:applied', { roomId: room.id, effect: { id: played.id, type: 'steal_failed', playerId: senderId, square: target, ts: Date.now() } }); }catch(_){ } - } else { + // tous les même + else if (cardId === "pareil") { + try { + room.activeCardEffects = room.activeCardEffects || []; + const effect = { + id: played.id, + type: "tous_memes", + playerId: senderId, + ts: Date.now(), + remainingTurns: (payload && payload.turns) || 2, + decrementOn: "opponent", + }; + room.activeCardEffects.push(effect); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + played.payload = Object.assign({}, payload, { + applied: "tous_memes", + appliedToPlayer: senderId, + }); + } catch (e) { + console.error("tous_memes effect error", e); + } + } + + // vole de pièce + else if (cardId === "vole_piece") { + try { + const board = room.boardState; + let target = payload && payload.targetSquare; + if (!target) { + try { + target = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + target = null; + } + } + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const targetPiece = ((board && board.pieces) || []).find( + (p) => p.square === target, + ); + if ( + !board || + !target || + !targetPiece || + targetPiece.color === playerColorShort || + (targetPiece.type && String(targetPiece.type).toLowerCase() === "k") + ) { + played.payload = Object.assign({}, payload, { + applied: "steal_failed", + attemptedTo: target, + }); + try { + const owner = (room.players || []).find((p) => p.id === senderId); + if (owner && owner.socketId) + io.to(owner.socketId).emit("card:effect:applied", { + roomId: room.id, + effect: { + id: played.id, + type: "steal_failed", + playerId: senderId, + square: target, + ts: Date.now(), + }, + }); + } catch (_) {} + } else { const oldColor = targetPiece.color; targetPiece.color = playerColorShort; room.activeCardEffects = room.activeCardEffects || []; - const originalOwner = (room.players || []).find(p => (p.color && p.color[0]) === oldColor); - const effect = { id: played.id, type: 'steal', pieceId: targetPiece.id, pieceSquare: target, fromColor: oldColor, toPlayerId: senderId, originalOwnerId: originalOwner && originalOwner.id, playerId: senderId, ts: Date.now() }; + const originalOwner = (room.players || []).find( + (p) => (p.color && p.color[0]) === oldColor, + ); + const effect = { + id: played.id, + type: "steal", + pieceId: targetPiece.id, + pieceSquare: target, + fromColor: oldColor, + toPlayerId: senderId, + originalOwnerId: originalOwner && originalOwner.id, + playerId: senderId, + ts: Date.now(), + }; room.activeCardEffects.push(effect); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} - played.payload = Object.assign({}, payload, { applied: 'steal', appliedTo: target, fromColor: oldColor }); - try{ - if(board){ - board.turn = (board.turn === 'w') ? 'b' : 'w'; - try{ recordPlayerDrewPrev(room, senderId); }catch(_){ } + played.payload = Object.assign({}, payload, { + applied: "steal", + appliedTo: target, + fromColor: oldColor, + }); + try { + if (board) { + board.turn = board.turn === "w" ? "b" : "w"; + try { + recordPlayerDrewPrev(room, senderId); + } catch (_) {} // draw for the next player at the start of their turn const nextColor = board.turn; - const nextPlayer = (room.players || []).find(p => (p.color && p.color[0]) === nextColor); - if(nextPlayer){ try{ maybeDrawAtTurnStart(room, nextPlayer.id); }catch(_){ } } + const nextPlayer = (room.players || []).find( + (p) => (p.color && p.color[0]) === nextColor, + ); + if (nextPlayer) { + try { + maybeDrawAtTurnStart(room, nextPlayer.id); + } catch (_) {} + } } - }catch(_){ } - } - }catch(e){ console.error('steal effect error', e); } + } catch (_) {} + } + } catch (e) { + console.error("steal effect error", e); } + } - // vole d'une carte - else if(cardId === 'vole_carte'){ - try{ - 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(!targetPlayer || targetPlayer.id === senderId){ - played.payload = Object.assign({}, payload, { applied: 'steal_card_failed', attemptedTo: targetPlayerId }); - try{ const owner = (room.players || []).find(p => p.id === senderId); if(owner && owner.socketId) io.to(owner.socketId).emit('card:effect:applied', { roomId: room.id, effect: { id: played.id, type: 'steal_card_failed', playerId: senderId, targetPlayerId, ts: Date.now() } }); }catch(_){ } + // vole d'une carte + else if (cardId === "vole_carte") { + try { + 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 (!targetPlayer || targetPlayer.id === senderId) { + played.payload = Object.assign({}, payload, { + applied: "steal_card_failed", + attemptedTo: targetPlayerId, + }); + try { + const owner = (room.players || []).find((p) => p.id === senderId); + if (owner && owner.socketId) + io.to(owner.socketId).emit("card:effect:applied", { + roomId: room.id, + effect: { + id: played.id, + type: "steal_card_failed", + playerId: senderId, + targetPlayerId, + ts: Date.now(), + }, + }); + } catch (_) {} + } else { + room.hands = room.hands || {}; + const victimHand = room.hands[targetPlayerId] || []; + if (!victimHand || victimHand.length === 0) { + played.payload = Object.assign({}, payload, { + applied: "steal_card_failed_empty", + attemptedTo: targetPlayerId, + }); + try { + const owner = (room.players || []).find( + (p) => p.id === senderId, + ); + if (owner && owner.socketId) + io.to(owner.socketId).emit("card:effect:applied", { + roomId: room.id, + effect: { + id: played.id, + type: "steal_card_failed_empty", + playerId: senderId, + targetPlayerId, + ts: Date.now(), + }, + }); + } catch (_) {} } else { - room.hands = room.hands || {}; - const victimHand = room.hands[targetPlayerId] || []; - if(!victimHand || victimHand.length === 0){ - played.payload = Object.assign({}, payload, { applied: 'steal_card_failed_empty', attemptedTo: targetPlayerId }); - try{ const owner = (room.players || []).find(p => p.id === senderId); if(owner && owner.socketId) io.to(owner.socketId).emit('card:effect:applied', { roomId: room.id, effect: { id: played.id, type: 'steal_card_failed_empty', playerId: senderId, targetPlayerId, ts: Date.now() } }); }catch(_){ } - } else { - const idx = Math.floor(Math.random() * victimHand.length); - const stolen = victimHand.splice(idx,1)[0]; - room.hands[senderId] = room.hands[senderId] || []; - room.hands[senderId].push(stolen); - played.payload = Object.assign({}, payload, { applied: 'steal_card', stolenFrom: targetPlayerId, stolenCardId: stolen.cardId || stolen.id }); - try{ const stealer = (room.players || []).find(p => p.id === senderId); if(stealer && stealer.socketId) io.to(stealer.socketId).emit('card:stolen', { roomId: room.id, from: targetPlayerId, card: stolen }); }catch(_){ } - try{ const victim = (room.players || []).find(p => p.id === targetPlayerId); if(victim && victim.socketId) io.to(victim.socketId).emit('card:lost', { roomId: room.id, lostCount: 1 }); }catch(_){ } - } + const idx = Math.floor(Math.random() * victimHand.length); + const stolen = victimHand.splice(idx, 1)[0]; + room.hands[senderId] = room.hands[senderId] || []; + room.hands[senderId].push(stolen); + played.payload = Object.assign({}, payload, { + applied: "steal_card", + stolenFrom: targetPlayerId, + stolenCardId: stolen.cardId || stolen.id, + }); + try { + const stealer = (room.players || []).find( + (p) => p.id === senderId, + ); + if (stealer && stealer.socketId) + io.to(stealer.socketId).emit("card:stolen", { + roomId: room.id, + from: targetPlayerId, + card: stolen, + }); + } catch (_) {} + try { + const victim = (room.players || []).find( + (p) => p.id === targetPlayerId, + ); + if (victim && victim.socketId) + io.to(victim.socketId).emit("card:lost", { + roomId: room.id, + lostCount: 1, + }); + } catch (_) {} } - - }catch(e){ console.error('steal-card effect error', e); } } + } catch (e) { + console.error("steal-card effect error", e); + } + } - // carte sans effet - else if(cardId === 'sans_effet'){ - try{ - played.payload = Object.assign({}, payload, { applied: 'no_effect' }); - try{ const owner = (room.players || []).find(p => p.id === senderId); if(owner && owner.socketId) io.to(owner.socketId).emit('card:effect:applied', { roomId: room.id, effect: { id: played.id, type: 'no_effect', playerId: senderId, ts: Date.now() } }); }catch(_){ } - }catch(e){ console.error('no_effect card error', e); } - } + // carte sans effet + else if (cardId === "sans_effet") { + try { + played.payload = Object.assign({}, payload, { applied: "no_effect" }); + try { + const owner = (room.players || []).find((p) => p.id === senderId); + if (owner && owner.socketId) + io.to(owner.socketId).emit("card:effect:applied", { + roomId: room.id, + effect: { + id: played.id, + type: "no_effect", + playerId: senderId, + ts: Date.now(), + }, + }); + } catch (_) {} + } catch (e) { + console.error("no_effect card error", e); + } + } - // resurrection - else if(cardId === 'resurection'){ - try{ - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const available = (room.captured || []).filter(c => c && c.piece && c.piece.color === playerColorShort); - if(!available || available.length === 0){ - played.payload = Object.assign({}, payload, { applied: 'resurrection_failed_no_captured' }); - try{ const owner = (room.players || []).find(p => p.id === senderId); if(owner && owner.socketId) io.to(owner.socketId).emit('card:effect:applied', { roomId: room.id, effect: { id: played.id, type: 'resurrection_failed_no_captured', playerId: senderId, ts: Date.now() } }); }catch(_){ } + // resurrection + else if (cardId === "resurection") { + try { + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const available = (room.captured || []).filter( + (c) => c && c.piece && c.piece.color === playerColorShort, + ); + if (!available || available.length === 0) { + played.payload = Object.assign({}, payload, { + applied: "resurrection_failed_no_captured", + }); + try { + const owner = (room.players || []).find((p) => p.id === senderId); + if (owner && owner.socketId) + io.to(owner.socketId).emit("card:effect:applied", { + roomId: room.id, + effect: { + id: played.id, + type: "resurrection_failed_no_captured", + playerId: senderId, + ts: Date.now(), + }, + }); + } catch (_) {} + } else { + const selectedId = + payload && + (payload.captureId || + payload.capturedId || + payload.targetCapturedId || + payload.selectedCapturedId); + let capturedEntry = null; + if (selectedId) { + const idx = (room.captured || []).findIndex( + (c) => + c && + c.id === selectedId && + c.piece && + c.piece.color === playerColorShort, + ); + if (idx !== -1) capturedEntry = room.captured[idx]; + } + if (!capturedEntry) { + capturedEntry = available[available.length - 1]; + } + if (!capturedEntry) { + played.payload = Object.assign({}, payload, { + applied: "resurrection_failed_no_valid", + }); + try { + const owner = (room.players || []).find( + (p) => p.id === senderId, + ); + if (owner && owner.socketId) + io.to(owner.socketId).emit("card:effect:applied", { + roomId: room.id, + effect: { + id: played.id, + type: "resurrection_failed_no_valid", + playerId: senderId, + ts: Date.now(), + }, + }); + } catch (_) {} + } else { + let target = payload && payload.targetSquare; + if (!target) { + try { + target = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + target = null; + } + } + const board = room.boardState; + const occupied = ((board && board.pieces) || []).find( + (p) => p.square === target, + ); + if (!board || !target || occupied) { + played.payload = Object.assign({}, payload, { + applied: "resurrection_failed_bad_square", + attemptedTo: target, + }); + try { + const owner = (room.players || []).find( + (p) => p.id === senderId, + ); + if (owner && owner.socketId) + io.to(owner.socketId).emit("card:effect:applied", { + roomId: room.id, + effect: { + id: played.id, + type: "resurrection_failed_bad_square", + playerId: senderId, + square: target, + ts: Date.now(), + }, + }); + } catch (_) {} } else { - const selectedId = payload && (payload.captureId || payload.capturedId || payload.targetCapturedId || payload.selectedCapturedId); - let capturedEntry = null; - if(selectedId){ - const idx = (room.captured || []).findIndex(c => c && c.id === selectedId && c.piece && c.piece.color === playerColorShort); - if(idx !== -1) capturedEntry = room.captured[idx]; - } - if(!capturedEntry){ - capturedEntry = available[available.length - 1]; - } - if(!capturedEntry){ - played.payload = Object.assign({}, payload, { applied: 'resurrection_failed_no_valid' }); - try{ const owner = (room.players || []).find(p => p.id === senderId); if(owner && owner.socketId) io.to(owner.socketId).emit('card:effect:applied', { roomId: room.id, effect: { id: played.id, type: 'resurrection_failed_no_valid', playerId: senderId, ts: Date.now() } }); }catch(_){ } - } else { - let target = payload && payload.targetSquare; - if(!target){ try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } } - const board = room.boardState; - const occupied = (board && board.pieces || []).find(p => p.square === target); - if(!board || !target || occupied){ - played.payload = Object.assign({}, payload, { applied: 'resurrection_failed_bad_square', attemptedTo: target }); - try{ const owner = (room.players || []).find(p => p.id === senderId); if(owner && owner.socketId) io.to(owner.socketId).emit('card:effect:applied', { roomId: room.id, effect: { id: played.id, type: 'resurrection_failed_bad_square', playerId: senderId, square: target, ts: Date.now() } }); }catch(_){ } - } else { - for(let i = room.captured.length - 1; i >= 0; i--){ if(room.captured[i] && room.captured[i].id === capturedEntry.id){ room.captured.splice(i,1); break; } } - const orig = capturedEntry.piece || {}; - const newPiece = Object.assign({}, orig); - newPiece.id = ((playerColorShort === 'w') ? 'w_' : 'b_') + (newPiece.type || 'P') + '_' + uuidv4().slice(0,6); - newPiece.square = target; - newPiece.color = playerColorShort; - if(newPiece.promoted) newPiece.promoted = true; - board.pieces = board.pieces || []; - board.pieces.push(newPiece); - const effect = { id: played.id, type: 'resurrection', pieceId: newPiece.id, pieceType: newPiece.type, placedAt: target, playerId: senderId, ts: Date.now() }; - room.activeCardEffects = room.activeCardEffects || []; - room.activeCardEffects.push(effect); - try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } - played.payload = Object.assign({}, payload, { applied: 'resurrection', appliedTo: target, resurrectedId: newPiece.id, resurrectedType: newPiece.type }); + for (let i = room.captured.length - 1; i >= 0; i--) { + if ( + room.captured[i] && + room.captured[i].id === capturedEntry.id + ) { + room.captured.splice(i, 1); + break; } } + const orig = capturedEntry.piece || {}; + const newPiece = Object.assign({}, orig); + newPiece.id = + (playerColorShort === "w" ? "w_" : "b_") + + (newPiece.type || "P") + + "_" + + uuidv4().slice(0, 6); + newPiece.square = target; + newPiece.color = playerColorShort; + if (newPiece.promoted) newPiece.promoted = true; + board.pieces = board.pieces || []; + board.pieces.push(newPiece); + const effect = { + id: played.id, + type: "resurrection", + pieceId: newPiece.id, + pieceType: newPiece.type, + placedAt: target, + playerId: senderId, + ts: Date.now(), + }; + room.activeCardEffects = room.activeCardEffects || []; + room.activeCardEffects.push(effect); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + played.payload = Object.assign({}, payload, { + applied: "resurrection", + appliedTo: target, + resurrectedId: newPiece.id, + resurrectedType: newPiece.type, + }); } - }catch(e){ console.error('resurrection effect error', e); } + } } + } catch (e) { + console.error("resurrection effect error", e); + } + } // rebond: - else if(cardId === 'rebond'){ - try{ + else if (cardId === "rebond") { + try { const board = room.boardState; let target = payload && payload.targetSquare; - if(!target){ - try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } + if (!target) { + try { + target = socket.data && socket.data.lastSelectedSquare; + } catch (e) { + target = null; + } } - const roomPlayer = room.players.find(p => p.id === senderId); - const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; - const targetPiece = (board && board.pieces || []).find(p => p.square === target); - if(!board || !target || !targetPiece || targetPiece.color !== playerColorShort){ - try{ + const roomPlayer = room.players.find((p) => p.id === senderId); + const playerColorShort = + (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const targetPiece = ((board && board.pieces) || []).find( + (p) => p.square === target, + ); + if ( + !board || + !target || + !targetPiece || + targetPiece.color !== playerColorShort + ) { + try { room.hands = room.hands || {}; room.hands[senderId] = room.hands[senderId] || []; - if(removed) room.hands[senderId].push(removed); + 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){ console.error('restore removed card error', e); } - return cb && cb({ error: 'no valid target', message: "Aucune cible valide n'a été sélectionnée." }); + 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) { + console.error("restore removed card error", e); + } + return ( + cb && + cb({ + error: "no valid target", + message: "Aucune cible valide n'a été sélectionnée.", + }) + ); } room.activeCardEffects = room.activeCardEffects || []; - room.activeCardEffects.push({ id: played.id, type: 'rebondir', pieceId: targetPiece.id, pieceSquare: target, playerId: senderId }); - played.payload = Object.assign({}, payload, { applied: 'rebondir', appliedTo: target }); - }catch(e){ console.error('rebondir effect error', e); } + room.activeCardEffects.push({ + id: played.id, + type: "rebondir", + pieceId: targetPiece.id, + pieceSquare: target, + playerId: senderId, + }); + played.payload = Object.assign({}, payload, { + applied: "rebondir", + appliedTo: target, + }); + } catch (e) { + console.error("rebondir effect error", e); + } } - }catch(e){ - console.error('card:play effect error', e); + } catch (e) { + console.error("card:play effect error", e); } - room.playedCards.push(played); - try{ - const board = room.boardState; - if(room.status === 'playing' && board){ - room._cardPlayedThisTurn = room._cardPlayedThisTurn || {}; - room._cardPlayedThisTurn[played.playerId] = true; + room.playedCards.push(played); + try { + const board = room.boardState; + if (room.status === "playing" && board) { + room._cardPlayedThisTurn = room._cardPlayedThisTurn || {}; + room._cardPlayedThisTurn[played.playerId] = true; + } + } catch (e) { + console.error("mark card played error", e); } - }catch(e){ console.error('mark card played error', e); } - io.to(roomId).emit('card:played', played); - try{ - const countsAsMove = (cardId === 'inversion'); - if(countsAsMove){ - try{ - const board = room.boardState; - if(board){ - board.turn = (board.turn === 'w') ? 'b' : 'w'; - try{ recordPlayerDrewPrev(room, senderId); }catch(_){ } + io.to(roomId).emit("card:played", played); + try { + const countsAsMove = cardId === "inversion"; + if (countsAsMove) { + try { + const board = room.boardState; + if (board) { + board.turn = board.turn === "w" ? "b" : "w"; + try { + recordPlayerDrewPrev(room, senderId); + } catch (_) {} - try{ - room.activeCardEffects = room.activeCardEffects || []; - for(let i = room.activeCardEffects.length - 1; i >= 0; i--){ - const e = room.activeCardEffects[i]; - if(typeof e.remainingTurns === 'number'){ - let shouldDecrement = false; - if(e.decrementOn === 'opponent'){ - shouldDecrement = (e.playerId !== senderId); - } else if(e.decrementOn === 'owner'){ - shouldDecrement = (e.playerId === senderId); - } else { - shouldDecrement = (e.playerId === senderId); - } - if(shouldDecrement){ - e.remainingTurns = e.remainingTurns - 1; - try{ io.to(room.id).emit('card:effect:updated', { roomId: room.id, effect: e }); }catch(_){ } - if(e.remainingTurns <= 0){ - room.activeCardEffects.splice(i,1); - try{ io.to(room.id).emit('card:effect:removed', { roomId: room.id, effectId: e.id, type: e.type, playerId: e.playerId }); }catch(_){ } + try { + room.activeCardEffects = room.activeCardEffects || []; + for (let i = room.activeCardEffects.length - 1; i >= 0; i--) { + const e = room.activeCardEffects[i]; + if (typeof e.remainingTurns === "number") { + let shouldDecrement = false; + if (e.decrementOn === "opponent") { + shouldDecrement = e.playerId !== senderId; + } else if (e.decrementOn === "owner") { + shouldDecrement = e.playerId === senderId; + } else { + shouldDecrement = e.playerId === senderId; + } + if (shouldDecrement) { + e.remainingTurns = e.remainingTurns - 1; + try { + io.to(room.id).emit("card:effect:updated", { + roomId: room.id, + effect: e, + }); + } catch (_) {} + if (e.remainingTurns <= 0) { + room.activeCardEffects.splice(i, 1); + try { + io.to(room.id).emit("card:effect:removed", { + roomId: room.id, + effectId: e.id, + type: e.type, + playerId: e.playerId, + }); + } catch (_) {} + } } } } + } catch (err) { + console.error( + "updating temporary effects error (inversion)", + err, + ); } - }catch(err){ console.error('updating temporary effects error (inversion)', err); } - try{ room._cardPlayedThisTurn = {}; }catch(_){ } + try { + room._cardPlayedThisTurn = {}; + } catch (_) {} - try{ - const nextColor = board.turn; - const nextPlayer = room.players.find(p => (p.color && p.color[0]) === nextColor); - if(nextPlayer){ - maybeDrawAtTurnStart(room, nextPlayer.id); - } else { + try { + const nextColor = board.turn; + const nextPlayer = room.players.find( + (p) => (p.color && p.color[0]) === nextColor, + ); + if (nextPlayer) { + maybeDrawAtTurnStart(room, nextPlayer.id); + } else { + sendRoomUpdate(room); + } + } catch (e) { sendRoomUpdate(room); } - }catch(e){ sendRoomUpdate(room); } - } else { + } else { + sendRoomUpdate(room); + } + } catch (err) { + console.error("inversion end-of-turn handling error", err); sendRoomUpdate(room); } - }catch(err){ console.error('inversion end-of-turn handling error', err); sendRoomUpdate(room); } - } else { - room._freeMoveFor = played.playerId; // client may use this flag to enable a free move UI - sendRoomUpdate(room); - try{ io.to(room.id).emit('card:free_move_allowed', { roomId: room.id, playerId: played.playerId }); }catch(_){ } + } else { + room._freeMoveFor = played.playerId; // client may use this flag to enable a free move UI + sendRoomUpdate(room); + try { + io.to(room.id).emit("card:free_move_allowed", { + roomId: room.id, + playerId: played.playerId, + }); + } catch (_) {} + } + } catch (e) { + /* fallback */ sendRoomUpdate(room); } - }catch(e){ /* fallback */ sendRoomUpdate(room); } cb && cb({ ok: true, played }); }); @@ -2528,16 +4946,22 @@ server.listen(PORT, () => { }); setInterval(() => { - try{ - for(const [id, room] of rooms.entries()){ - try{ - if(!room) continue; - if(room.status === 'finished') continue; + try { + for (const [id, room] of rooms.entries()) { + try { + if (!room) continue; + if (room.status === "finished") continue; const res = checkAndHandleVictory(room); - if(res && res.over){ - try{ sendRoomUpdate(room); }catch(_){ } + if (res && res.over) { + try { + sendRoomUpdate(room); + } catch (_) {} } - }catch(e){ console.error('periodic room check error for', id, e); } + } catch (e) { + console.error("periodic room check error for", id, e); + } } - }catch(e){ console.error('periodic victory sweep error', e); } + } catch (e) { + console.error("periodic victory sweep error", e); + } }, 1000);