inversion
This commit is contained in:
parent
72d096f0e5
commit
1624e4caf8
2 changed files with 127 additions and 6 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
92
server.js
92
server.js
|
|
@ -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 });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue