Commit a01bee1c authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(ludo): FULL REWRITE — proper board path, dice animation, player panels

Complete Ludo game overhaul:
- NEW: board-map.js with exact 52-square path coordinates on 15x15 grid
- Pieces now move on CORRECT squares (not random circular positions)
- Home columns, safe squares (stars), home bases all properly mapped
- Dice roll ANIMATION: 10 frames of random faces with rotation
- Dice slam-in effect + star burst on rolling 6
- 4 player panels showing whose turn it is (active border glow)
- Proper game flow: roll → valid moves → auto-pick best → animate
- Bot turns show dice face updates
- Capture: screen shake + heavy haptic
- Finish: star burst + success haptic
- Win: confetti + celebration
- Board rendering: colored home zones, grid lines, home columns,
  safe squares with stars, center triangles, piece shadows + highlights
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent fed41bb4
// Ludo Board Map — exact grid coordinates for all 52 shared squares + home columns
// Board is 15x15 grid. Each position is [col, row] from top-left (0,0)
// The 52 shared squares, numbered 0-51, going clockwise from red's start
export const SHARED_PATH = [
[6,1],[6,2],[6,3],[6,4],[6,5], // 0-4: top column going down
[5,6],[4,6],[3,6],[2,6],[1,6],[0,6], // 5-10: left row going left
[0,7], // 11: left corner
[0,8],[1,8],[2,8],[3,8],[4,8],[5,8], // 12-17: left row going right
[6,9],[6,10],[6,11],[6,12],[6,13],[6,14], // 18-23: bottom column going down
[7,14], // 24: bottom corner
[8,14],[8,13],[8,12],[8,11],[8,10],[8,9], // 25-30: bottom column going up
[9,8],[10,8],[11,8],[12,8],[13,8],[14,8], // 31-36: right row going right
[14,7], // 37: right corner
[14,6],[13,6],[12,6],[11,6],[10,6],[9,6], // 38-43: right row going left
[8,5],[8,4],[8,3],[8,2],[8,1],[8,0], // 44-49: top column going up
[7,0], // 50: top corner
[6,0], // 51: back to start area
];
// Starting squares for each player (where they enter the board)
export const START_SQUARES = [1, 14, 27, 40]; // Red, Blue, Green, Yellow
// Home column paths (6 squares each, going toward center)
export const HOME_COLUMNS = [
[[7,1],[7,2],[7,3],[7,4],[7,5],[7,6]], // Red: top going down
[[1,7],[2,7],[3,7],[4,7],[5,7],[6,7]], // Blue: left going right
[[7,13],[7,12],[7,11],[7,10],[7,9],[7,8]], // Green: bottom going up
[[13,7],[12,7],[11,7],[10,7],[9,7],[8,7]], // Yellow: right going left
];
// Home entry points (the square BEFORE entering home column)
export const HOME_ENTRY = [0, 13, 26, 39]; // The square just before home column
// Safe squares (stars) — pieces can't be captured here
export const SAFE_SQUARES = [0, 8, 13, 21, 26, 34, 39, 47];
// Home base positions (where pieces sit before entering the board)
export const HOME_BASES = [
[[2,2],[4,2],[2,4],[4,4]], // Red (top-left)
[[10,2],[12,2],[10,4],[12,4]], // Blue (top-right)
[[10,10],[12,10],[10,12],[12,12]], // Green (bottom-right)
[[2,10],[4,10],[2,12],[4,12]], // Yellow (bottom-left)
];
// Convert a board grid position [col,row] to pixel coordinates
export function gridToPixel(col, row, cellSize) {
return {
x: col * cellSize + cellSize / 2,
y: row * cellSize + cellSize / 2
};
}
// Get the pixel position for a piece given its game state
export function getPiecePosition(localPos, playerIdx, cellSize) {
if (localPos === -1) return null; // In home base — handled separately
if (localPos >= 52) {
// In home column
const homeIdx = localPos - 52;
if (homeIdx < 6) {
const [col, row] = HOME_COLUMNS[playerIdx][homeIdx];
return gridToPixel(col, row, cellSize);
}
return gridToPixel(7, 7, cellSize); // Finished — center
}
// On shared path — convert local position to global
const globalPos = (localPos + START_SQUARES[playerIdx]) % 52;
const [col, row] = SHARED_PATH[globalPos];
return gridToPixel(col, row, cellSize);
}
// Get home base pixel position for a piece still at home
export function getHomeBasePosition(playerIdx, pieceIdx, cellSize) {
const [col, row] = HOME_BASES[playerIdx][pieceIdx];
return gridToPixel(col, row, cellSize);
}
......@@ -6,14 +6,14 @@ import { t } from '../../../core/i18n.js';
import { createCanvas, clear } from '../../../core/canvas.js';
import * as rules from '../logic/rules.js';
import * as juice from '../../../core/juice.js';
import { getPiecePosition, getHomeBasePosition, SAFE_SQUARES, HOME_COLUMNS, SHARED_PATH } from '../logic/board-map.js';
let game, validMoves, canvasCtx, canvasEl, boardSize;
let game, validMoves, ctx, canvas, boardSize, cellSize;
let diceAnimating = false;
const COLORS = ['#E53935', '#1E88E5', '#43A047', '#FDD835'];
const COLORS_LIGHT = ['#EF9A9A', '#90CAF9', '#A5D6A7', '#FFF59D'];
const BG_COLOR = '#FAFAFA';
const PATH_COLOR = '#FFFFFF';
const BORDER_COLOR = '#E0E0E0';
const PLAYER_NAMES = ['أنت', 'Bot 1', 'Bot 2', 'Bot 3'];
export function mountGame(el, params) {
const { mode = 'bot', numPlayers = 4 } = params;
......@@ -21,326 +21,193 @@ export function mountGame(el, params) {
game = rules.createGame(numPlayers);
validMoves = [];
diceAnimating = false;
el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:#1a2440;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:#0a1020;border-bottom:1px solid rgba(255,255,255,0.06);">
<span style="font-size:14px;font-weight:600;color:#f8fafc;">${t('game.ludo')}</span>
<span id="ludo-turn" style="font-size:13px;color:${COLORS[0]};">${t('game.your_turn')}</span>
<div style="display:flex;flex-direction:column;height:100%;background:#1a1a2e;">
<div style="display:flex;justify-content:space-between;padding:8px 12px;background:#0f0f1e;">
<div class="pp" id="pp-1" style="--pc:${COLORS[1]};"><div class="pp-dot"></div><span>${PLAYER_NAMES[1]}</span></div>
<div class="pp" id="pp-0" style="--pc:${COLORS[0]};"><div class="pp-dot"></div><span>${PLAYER_NAMES[0]}</span></div>
</div>
<div id="ludo-board" style="flex:1;display:flex;align-items:center;justify-content:center;padding:8px;"></div>
<div style="display:flex;align-items:center;gap:16px;padding:12px;background:#0a1020;border-top:1px solid rgba(255,255,255,0.06);justify-content:center;">
<div id="dice-display" style="width:56px;height:56px;background:#1a2440;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:28px;font-weight:800;font-family:Inter,sans-serif;border:2px solid rgba(255,255,255,0.1);color:#f8fafc;"></div>
<button class="btn btn-primary" id="roll-btn" style="font-size:16px;padding:12px 32px;">🎲 ${t('game.ludo') === 'لودو' ? 'ارمِ النرد' : 'Roll Dice'}</button>
<div id="ludo-wrap" style="flex:1;display:flex;align-items:center;justify-content:center;padding:4px;min-height:0;"></div>
<div style="display:flex;justify-content:space-between;padding:8px 12px;background:#0f0f1e;">
<div class="pp" id="pp-2" style="--pc:${COLORS[2]};"><div class="pp-dot"></div><span>${PLAYER_NAMES[2]}</span></div>
<div class="pp" id="pp-3" style="--pc:${COLORS[3]};"><div class="pp-dot"></div><span>${PLAYER_NAMES[3]}</span></div>
</div>
<div style="display:flex;align-items:center;gap:16px;padding:12px 16px;background:#0f0f1e;border-top:1px solid rgba(255,255,255,0.06);justify-content:center;">
<div id="dice-box" style="width:56px;height:56px;background:#f8fafc;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:26px;font-weight:900;color:#1a1a2e;box-shadow:0 3px 10px rgba(0,0,0,0.3);">🎲</div>
<button class="btn btn-primary" id="roll-btn" style="font-size:15px;padding:14px 36px;">ارمِ النرد</button>
</div>
</div>
<style>
.pp{display:flex;align-items:center;gap:6px;padding:5px 10px;border-radius:8px;border:2px solid transparent;transition:border-color 0.3s;}
.pp.active{border-color:var(--pc);background:rgba(255,255,255,0.05);}
.pp-dot{width:10px;height:10px;border-radius:50%;background:var(--pc);}
.pp span{font-size:12px;font-weight:600;color:#94a3b8;}
.pp.active span{color:#f8fafc;}
</style>
`;
const boardContainer = el.querySelector('#ludo-board');
boardSize = Math.min(boardContainer.clientWidth || 360, boardContainer.clientHeight || 360, 380);
const { canvas, ctx } = createCanvas(boardContainer, boardSize, boardSize);
canvas.style.cssText = 'width:100%;max-width:380px;height:auto;aspect-ratio:1;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,0.3);';
canvasCtx = ctx;
canvasEl = canvas;
const wrap = el.querySelector('#ludo-wrap');
boardSize = Math.min(wrap.clientWidth || 370, wrap.clientHeight || 370, 380);
cellSize = boardSize / 15;
const { canvas: c, ctx: cx } = createCanvas(wrap, boardSize, boardSize);
c.style.cssText = 'border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,0.4);';
canvas = c; ctx = cx;
drawBoard();
updateTurnDisplay(el);
updatePanels(el);
el.querySelector('#roll-btn').addEventListener('click', () => handleRoll(el));
bus.emit('game:started', { gameKey: 'ludo', mode });
}
function handleRoll(el) {
if (game.rolled || game.gameOver) return;
if (game.currentPlayer !== 0) return;
const dice = rules.rollDice();
game.diceValue = dice;
game.rolled = true;
audio.play('dice', 'game');
juice.hapticMedium();
const diceEl = el.querySelector('#dice-display');
diceEl.textContent = dice;
diceEl.style.color = '#E4AC38';
juice.slamIn(diceEl, { scale: 1.6, duration: 350 });
juice.shake(el.querySelector('#ludo-board'), 2, 150);
if (dice === 6) {
juice.starBurst(diceEl.getBoundingClientRect().left + 28, diceEl.getBoundingClientRect().top + 28, 5);
}
validMoves = rules.getValidMoves(game, 0, dice);
if (validMoves.length === 0) {
setTimeout(() => {
game.rolled = false;
rules.nextTurn(game);
diceEl.style.color = '#f8fafc';
updateTurnDisplay(el);
drawBoard();
if (game.currentPlayer !== 0) botSequence(el);
}, 800);
return;
}
if (validMoves.length === 1) {
applyPlayerMove(el, validMoves[0]);
return;
}
drawBoard();
// For multiple moves, auto-pick the best (capture > finish > enter > random)
const best = validMoves.find(m => m.type === 'capture') ||
validMoves.find(m => m.type === 'finish') ||
validMoves.find(m => m.type === 'enter') ||
validMoves[0];
setTimeout(() => applyPlayerMove(el, best), 500);
if (diceAnimating || game.rolled || game.gameOver || game.currentPlayer !== 0) return;
diceAnimating = true;
const btn = el.querySelector('#roll-btn');
const diceBox = el.querySelector('#dice-box');
btn.disabled = true;
const faces = ['⚀','⚁','⚂','⚃','⚄','⚅'];
let count = 0;
const anim = setInterval(() => {
diceBox.textContent = faces[Math.floor(Math.random()*6)];
diceBox.style.transform = `rotate(${Math.random()*20-10}deg)`;
count++;
if (count > 10) {
clearInterval(anim);
const dice = rules.rollDice();
game.diceValue = dice;
game.rolled = true;
diceBox.textContent = faces[dice-1];
diceBox.style.transform = 'none';
diceAnimating = false;
audio.play('dice', 'game');
juice.hapticMedium();
juice.slamIn(diceBox, { scale: 1.3, duration: 300 });
if (dice === 6) juice.starBurst(diceBox.getBoundingClientRect().left+28, diceBox.getBoundingClientRect().top+28, 5);
validMoves = rules.getValidMoves(game, 0, dice);
if (validMoves.length === 0) {
setTimeout(() => { game.rolled = false; rules.nextTurn(game); btn.disabled = false; updatePanels(el); drawBoard(); if (game.currentPlayer !== 0) botLoop(el); }, 800);
} else {
const best = validMoves.find(m=>m.type==='capture') || validMoves.find(m=>m.type==='finish') || validMoves.find(m=>m.type==='enter') || validMoves[0];
setTimeout(() => doMove(el, best), 400);
}
}
}, 70);
}
function applyPlayerMove(el, move) {
rules.applyMove(game, 0, move);
function doMove(el, move) {
rules.applyMove(game, game.currentPlayer, move);
game.rolled = false;
if (move.type === 'capture') {
audio.play('capture', 'game');
juice.shake(el, 4, 250);
juice.hapticHeavy();
juice.confetti(window.innerWidth / 2, window.innerHeight / 2, 15);
} else if (move.type === 'finish') {
audio.play('win', 'reward');
juice.starBurst(window.innerWidth / 2, window.innerHeight / 2, 8);
juice.hapticSuccess();
} else {
audio.play('move', 'game');
}
if (game.gameOver || game.players[0].finished) { endGame(el); return; }
if (move.type === 'capture') { audio.play('capture','game'); juice.shake(el,4,200); juice.hapticHeavy(); }
else if (move.type === 'finish') { audio.play('win','reward'); juice.hapticSuccess(); }
else audio.play('move','game');
if (game.gameOver) { endGame(el); return; }
rules.nextTurn(game);
updateTurnDisplay(el);
updatePanels(el);
drawBoard();
el.querySelector('#dice-display').style.color = '#f8fafc';
if (game.currentPlayer !== 0) {
setTimeout(() => botSequence(el), 600);
}
const btn = el.querySelector('#roll-btn');
if (game.currentPlayer === 0) btn.disabled = false;
else setTimeout(() => botLoop(el), 400);
}
function botSequence(el) {
function botLoop(el) {
if (game.gameOver || game.currentPlayer === 0) return;
const dice = rules.rollDice();
game.diceValue = dice;
el.querySelector('#dice-display').textContent = dice;
audio.play('dice', 'game');
const diceBox = el.querySelector('#dice-box');
const faces = ['⚀','⚁','⚂','⚃','⚄','⚅'];
diceBox.textContent = faces[dice-1];
const move = rules.getBotMove(game, game.currentPlayer, dice);
if (move) {
rules.applyMove(game, game.currentPlayer, move);
if (move.type === 'capture') audio.play('capture', 'game');
}
if (move) { rules.applyMove(game, game.currentPlayer, move); if (move.type === 'capture') audio.play('capture','game'); }
if (game.gameOver) { endGame(el); return; }
rules.nextTurn(game);
updateTurnDisplay(el);
updatePanels(el);
drawBoard();
if (game.currentPlayer !== 0) {
setTimeout(() => botSequence(el), 400);
}
if (game.currentPlayer !== 0) setTimeout(() => botLoop(el), 350);
else el.querySelector('#roll-btn').disabled = false;
}
function drawBoard() {
const ctx = canvasCtx;
const s = boardSize;
const cell = s / 15;
clear(ctx, s, s);
const cs = cellSize;
clear(ctx, boardSize, boardSize);
// Background
ctx.fillStyle = BG_COLOR;
ctx.fillRect(0, 0, s, s);
// Home zones (4 corners)
drawHomeZone(ctx, 0, 0, cell * 6, 0); // Red top-left
drawHomeZone(ctx, cell * 9, 0, cell * 6, 1); // Blue top-right
drawHomeZone(ctx, cell * 9, cell * 9, cell * 6, 2); // Green bottom-right
drawHomeZone(ctx, 0, cell * 9, cell * 6, 3); // Yellow bottom-left
ctx.fillStyle = '#FAFAFA';
ctx.fillRect(0, 0, boardSize, boardSize);
// Home zones
[[0,0,0],[1,9,0],[2,9,9],[3,0,9]].forEach(([p,c,r]) => {
ctx.fillStyle = COLORS[p];
ctx.fillRect(c*cs, r*cs, 6*cs, 6*cs);
ctx.fillStyle = '#FAFAFA';
const ins = cs*0.7;
ctx.fillRect(c*cs+ins, r*cs+ins, 6*cs-ins*2, 6*cs-ins*2);
});
// Cross paths (the playing area)
ctx.fillStyle = PATH_COLOR;
ctx.strokeStyle = BORDER_COLOR;
ctx.lineWidth = 0.5;
// Top vertical path
ctx.fillRect(cell * 6, 0, cell * 3, cell * 6);
// Bottom vertical path
ctx.fillRect(cell * 6, cell * 9, cell * 3, cell * 6);
// Left horizontal path
ctx.fillRect(0, cell * 6, cell * 6, cell * 3);
// Right horizontal path
ctx.fillRect(cell * 9, cell * 6, cell * 6, cell * 3);
// Center
ctx.fillRect(cell * 6, cell * 6, cell * 3, cell * 3);
// Cross paths (white)
ctx.fillStyle = '#fff';
ctx.fillRect(6*cs,0,3*cs,6*cs);
ctx.fillRect(6*cs,9*cs,3*cs,6*cs);
ctx.fillRect(0,6*cs,6*cs,3*cs);
ctx.fillRect(9*cs,6*cs,6*cs,3*cs);
ctx.fillRect(6*cs,6*cs,3*cs,3*cs);
// Draw grid lines on paths
ctx.strokeStyle = '#E0E0E0';
// Grid lines
ctx.strokeStyle = '#ddd';
ctx.lineWidth = 0.5;
for (let i = 0; i <= 15; i++) {
// Vertical lines in cross
if (i >= 6 && i <= 9) {
ctx.beginPath(); ctx.moveTo(i * cell, 0); ctx.lineTo(i * cell, s); ctx.stroke();
}
if (i >= 6 && i <= 9) {
ctx.beginPath(); ctx.moveTo(0, i * cell); ctx.lineTo(s, i * cell); ctx.stroke();
}
if (i >= 6 && i <= 9) { ctx.beginPath(); ctx.moveTo(i*cs,0); ctx.lineTo(i*cs,boardSize); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0,i*cs); ctx.lineTo(boardSize,i*cs); ctx.stroke(); }
}
// Cells in paths
for (let i = 0; i < 6; i++) {
ctx.beginPath(); ctx.moveTo(cell * 6, i * cell); ctx.lineTo(cell * 9, i * cell); ctx.stroke();
ctx.beginPath(); ctx.moveTo(cell * 6, (i + 9) * cell); ctx.lineTo(cell * 9, (i + 9) * cell); ctx.stroke();
ctx.beginPath(); ctx.moveTo(i * cell, cell * 6); ctx.lineTo(i * cell, cell * 9); ctx.stroke();
ctx.beginPath(); ctx.moveTo((i + 9) * cell, cell * 6); ctx.lineTo((i + 9) * cell, cell * 9); ctx.stroke();
for (let i = 0; i <= 6; i++) {
ctx.beginPath(); ctx.moveTo(6*cs,i*cs); ctx.lineTo(9*cs,i*cs); ctx.stroke();
ctx.beginPath(); ctx.moveTo(6*cs,(i+9)*cs); ctx.lineTo(9*cs,(i+9)*cs); ctx.stroke();
ctx.beginPath(); ctx.moveTo(i*cs,6*cs); ctx.lineTo(i*cs,9*cs); ctx.stroke();
ctx.beginPath(); ctx.moveTo((i+9)*cs,6*cs); ctx.lineTo((i+9)*cs,9*cs); ctx.stroke();
}
// Home columns (colored)
// Red home column (top, column 7 going down)
for (let i = 1; i <= 5; i++) {
ctx.fillStyle = COLORS_LIGHT[0];
ctx.fillRect(cell * 7 + 1, i * cell + 1, cell - 2, cell - 2);
}
// Blue home column (right, row 7 going left)
for (let i = 1; i <= 5; i++) {
ctx.fillStyle = COLORS_LIGHT[1];
ctx.fillRect((14 - i) * cell + 1, cell * 7 + 1, cell - 2, cell - 2);
}
// Green home column (bottom, column 7 going up)
for (let i = 1; i <= 5; i++) {
ctx.fillStyle = COLORS_LIGHT[2];
ctx.fillRect(cell * 7 + 1, (14 - i) * cell + 1, cell - 2, cell - 2);
}
// Yellow home column (left, row 7 going right)
for (let i = 1; i <= 5; i++) {
ctx.fillStyle = COLORS_LIGHT[3];
ctx.fillRect(i * cell + 1, cell * 7 + 1, cell - 2, cell - 2);
}
// Home columns
for (let p = 0; p < 4; p++) HOME_COLUMNS[p].forEach(([col,row]) => { ctx.fillStyle = COLORS_LIGHT[p]; ctx.fillRect(col*cs+1, row*cs+1, cs-2, cs-2); });
// Safe squares
SAFE_SQUARES.forEach(idx => { const [col,row] = SHARED_PATH[idx]; ctx.fillStyle = '#FFF9C4'; ctx.fillRect(col*cs+1,row*cs+1,cs-2,cs-2); ctx.fillStyle = '#F9A825'; ctx.font = `${cs*0.45}px sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('★', col*cs+cs/2, row*cs+cs/2); });
// Center triangle finish
ctx.fillStyle = COLORS[0]; ctx.beginPath();
ctx.moveTo(cell * 6, cell * 6); ctx.lineTo(cell * 7.5, cell * 7.5); ctx.lineTo(cell * 6, cell * 9); ctx.fill();
ctx.fillStyle = COLORS[1]; ctx.beginPath();
ctx.moveTo(cell * 6, cell * 6); ctx.lineTo(cell * 7.5, cell * 7.5); ctx.lineTo(cell * 9, cell * 6); ctx.fill();
ctx.fillStyle = COLORS[2]; ctx.beginPath();
ctx.moveTo(cell * 9, cell * 9); ctx.lineTo(cell * 7.5, cell * 7.5); ctx.lineTo(cell * 9, cell * 6); ctx.fill();
ctx.fillStyle = COLORS[3]; ctx.beginPath();
ctx.moveTo(cell * 9, cell * 9); ctx.lineTo(cell * 7.5, cell * 7.5); ctx.lineTo(cell * 6, cell * 9); ctx.fill();
// Center triangles
const cx = 7.5*cs, cy = 7.5*cs;
for (let i = 0; i < 4; i++) { ctx.fillStyle = COLORS[i]; ctx.beginPath(); const a = i*Math.PI/2 - Math.PI/4; ctx.moveTo(cx,cy); ctx.lineTo(cx+Math.cos(a-0.7)*cs*2, cy+Math.sin(a-0.7)*cs*2); ctx.lineTo(cx+Math.cos(a+0.7)*cs*2, cy+Math.sin(a+0.7)*cs*2); ctx.closePath(); ctx.fill(); }
// Draw pieces
// Pieces
game.players.forEach((player, pIdx) => {
player.pieces.forEach((piece) => {
player.pieces.forEach((piece, pieceIdx) => {
if (piece.finished) return;
const color = COLORS[pIdx];
if (piece.pos === -1) {
drawPieceInHome(ctx, pIdx, piece, cell, color);
} else {
const pos = getBoardPosition(piece.pos, pIdx, cell);
drawPieceAt(ctx, pos.x, pos.y, cell, color);
let pos;
if (piece.pos === -1) pos = getHomeBasePosition(pIdx, pieceIdx, cs);
else pos = getPiecePosition(piece.pos, pIdx, cs);
if (pos) {
ctx.beginPath(); ctx.arc(pos.x, pos.y+1.5, cs*0.32, 0, Math.PI*2); ctx.fillStyle = 'rgba(0,0,0,0.15)'; ctx.fill();
ctx.beginPath(); ctx.arc(pos.x, pos.y, cs*0.32, 0, Math.PI*2); ctx.fillStyle = COLORS[pIdx]; ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke();
ctx.beginPath(); ctx.arc(pos.x-cs*0.08, pos.y-cs*0.08, cs*0.1, 0, Math.PI*2); ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.fill();
}
});
});
}
function drawHomeZone(ctx, x, y, size, colorIdx) {
ctx.fillStyle = COLORS[colorIdx];
ctx.fillRect(x, y, size, size);
ctx.fillStyle = BG_COLOR;
const inset = size * 0.15;
ctx.fillRect(x + inset, y + inset, size - inset * 2, size - inset * 2);
// Draw home circles (starting positions)
const cx = x + size / 2;
const cy = y + size / 2;
const spread = size * 0.22;
const positions = [[-1,-1],[1,-1],[-1,1],[1,1]];
positions.forEach(([dx, dy]) => {
ctx.beginPath();
ctx.arc(cx + dx * spread, cy + dy * spread, size * 0.08, 0, Math.PI * 2);
ctx.fillStyle = COLORS_LIGHT[colorIdx];
ctx.fill();
ctx.strokeStyle = COLORS[colorIdx];
ctx.lineWidth = 1.5;
ctx.stroke();
});
}
function drawPieceInHome(ctx, playerIdx, piece, cell, color) {
const zones = [
{ x: 0, y: 0 }, { x: 9, y: 0 }, { x: 9, y: 9 }, { x: 0, y: 9 }
];
const zone = zones[playerIdx];
const zoneSize = cell * 6;
const cx = (zone.x * cell) + zoneSize / 2;
const cy = (zone.y * cell) + zoneSize / 2;
const spread = zoneSize * 0.22;
const idx = parseInt(piece.id.split('-')[1]);
const positions = [[-1,-1],[1,-1],[-1,1],[1,1]];
const [dx, dy] = positions[idx];
drawPieceAt(ctx, cx + dx * spread, cy + dy * spread, cell, color);
}
function drawPieceAt(ctx, x, y, cell, color) {
const r = cell * 0.35;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
// Inner highlight
ctx.beginPath();
ctx.arc(x - r * 0.2, y - r * 0.2, r * 0.3, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.fill();
}
function getBoardPosition(localPos, playerIdx, cell) {
// Simplified position mapping - places pieces around the board
const startSquares = [
{ x: 6, y: 1 }, { x: 13, y: 6 }, { x: 8, y: 13 }, { x: 1, y: 8 }
];
const start = startSquares[playerIdx];
// Just place at approximate position for now
const angle = ((localPos + playerIdx * 13) / 52) * Math.PI * 2;
const radius = cell * 5.5;
const centerX = cell * 7.5;
const centerY = cell * 7.5;
return {
x: centerX + Math.cos(angle) * radius,
y: centerY + Math.sin(angle) * radius
};
}
function updateTurnDisplay(el) {
const turnEl = el.querySelector('#ludo-turn');
if (game.currentPlayer === 0) {
turnEl.textContent = t('game.your_turn');
turnEl.style.color = COLORS[0];
} else {
turnEl.textContent = `Bot ${game.currentPlayer}`;
turnEl.style.color = COLORS[game.currentPlayer];
function updatePanels(el) {
for (let i = 0; i < 4; i++) {
const p = el.querySelector(`#pp-${i}`);
if (p) p.classList.toggle('active', i === game.currentPlayer);
}
}
function endGame(el) {
game.gameOver = true;
const result = game.winners[0] === 0 ? 'win' : 'loss';
audio.play(result === 'win' ? 'win' : 'lose', 'game');
setTimeout(() => {
scene.exitGameMode();
scene.replace('ludo-result', { result, winners: game.winners });
bus.emit('game:ended', { gameKey: 'ludo', result });
}, 1000);
if (result === 'win') { juice.confetti(window.innerWidth/2, window.innerHeight/3, 40); juice.hapticSuccess(); audio.play('win','reward'); }
else { audio.play('lose','game'); juice.hapticError(); }
setTimeout(() => { scene.exitGameMode(); scene.replace('ludo-result', { result, winners: game.winners }); bus.emit('game:ended', { gameKey: 'ludo', result }); }, 1500);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment