inversion

This commit is contained in:
Didictateur 2025-11-23 13:13:31 +01:00
parent 72d096f0e5
commit 1624e4caf8
2 changed files with 127 additions and 6 deletions

View file

@ -115,6 +115,8 @@
if(id.indexOf('coin') !== -1 || id === 'coin_coin') return 'owned'; if(id.indexOf('coin') !== -1 || id === 'coin_coin') return 'owned';
// teleportation: require selecting one of your pieces first (like promotion) // teleportation: require selecting one of your pieces first (like promotion)
if(id.indexOf('teleport') !== -1 || id.indexOf('tele') !== -1 || id.indexOf('t_l_portation') !== -1) return 'owned'; if(id.indexOf('teleport') !== -1 || id.indexOf('tele') !== -1 || id.indexOf('t_l_portation') !== -1) return 'owned';
// inversion: first choose one of your pieces, then choose an enemy piece to swap places
if(id.indexOf('inversion') !== -1) return 'own_then_enemy';
return null; return null;
} }
@ -483,8 +485,35 @@
// If we're in a pending card-targeting mode, handle target selection and do NOT attempt moves // If we're in a pending card-targeting mode, handle target selection and do NOT attempt moves
if(pendingCard){ if(pendingCard){
const req = pendingCard.requireType; // 'empty' | 'owned' | 'owned_pawn' | 'enemy' const req = pendingCard.requireType; // 'empty' | 'owned' | 'owned_pawn' | 'enemy' | 'own_then_enemy' | 'enemy_queen'
const myShort = (myColor && myColor[0]) || ''; const myShort = (myColor && myColor[0]) || '';
// handle two-step selection for cards like 'inversion'
if(req === 'own_then_enemy'){
// first select one of your own pieces
if(!pendingCard.firstTarget){
const piece = currentBoardState && Array.isArray(currentBoardState.pieces) && currentBoardState.pieces.find(p => p.square === square);
if(!piece || piece.color !== myShort){ log('select one of your pieces (first)'); return; }
if(piece.type && piece.type.toLowerCase() === 'k'){ log('cannot select king'); return; }
// store first selection and highlight enemy pieces
pendingCard.firstTarget = square;
highlightTargets('enemy');
// mark visually the chosen source (temporary)
try{ const el = boardEl.querySelector(`.square[data-square="${square}"]`); if(el) el.classList.add('target-chosen'); }catch(_){ }
log('First piece selected for inversion:', square);
return;
}
// second selection: enemy piece to swap with
const occ = currentBoardState && Array.isArray(currentBoardState.pieces) && currentBoardState.pieces.find(p => p.square === square);
if(!occ || occ.color === myShort){ log('select an enemy piece (second)'); return; }
// emit swap payload: sourceSquare + targetSquare
socket.emit('card:play', { roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { sourceSquare: pendingCard.firstTarget, targetSquare: square } }, (resp)=>{
if(resp && resp.error){ log('card:play error', resp); alert(resp.error); }
else { log('inversion played', resp); }
});
// cleanup visuals
try{ const el = boardEl.querySelector(`.square[data-square="${pendingCard.firstTarget}"]`); if(el) el.classList.remove('target-chosen'); }catch(_){ }
pendingCard = null; clearTargetHighlights(); return;
}
if(req === 'empty'){ if(req === 'empty'){
const occupied = currentBoardState && Array.isArray(currentBoardState.pieces) && currentBoardState.pieces.find(p => p.square === square); const occupied = currentBoardState && Array.isArray(currentBoardState.pieces) && currentBoardState.pieces.find(p => p.square === square);
if(occupied){ log('square not empty for mine placement'); return; } if(occupied){ log('square not empty for mine placement'); return; }
@ -497,6 +526,7 @@
} else if(req === 'owned'){ } else if(req === 'owned'){
const piece = currentBoardState && Array.isArray(currentBoardState.pieces) && currentBoardState.pieces.find(p => p.square === square); const piece = currentBoardState && Array.isArray(currentBoardState.pieces) && currentBoardState.pieces.find(p => p.square === square);
if(!piece || piece.color !== myShort){ log('select one of your pieces'); return; } if(!piece || piece.color !== myShort){ log('select one of your pieces'); return; }
if(piece.type && piece.type.toLowerCase() === 'k'){ log('cannot select king'); return; }
socket.emit('card:play', { roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { targetSquare: square } }, (resp)=>{ 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); } if(resp && resp.error){ log('card:play error', resp); alert(resp.error); }
else { log('card played with target', resp); } else { log('card played with target', resp); }
@ -524,6 +554,7 @@
} else if(req === 'enemy'){ } else if(req === 'enemy'){
const occ = currentBoardState && Array.isArray(currentBoardState.pieces) && currentBoardState.pieces.find(p => p.square === square); const occ = currentBoardState && Array.isArray(currentBoardState.pieces) && currentBoardState.pieces.find(p => p.square === square);
if(!occ || occ.color === myShort){ log('select an enemy piece'); return; } if(!occ || occ.color === myShort){ log('select an enemy 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)=>{ 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); } if(resp && resp.error){ log('card:play error', resp); alert(resp.error); }
else { log('card played with target', resp); else { log('card played with target', resp);
@ -724,16 +755,20 @@
if(!occ) sqEl.classList.add('targetable'); if(!occ) sqEl.classList.add('targetable');
} else if(type === 'owned'){ } else if(type === 'owned'){
const occ = (currentBoardState.pieces||[]).find(p=>p.square===sq); const occ = (currentBoardState.pieces||[]).find(p=>p.square===sq);
if(occ && occ.color === myShort) sqEl.classList.add('targetable'); if(occ && occ.color === myShort && occ.type && occ.type.toLowerCase() !== 'k') sqEl.classList.add('targetable');
} else if(type === 'owned_pawn'){ } else if(type === 'owned_pawn'){
const occ = (currentBoardState.pieces||[]).find(p=>p.square===sq); const occ = (currentBoardState.pieces||[]).find(p=>p.square===sq);
if(occ && occ.color === myShort && occ.type && occ.type.toLowerCase() === 'p') sqEl.classList.add('targetable'); if(occ && occ.color === myShort && occ.type && occ.type.toLowerCase() === 'p') sqEl.classList.add('targetable');
} else if(type === 'own_then_enemy'){
// 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 === 'enemy_queen'){ } else if(type === 'enemy_queen'){
const occ = (currentBoardState.pieces||[]).find(p=>p.square===sq); 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'); if(occ && occ.color && occ.color !== myShort && occ.type && occ.type.toLowerCase() === 'q') sqEl.classList.add('targetable');
} else if(type === 'enemy'){ } else if(type === 'enemy'){
const occ = (currentBoardState.pieces||[]).find(p=>p.square===sq); const occ = (currentBoardState.pieces||[]).find(p=>p.square===sq);
if(occ && occ.color && occ.color !== myShort) sqEl.classList.add('targetable'); if(occ && occ.color && occ.color !== myShort && occ.type && occ.type.toLowerCase() !== 'k') sqEl.classList.add('targetable');
} }
}); });
// add a small note near hands // add a small note near hands

View file

@ -136,6 +136,48 @@ function visibleSquaresForPlayer(room, playerId){
return out; 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;
const board = room.boardState;
// flip turn
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{
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(e){ console.error('updating temporary effects error', e); }
// reset per-turn card-play flags
try{ room._cardPlayedThisTurn = {}; }catch(_){ }
// perform next player's draw (if autoDraw) or at least send room update
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); }
}
// Build a default deck from README list. Each card has a unique id, cardId (slug), title and description. // Build a default deck from README list. Each card has a unique id, cardId (slug), title and description.
function buildDefaultDeck(){ function buildDefaultDeck(){
const cards = [ const cards = [
@ -174,7 +216,7 @@ function buildDefaultDeck(){
["toucher c'est jouer","Toucher une pièce adverse qu'il sera obligé de jouer"], ["toucher c'est jouer","Toucher une pièce adverse qu'il sera obligé de jouer"],
// ['marécage','Pendant X tours, toutes les pièces ne peuvent se déplacer que comme un roi'], // ['marécage','Pendant X tours, toutes les pièces ne peuvent se déplacer que comme un roi'],
// ['sniper','Capturer une pièce sans avoir à bouger la pièce capturante'], // ['sniper','Capturer une pièce sans avoir à bouger la pièce capturante'],
// ['inversion','Échange la position d une pièce avec une pièce adverse'], ['inversion','Échange la position d une pièce avec une pièce adverse'],
// ['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'], // ['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'],
// ['punching ball','Replace le roi dans sa position initiale, et place un nouveau pion à l ancienne position du roi'], // ['punching ball','Replace le roi dans sa position initiale, et place un nouveau pion à l ancienne position du roi'],
// ['reversi','Si deux pions encadrent parfaitement une pièce adverse, cette dernière change de camp'], // ['reversi','Si deux pions encadrent parfaitement une pièce adverse, cette dernière change de camp'],
@ -1412,6 +1454,50 @@ io.on('connection', (socket) => {
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(_){ } 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 apply error', e); }
}catch(e){ console.error('parrure effect error', e); } }catch(e){ console.error('parrure 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{
const board = room.boardState;
const payloadSrc = payload && payload.sourceSquare;
const payloadTgt = payload && payload.targetSquare;
// fallback to lastSelectedSquare for source if client didn't provide both
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);
// validate both pieces exist and belong respectively to player and opponent
if(!board || !source || !target || !srcPiece || !tgtPiece || srcPiece.color !== playerColorShort || tgtPiece.color === playerColorShort){
// invalid target(s): restore removed card to hand and abort
try{ room.hands = room.hands || {}; room.hands[senderId] = room.hands[senderId] || []; if(removed) room.hands[senderId].push(removed); room.discard = room.discard || []; for(let i = room.discard.length - 1; i >= 0; i--){ if(room.discard[i] && room.discard[i].id === (removed && removed.id)){ room.discard.splice(i,1); break; } } }catch(e){ console.error('restore removed card error', e); }
return cb && cb({ error: 'no valid targets' });
}
// swap their squares
try{
const sSquare = srcPiece.square;
const tSquare = tgtPiece.square;
srcPiece.square = tSquare;
tgtPiece.square = sSquare;
// update any activeCardEffects that reference squares
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(_){ }
// bump board version
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: allow the selected piece to move to any empty square for one turn // teleportation: allow the selected piece to move to any empty square for one turn
else if((typeof cardId === 'string' && (cardId.indexOf('teleport') !== -1 || cardId.indexOf('t_l_portation') !== -1 || cardId.indexOf('t_lportation') !== -1)) || cardId === 'teleport'){ else if((typeof cardId === 'string' && (cardId.indexOf('teleport') !== -1 || cardId.indexOf('t_l_portation') !== -1 || cardId.indexOf('t_lportation') !== -1)) || cardId === 'teleport'){
@ -2029,8 +2115,8 @@ io.on('connection', (socket) => {
}catch(e){ console.error('mark card played error', e); } }catch(e){ console.error('mark card played error', e); }
// emit card played to entire room (informational) // emit card played to entire room (informational)
io.to(roomId).emit('card:played', played); io.to(roomId).emit('card:played', played);
// send personalized room updates (will include updated hands and discardCount) // After playing a card, the player's turn is consumed: perform end-of-turn bookkeeping then send updates
sendRoomUpdate(room); try{ endTurnAfterCard(room, played.playerId); }catch(_){ /* fallback to simple update */ sendRoomUpdate(room); }
cb && cb({ ok: true, played }); cb && cb({ ok: true, played });
}); });