carte rebond
This commit is contained in:
parent
624cc4007d
commit
dd0d1f8f15
2 changed files with 110 additions and 78 deletions
|
|
@ -125,7 +125,7 @@ button:hover{transform:translateY(-3px);transition:all 180ms ease}
|
|||
/* card hand UI */
|
||||
.player-hand{border:1px solid rgba(11,18,32,0.06);padding:8px;border-radius:8px;background:#fff}
|
||||
.card-item{display:flex;gap:8px;align-items:flex-start;background:#f8fafc;padding:8px;border-radius:8px;border:1px solid rgba(11,18,32,0.04)}
|
||||
.card-art{width:64px;height:48px;background:linear-gradient(180deg,#eef2ff,#ffffff);display:flex;align-items:center;justify-content:center;color:#475569;border-radius:6px;font-size:12px}
|
||||
.card-art{width:64px;height:60px;background:linear-gradient(180deg,#eef2ff,#ffffff);display:flex;align-items:center;justify-content:center;color:#475569;border-radius:6px;font-size:12px}
|
||||
.card-meta{display:flex;flex-direction:column}
|
||||
.card-title{font-weight:700;color:#0b1220}
|
||||
.card-desc{color:#475569;font-size:13px}
|
||||
|
|
@ -137,7 +137,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:260px;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: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-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}
|
||||
|
|
|
|||
174
server.js
174
server.js
|
|
@ -49,13 +49,13 @@ function sendRoomUpdate(room){
|
|||
// Build a default deck from README list. Each card has a unique id, cardId (slug), title and description.
|
||||
function buildDefaultDeck(){
|
||||
const cards = [
|
||||
['tronquer le plateau','Tronque au maximum le plateau sans supprimer de pièce'],
|
||||
// ['tronquer le plateau','Tronque au maximum le plateau sans supprimer de pièce'],
|
||||
['rebondir sur les bords','Les déplacements en diagonales de la pièce sélectionnée peuvent rebondir une fois sur les bords'],
|
||||
['agrandir le plateau','Rajoute une rangée dans toutes les directions'],
|
||||
// ['rebondir sur les bords','Les déplacements en diagonales de la pièce séléctionées peuvent rebondir une fois sur les bords'],
|
||||
// ['adoubement','La pièce sélectionnée peut maintenant faire les déplacements du cavalier en plus'],
|
||||
// ['folie','La pièce sélectionnée peut maintenant faire les déplacements du fou en plus'],
|
||||
// ['fortification','La pièce sélectionnée peut maintenant faire les déplacements de la tour en plus'],
|
||||
// ['l anneau','Le plateau devient un anneau pendant un tour'],
|
||||
// ["l'anneau","Le plateau devient un anneau pendant un tour"],
|
||||
// ['brouillard de guerre','Les joueur ne peuvent voir que au alentour de leurs pièces pendant X tours'],
|
||||
// ['changer la pièce à capturer','Le joueur choisie la nouvelle pièce jouant le rôle de roi sans la révéler'],
|
||||
// ['trou de ver','Deux cases du plateau deviennent maintenant la même'],
|
||||
|
|
@ -161,9 +161,11 @@ function computeLegalMoves(room, square){
|
|||
|
||||
// helpers
|
||||
function squareToCoord(sq){
|
||||
if(!/^[a-z][1-9][0-9]*$/i.test(sq)) return null;
|
||||
const file = sq.charCodeAt(0) - 'a'.charCodeAt(0);
|
||||
const rank = parseInt(sq.slice(1),10) - 1; // 0-indexed
|
||||
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
|
||||
return { x: file, y: rank };
|
||||
}
|
||||
function coordToSquare(x,y){
|
||||
|
|
@ -243,12 +245,48 @@ function computeLegalMoves(room, square){
|
|||
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]);
|
||||
directions.forEach(([dx,dy])=>{
|
||||
let tx = x + dx, ty = y + dy;
|
||||
|
||||
// detect if this specific piece has an active rebondir effect
|
||||
const hasRebond = (room.activeCardEffects || []).some(e => e.type === 'rebondir' && e.pieceSquare === square);
|
||||
|
||||
directions.forEach(([dx0,dy0])=>{
|
||||
if(!hasRebond){
|
||||
// standard sliding behavior
|
||||
let tx = x + dx0, ty = y + dy0;
|
||||
while(isInside(tx,ty)){
|
||||
const cont = pushIfEmptyOrCapture(tx,ty);
|
||||
if(!cont) break;
|
||||
tx += dx; ty += dy;
|
||||
tx += dx0; ty += dy0;
|
||||
}
|
||||
} 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 bounced = false;
|
||||
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;
|
||||
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
|
||||
}
|
||||
const tsq = coordToSquare(tx,ty);
|
||||
const occ = getPieceAt(tsq);
|
||||
if(!occ){
|
||||
moves.push({ from: square, to: tsq });
|
||||
// advance current position along the (possibly reflected) direction
|
||||
cx = tx; cy = ty;
|
||||
continue;
|
||||
}
|
||||
// occupied
|
||||
if(occ.color !== color) moves.push({ from: square, to: tsq });
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
return moves;
|
||||
|
|
@ -421,6 +459,12 @@ io.on('connection', (socket) => {
|
|||
// move the piece
|
||||
moving.square = to;
|
||||
|
||||
// consume any active card effects bound to this piece (they apply to the piece once and are then removed)
|
||||
try{
|
||||
room.activeCardEffects = room.activeCardEffects || [];
|
||||
room.activeCardEffects = room.activeCardEffects.filter(e => !(e.type === 'rebondir' && e.pieceSquare === from));
|
||||
}catch(e){ console.error('consuming card effects error', e); }
|
||||
|
||||
// advance version and flip turn
|
||||
board.version = (board.version || 0) + 1;
|
||||
board.turn = (board.turn === 'w') ? 'b' : 'w';
|
||||
|
|
@ -536,6 +580,8 @@ io.on('connection', (socket) => {
|
|||
const room = rooms.get(roomId);
|
||||
if(!room) return cb && cb({ error: 'room not found' });
|
||||
const playerId = socket.data.playerId || null;
|
||||
// remember last selected square for this socket so cards that need a target can use it
|
||||
try{ socket.data.lastSelectedSquare = square || null; }catch(e){}
|
||||
// compute legal moves for the selected square (allow capturing king)
|
||||
let moves = [];
|
||||
try{
|
||||
|
|
@ -548,6 +594,7 @@ io.on('connection', (socket) => {
|
|||
try{
|
||||
// send to selecting socket (include moves)
|
||||
socket.emit('game:select', { playerId, square, moves });
|
||||
// (pending-target flow removed) — server no longer auto-applies pending card targets on selection
|
||||
// notify other sockets in the room about the selection but without revealing the legal moves
|
||||
socket.to(roomId).emit('game:select', { playerId, square, moves: [] });
|
||||
}catch(e){
|
||||
|
|
@ -579,6 +626,28 @@ io.on('connection', (socket) => {
|
|||
const played = { id: uuidv4().slice(0,8), playerId: senderId, cardId, payload, ts: Date.now() };
|
||||
room.playedCards = room.playedCards || [];
|
||||
|
||||
// Pre-check for rebondir: if the card is rebondir and there is no explicit target
|
||||
// then require that the socket has a lastSelectedSquare which belongs to the player.
|
||||
try{
|
||||
const isRebond = (typeof cardId === 'string') && (cardId.indexOf('rebondir') !== -1 || cardId.indexOf('rebond') !== -1);
|
||||
if(isRebond){
|
||||
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){
|
||||
// nothing should happen if there's no selected piece or the selected piece isn't owned by the player
|
||||
return cb && cb({ error: 'no valid target' });
|
||||
}
|
||||
// ensure the payload has the resolved target for downstream handling
|
||||
payload = payload || {};
|
||||
payload.targetSquare = targetCandidate;
|
||||
played.payload = Object.assign({}, payload);
|
||||
}
|
||||
}catch(e){ console.error('rebondir pre-check error', e); }
|
||||
|
||||
// remove the played card from the player's hand and move it to discard
|
||||
try{
|
||||
room.hands = room.hands || {};
|
||||
|
|
@ -611,9 +680,11 @@ io.on('connection', (socket) => {
|
|||
|
||||
// helper: parse square -> coords (0-indexed)
|
||||
function squareToCoord(sq){
|
||||
if(!sq || typeof sq !== 'string') return null;
|
||||
const file = sq.charCodeAt(0) - 'a'.charCodeAt(0);
|
||||
const rank = parseInt(sq.slice(1),10) - 1;
|
||||
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){
|
||||
|
|
@ -637,68 +708,29 @@ io.on('connection', (socket) => {
|
|||
played.payload = Object.assign({}, payload, { applied: 'agrandir_plateau', oldWidth: oldW, oldHeight: oldH, newWidth: newW, newHeight: newH });
|
||||
}
|
||||
} else if(cardId === 'tronquer_plateau' || cardId === 'tronquer_le_plateau' || (typeof cardId === 'string' && cardId.indexOf('tronquer') !== -1)){
|
||||
// Shrink the board by trimming empty outer files/ranks as much as possible without removing any piece.
|
||||
// Tronquer (trim) is currently disabled: record the play but do not modify the board.
|
||||
// Historically this code computed the occupied bounding box and rewrote board.pieces/width/height.
|
||||
// That behavior caused unintended piece removals in some edge cases, so trimming is commented out for now.
|
||||
played.payload = Object.assign({}, payload, { applied: 'tronquer_plateau', note: 'disabled - trimming commented out by developer' });
|
||||
}
|
||||
// rebondir: grant a one-time bounce ability to a specific piece (targetSquare required in payload)
|
||||
else if(cardId === 'rebondir_sur_les_bords' || cardId === 'rebondir' || (typeof cardId === 'string' && cardId.indexOf('rebondir') !== -1)){
|
||||
try{
|
||||
const board = room.boardState;
|
||||
if(board && board.width && board.height){
|
||||
const oldW = board.width;
|
||||
const oldH = board.height;
|
||||
let width = oldW;
|
||||
let height = oldH;
|
||||
|
||||
// helper: parse square -> coords (0-indexed)
|
||||
function squareToCoord(sq){
|
||||
if(!sq || typeof sq !== 'string') return null;
|
||||
const file = sq.charCodeAt(0) - 'a'.charCodeAt(0);
|
||||
const rank = parseInt(sq.slice(1),10) - 1;
|
||||
return { x: file, y: rank };
|
||||
// allow the client to either provide payload.targetSquare or rely on the last selected square
|
||||
let target = payload && payload.targetSquare;
|
||||
if(!target){
|
||||
try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; }
|
||||
}
|
||||
function coordToSquare(x,y){
|
||||
return String.fromCharCode('a'.charCodeAt(0) + x) + (y+1);
|
||||
}
|
||||
|
||||
function colHasPiece(x){
|
||||
return (board.pieces || []).some(p => { const c = squareToCoord(p.square); return c && c.x === x; });
|
||||
}
|
||||
function rowHasPiece(y){
|
||||
return (board.pieces || []).some(p => { const c = squareToCoord(p.square); return c && c.y === y; });
|
||||
}
|
||||
|
||||
// trim left columns that are empty
|
||||
while(width > 1 && !colHasPiece(0)){
|
||||
// shift all pieces left by 1
|
||||
(board.pieces || []).forEach(p => {
|
||||
const c = squareToCoord(p.square);
|
||||
if(!c) return;
|
||||
p.square = coordToSquare(c.x - 1, c.y);
|
||||
});
|
||||
width -= 1;
|
||||
}
|
||||
// trim right columns that are empty
|
||||
while(width > 1 && !colHasPiece(width - 1)){
|
||||
// no shift needed for trimming right
|
||||
width -= 1;
|
||||
}
|
||||
// trim bottom ranks that are empty
|
||||
while(height > 1 && !rowHasPiece(0)){
|
||||
// shift all pieces down by 1 (y-1)
|
||||
(board.pieces || []).forEach(p => {
|
||||
const c = squareToCoord(p.square);
|
||||
if(!c) return;
|
||||
p.square = coordToSquare(c.x, c.y - 1);
|
||||
});
|
||||
height -= 1;
|
||||
}
|
||||
// trim top ranks that are empty
|
||||
while(height > 1 && !rowHasPiece(height - 1)){
|
||||
height -= 1;
|
||||
}
|
||||
|
||||
board.width = width;
|
||||
board.height = height;
|
||||
board.version = (board.version || 0) + 1;
|
||||
|
||||
played.payload = Object.assign({}, payload, { applied: 'tronquer_plateau', oldWidth: oldW, oldHeight: oldH, newWidth: width, newHeight: height });
|
||||
if(!board) {
|
||||
played.payload = Object.assign({}, payload, { applied: 'rebondir', appliedTo: null, note: 'no board state' });
|
||||
} else {
|
||||
room.activeCardEffects = room.activeCardEffects || [];
|
||||
// effect: type, pieceSquare, playerId, expiresOnMoveVersion (optional)
|
||||
room.activeCardEffects.push({ id: played.id, type: 'rebondir', 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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue