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 */
|
/* card hand UI */
|
||||||
.player-hand{border:1px solid rgba(11,18,32,0.06);padding:8px;border-radius:8px;background:#fff}
|
.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-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-meta{display:flex;flex-direction:column}
|
||||||
.card-title{font-weight:700;color:#0b1220}
|
.card-title{font-weight:700;color:#0b1220}
|
||||||
.card-desc{color:#475569;font-size:13px}
|
.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}
|
.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}
|
.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}
|
.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-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-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}
|
.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.
|
// 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 = [
|
||||||
['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'],
|
['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'],
|
// ['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'],
|
// ['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'],
|
// ['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'],
|
// ['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'],
|
// ['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'],
|
// ['trou de ver','Deux cases du plateau deviennent maintenant la même'],
|
||||||
|
|
@ -161,9 +161,11 @@ function computeLegalMoves(room, square){
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
function squareToCoord(sq){
|
function squareToCoord(sq){
|
||||||
if(!/^[a-z][1-9][0-9]*$/i.test(sq)) return null;
|
if(!sq) return null;
|
||||||
const file = sq.charCodeAt(0) - 'a'.charCodeAt(0);
|
const s = String(sq).trim().toLowerCase();
|
||||||
const rank = parseInt(sq.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 };
|
return { x: file, y: rank };
|
||||||
}
|
}
|
||||||
function coordToSquare(x,y){
|
function coordToSquare(x,y){
|
||||||
|
|
@ -243,12 +245,48 @@ function computeLegalMoves(room, square){
|
||||||
const directions = [];
|
const directions = [];
|
||||||
if(type === 'B' || type === 'Q') directions.push([1,1],[1,-1],[-1,1],[-1,-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]);
|
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)){
|
while(isInside(tx,ty)){
|
||||||
const cont = pushIfEmptyOrCapture(tx,ty);
|
const cont = pushIfEmptyOrCapture(tx,ty);
|
||||||
if(!cont) break;
|
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;
|
return moves;
|
||||||
|
|
@ -421,6 +459,12 @@ io.on('connection', (socket) => {
|
||||||
// move the piece
|
// move the piece
|
||||||
moving.square = to;
|
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
|
// advance version and flip turn
|
||||||
board.version = (board.version || 0) + 1;
|
board.version = (board.version || 0) + 1;
|
||||||
board.turn = (board.turn === 'w') ? 'b' : 'w';
|
board.turn = (board.turn === 'w') ? 'b' : 'w';
|
||||||
|
|
@ -536,6 +580,8 @@ io.on('connection', (socket) => {
|
||||||
const room = rooms.get(roomId);
|
const room = rooms.get(roomId);
|
||||||
if(!room) return cb && cb({ error: 'room not found' });
|
if(!room) return cb && cb({ error: 'room not found' });
|
||||||
const playerId = socket.data.playerId || null;
|
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)
|
// compute legal moves for the selected square (allow capturing king)
|
||||||
let moves = [];
|
let moves = [];
|
||||||
try{
|
try{
|
||||||
|
|
@ -548,6 +594,7 @@ io.on('connection', (socket) => {
|
||||||
try{
|
try{
|
||||||
// send to selecting socket (include moves)
|
// send to selecting socket (include moves)
|
||||||
socket.emit('game:select', { playerId, square, 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
|
// notify other sockets in the room about the selection but without revealing the legal moves
|
||||||
socket.to(roomId).emit('game:select', { playerId, square, moves: [] });
|
socket.to(roomId).emit('game:select', { playerId, square, moves: [] });
|
||||||
}catch(e){
|
}catch(e){
|
||||||
|
|
@ -579,6 +626,28 @@ io.on('connection', (socket) => {
|
||||||
const played = { id: uuidv4().slice(0,8), playerId: senderId, cardId, payload, ts: Date.now() };
|
const played = { id: uuidv4().slice(0,8), playerId: senderId, cardId, payload, ts: Date.now() };
|
||||||
room.playedCards = room.playedCards || [];
|
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
|
// remove the played card from the player's hand and move it to discard
|
||||||
try{
|
try{
|
||||||
room.hands = room.hands || {};
|
room.hands = room.hands || {};
|
||||||
|
|
@ -611,9 +680,11 @@ io.on('connection', (socket) => {
|
||||||
|
|
||||||
// helper: parse square -> coords (0-indexed)
|
// helper: parse square -> coords (0-indexed)
|
||||||
function squareToCoord(sq){
|
function squareToCoord(sq){
|
||||||
if(!sq || typeof sq !== 'string') return null;
|
if(!sq) return null;
|
||||||
const file = sq.charCodeAt(0) - 'a'.charCodeAt(0);
|
const s = String(sq).trim().toLowerCase();
|
||||||
const rank = parseInt(sq.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 };
|
return { x: file, y: rank };
|
||||||
}
|
}
|
||||||
function coordToSquare(x,y){
|
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 });
|
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)){
|
} 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;
|
const board = room.boardState;
|
||||||
if(board && board.width && board.height){
|
// allow the client to either provide payload.targetSquare or rely on the last selected square
|
||||||
const oldW = board.width;
|
let target = payload && payload.targetSquare;
|
||||||
const oldH = board.height;
|
if(!target){
|
||||||
let width = oldW;
|
try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; }
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
function coordToSquare(x,y){
|
if(!board) {
|
||||||
return String.fromCharCode('a'.charCodeAt(0) + x) + (y+1);
|
played.payload = Object.assign({}, payload, { applied: 'rebondir', appliedTo: null, note: 'no board state' });
|
||||||
}
|
} else {
|
||||||
|
room.activeCardEffects = room.activeCardEffects || [];
|
||||||
function colHasPiece(x){
|
// effect: type, pieceSquare, playerId, expiresOnMoveVersion (optional)
|
||||||
return (board.pieces || []).some(p => { const c = squareToCoord(p.square); return c && c.x === x; });
|
room.activeCardEffects.push({ id: played.id, type: 'rebondir', pieceSquare: target, playerId: senderId });
|
||||||
}
|
played.payload = Object.assign({}, payload, { applied: 'rebondir', appliedTo: target });
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
}catch(e){ console.error('rebondir effect error', e); }
|
||||||
}
|
}
|
||||||
}catch(e){
|
}catch(e){
|
||||||
console.error('card:play effect error', e);
|
console.error('card:play effect error', e);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue