diff --git a/public/game.html b/public/game.html
index 3c84a3e..373ae31 100644
--- a/public/game.html
+++ b/public/game.html
@@ -98,6 +98,8 @@
if(id.indexOf('toucher') !== -1) return 'enemy';
// la parrure: select an enemy queen to downgrade to a pawn
if(id.indexOf('parrure') !== -1) return 'enemy_queen';
+ // tout ou rien: select a piece (any, except kings) to force it to only move when capturing
+ if(id.indexOf('tout') !== -1 && id.indexOf('rien') !== -1) return 'piece';
// kamikaz targets one of your pieces (play card, then choose a piece)
if(id.indexOf('kamikaz') !== -1) return 'owned';
if(id.indexOf('invis') !== -1 || id.indexOf('invisible') !== -1) return 'owned';
@@ -577,6 +579,16 @@
});
pendingCard = null; clearTargetHighlights(); return;
}
+ else if(req === 'piece'){
+ const occ = currentBoardState && Array.isArray(currentBoardState.pieces) && currentBoardState.pieces.find(p => p.square === square);
+ if(!occ){ log('select a piece'); return; }
+ if(occ.type && occ.type.toLowerCase() === 'k'){ log('cannot select king'); return; }
+ socket.emit('card:play', { roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { targetSquare: square } }, (resp)=>{
+ if(resp && resp.error){ log('card:play error', resp); alert(resp.error); }
+ else { log('card played with target', resp); }
+ });
+ pendingCard = null; clearTargetHighlights(); return;
+ }
}
// If this square contains a move-marker (either clicked marker or square area), attempt the move.
@@ -763,6 +775,9 @@
// initial phase: highlight only own pieces (client will switch to enemy after first selection)
const occ = (currentBoardState.pieces||[]).find(p=>p.square===sq);
if(occ && occ.color === myShort && occ.type && occ.type.toLowerCase() !== 'k') sqEl.classList.add('targetable');
+ } else if(type === 'piece'){
+ const occ = (currentBoardState.pieces||[]).find(p=>p.square===sq);
+ if(occ && occ.type && occ.type.toLowerCase() !== 'k') sqEl.classList.add('targetable');
} else if(type === 'enemy_queen'){
const occ = (currentBoardState.pieces||[]).find(p=>p.square===sq);
if(occ && occ.color && occ.color !== myShort && occ.type && occ.type.toLowerCase() === 'q') sqEl.classList.add('targetable');
diff --git a/public/styles/style.css b/public/styles/style.css
index 6280620..58e2a37 100644
--- a/public/styles/style.css
+++ b/public/styles/style.css
@@ -218,7 +218,7 @@ button:hover{transform:translateY(-3px);transition:all 180ms ease}
.pokemon-hand-counts{display:flex;gap:6px;align-items:center}
.opponent-count{background:rgba(11,18,32,0.03);padding:4px 8px;border-radius:8px;font-size:12px}
.cards-row{display:flex;gap:12px;overflow-x:auto;padding-bottom:6px}
-.pokemon-card{flex:0 0 180px;height:300px;border-radius:12px;background:linear-gradient(180deg,#fff,#f6f8ff);box-shadow:0 10px 24px rgba(11,18,32,0.06);border:1px solid rgba(11,18,32,0.04);display:flex;flex-direction:column;cursor:pointer}
+.pokemon-card{flex:0 0 180px;height:350px;border-radius:12px;background:linear-gradient(180deg,#fff,#f6f8ff);box-shadow:0 10px 24px rgba(11,18,32,0.06);border:1px solid rgba(11,18,32,0.04);display:flex;flex-direction:column;cursor:pointer}
.pokemon-card-top{flex:0 0 120px;padding:8px}
.pokemon-card-art{width:100%;height:100%;background:linear-gradient(135deg,#f0f9ff,#eef2ff);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#475569;font-weight:700}
.pokemon-card-mid{padding:8px;flex:1 1 auto;display:flex;flex-direction:column;gap:6px}
diff --git a/server.js b/server.js
index 181b9cd..56fdb52 100644
--- a/server.js
+++ b/server.js
@@ -225,7 +225,7 @@ function buildDefaultDeck(){
['mélange','La position de toutes les pièces sont échangées aléatoirement'],
['la parrure','Une reine est dégradée en pion'],
// ['tricherie','Choisis une carte de la pioche parmis trois'],
- // ['tout ou rien','Une pièce choisie ne peut maintenant se déplacer que si elle capture.'],
+ ['tout ou rien','Une pièce choisie ne peut maintenant se déplacer que si elle capture.'],
// ['tous les mêmes','Au yeux de l ennemie, toutes les pièces se ressemblent pendant 2 tours.'],
// ['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.'],
// ['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.'],
@@ -799,6 +799,19 @@ io.on('connection', (socket) => {
}
}catch(_){ }
+ // Enforce 'tout ou rien' restriction: if the moving piece is under 'tout_ou_rien', it may only move if the move captures
+ try{
+ const effects2 = room.activeCardEffects || [];
+ const tout = effects2.find(e => e && e.type === 'tout_ou_rien' && e.pieceId === moving.id);
+ if(tout){
+ // only allow moves that capture an occupied square
+ const targetIndexCheck = pieces.findIndex(p => p.square === to);
+ if(targetIndexCheck === -1){
+ return cb && cb({ error: 'must_capture_to_move' });
+ }
+ }
+ }catch(_){ }
+
// validate that 'to' is among legal moves
const legal = computeLegalMoves(room, from) || [];
const ok = legal.some(m => m.to === to);
@@ -896,10 +909,22 @@ io.on('connection', (socket) => {
// advance board version
board.version = (board.version || 0) + 1;
+ // If the player was granted a free move by playing a card just now (room._freeMoveFor), consume it
+ // and treat it like a consumed 'double_move' (i.e. do NOT flip the turn). This allows the player to
+ // perform a single free piece move after playing a card without consuming their main turn.
+ let consumedDoubleMove = false;
+ try{
+ if(room && room._freeMoveFor && room._freeMoveFor === senderId){
+ // consume the free-move token
+ consumedDoubleMove = 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(_){ }
+ }
+ }catch(e){ /* ignore */ }
+
// Attempt to consume a double-move effect for the moving player. If present, this allows the player
// to make an additional move without flipping the turn. We represent that effect as:
// { type: 'double_move', playerId, remainingMoves }
- let consumedDoubleMove = false;
try{
room.activeCardEffects = room.activeCardEffects || [];
for(let i = room.activeCardEffects.length - 1; i >= 0; i--){
@@ -1454,6 +1479,32 @@ io.on('connection', (socket) => {
}catch(e){ console.error('parrure apply error', e); }
}catch(e){ console.error('parrure effect error', e); }
}
+ // tout ou rien: choose a piece; that piece may only move if it captures (one owner turn)
+ else if((typeof cardId === 'string' && cardId.indexOf('tout') !== -1 && cardId.indexOf('rien') !== -1) || cardId === 'tout_ou_rien'){
+ 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){
+ // invalid target: do not restore card (consume) and return error
+ return cb && cb({ error: 'no valid target' });
+ }
+ // do not allow kings to be affected
+ if(targetPiece.type && targetPiece.type.toLowerCase() === 'k'){
+ // consume card but report invalid
+ return cb && cb({ error: 'cannot_target_king' });
+ }
+ // find owner of the targeted piece
+ const targetOwner = (room.players || []).find(p => (p.color && p.color[0]) === targetPiece.color) || null;
+ room.activeCardEffects = room.activeCardEffects || [];
+ // Make 'tout_ou_rien' permanent until explicitly removed by another effect
+ 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: pick one of your pieces, then pick an enemy piece — swap their squares
else if((typeof cardId === 'string' && cardId.indexOf('inversion') !== -1) || cardId === 'inversion'){
try{
@@ -2114,8 +2165,13 @@ io.on('connection', (socket) => {
}catch(e){ console.error('mark card played error', e); }
// emit card played to entire room (informational)
io.to(roomId).emit('card:played', played);
- // After playing a card, the player's turn is consumed: perform end-of-turn bookkeeping then send updates
- try{ endTurnAfterCard(room, played.playerId); }catch(_){ /* fallback to simple update */ sendRoomUpdate(room); }
+ // After playing a card, allow the player one free piece move that does NOT consume their turn.
+ try{
+ room._freeMoveFor = played.playerId; // client may use this flag to enable a free move UI
+ // broadcast updated room state so clients can reflect the free-move opportunity
+ sendRoomUpdate(room);
+ try{ io.to(room.id).emit('card:free_move_allowed', { roomId: room.id, playerId: played.playerId }); }catch(_){ }
+ }catch(e){ /* fallback */ sendRoomUpdate(room); }
cb && cb({ ok: true, played });
});