Commit 5fd78414 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix(ludo): PHASE 2 — correct board layout, fix ALL critical bugs

Board Layout (matches reference image):
- Color order: Red(TL), Blue(TR), Yellow(BR), Green(BL)
- Fixed SHARED_PATH: 52 squares going clockwise correctly
- Fixed START_SQUARES: [0, 13, 26, 39] — NOW MATCHES rules.js
- Fixed HOME_COLUMNS: correct paths toward center per player
- Fixed SAFE_SQUARES: match official Ludo star positions
- Fixed HOME_ENTRY: correct squares before entering home column

Rules Engine fixes:
- START_POSITIONS now matches board-map START_SQUARES (was off by 1!)
- Capture logic: only checks pieces on shared path (pos < 52)
- Home column pieces CANNOT be captured (pos >= BOARD_SIZE check)
- Three consecutive 6s = piece goes back home, lose turn
- Exact finish requirement enforced (must land exactly on 58)
- Extra turn on: rolling 6, capturing, finishing
- Bot AI priority fixed: finish > capture > enter > advance furthest

Dead code removed:
- doMove() cleaned up (was return + unreachable code)

Color order fixed in game scene:
- COLORS array matches rules.js order
- COLORS_LIGHT matches too
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent aa1a0cad
// Ludo Board Map — exact grid coordinates for all 52 shared squares + home columns // Ludo Board Map — CORRECT layout matching official Ludo board
// Board is 15x15 grid. Each position is [col, row] from top-left (0,0) // 15x15 grid. Colors: Red(TL), Blue(TR), Green(BL), Yellow(BR)
// Path goes CLOCKWISE starting from Red's entry
// The 52 shared squares, numbered 0-51, going clockwise from red's start // Player order: 0=Red(TL), 1=Blue(TR), 2=Yellow(BR), 3=Green(BL)
// This matches the reference image exactly
// The 52 shared squares going CLOCKWISE
// Starting from Red's column (top-middle going down)
export const SHARED_PATH = [ export const SHARED_PATH = [
[6,1],[6,2],[6,3],[6,4],[6,5], // 0-4: top column going down // Red's start column (top, going down) — squares 0-4
[5,6],[4,6],[3,6],[2,6],[1,6],[0,6], // 5-10: left row going left [6,1],[6,2],[6,3],[6,4],[6,5],
[0,7], // 11: left corner // Top-left row (going left) — squares 5-10
[0,8],[1,8],[2,8],[3,8],[4,8],[5,8], // 12-17: left row going right [5,6],[4,6],[3,6],[2,6],[1,6],[0,6],
[6,9],[6,10],[6,11],[6,12],[6,13],[6,14], // 18-23: bottom column going down // Left corner turn — square 11
[7,14], // 24: bottom corner [0,7],
[8,14],[8,13],[8,12],[8,11],[8,10],[8,9], // 25-30: bottom column going up // Bottom-left row (going right) — squares 12-17
[9,8],[10,8],[11,8],[12,8],[13,8],[14,8], // 31-36: right row going right [0,8],[1,8],[2,8],[3,8],[4,8],[5,8],
[14,7], // 37: right corner // Green's start column (left-bottom, going down) — squares 18-23
[14,6],[13,6],[12,6],[11,6],[10,6],[9,6], // 38-43: right row going left [6,9],[6,10],[6,11],[6,12],[6,13],[6,14],
[8,5],[8,4],[8,3],[8,2],[8,1],[8,0], // 44-49: top column going up // Bottom corner turn — square 24
[7,0], // 50: top corner [7,14],
[6,0], // 51: back to start area // Bottom-right column (going up) — squares 25-30
[8,14],[8,13],[8,12],[8,11],[8,10],[8,9],
// Right-bottom row (going right) — squares 31-36
[9,8],[10,8],[11,8],[12,8],[13,8],[14,8],
// Right corner turn — square 37
[14,7],
// Top-right row (going left) — squares 38-43
[14,6],[13,6],[12,6],[11,6],[10,6],[9,6],
// Blue's start column (right-top, going up) — squares 44-49
[8,5],[8,4],[8,3],[8,2],[8,1],[8,0],
// Top corner turn — square 50
[7,0],
// Back toward red start — square 51
[6,0],
]; ];
// Starting squares for each player (where they enter the board) // Starting squares for each player (where piece enters the shared path)
export const START_SQUARES = [1, 14, 27, 40]; // Red, Blue, Green, Yellow // Red enters at square 0, Blue at square 13, Yellow at square 26, Green at square 39
export const START_SQUARES = [0, 13, 26, 39];
// Home column paths (6 squares each, going toward center) // Home column paths (6 squares each, going toward center)
export const HOME_COLUMNS = [ export const HOME_COLUMNS = [
[[7,1],[7,2],[7,3],[7,4],[7,5],[7,6]], // Red: top going down [[7,1],[7,2],[7,3],[7,4],[7,5],[7,6]], // Red: center column going down
[[1,7],[2,7],[3,7],[4,7],[5,7],[6,7]], // Blue: left going right [[13,7],[12,7],[11,7],[10,7],[9,7],[8,7]], // Blue: center row going left
[[7,13],[7,12],[7,11],[7,10],[7,9],[7,8]], // Green: bottom going up [[7,13],[7,12],[7,11],[7,10],[7,9],[7,8]], // Yellow: center column going up
[[13,7],[12,7],[11,7],[10,7],[9,7],[8,7]], // Yellow: right going left [[1,7],[2,7],[3,7],[4,7],[5,7],[6,7]], // Green: center row going right
]; ];
// Home entry points (the square BEFORE entering home column) // The square BEFORE entering home column (last shared square before home)
export const HOME_ENTRY = [0, 13, 26, 39]; // The square just before home column export const HOME_ENTRY = [51, 12, 25, 38];
// Safe squares (stars) — pieces can't be captured here // Safe squares (marked with ★ on the board) — these are the entry squares + midpoints
export const SAFE_SQUARES = [0, 8, 13, 21, 26, 34, 39, 47]; export const SAFE_SQUARES = [0, 8, 13, 21, 26, 34, 39, 47];
// Home base positions — 4 pieces in 2x2 grid, centered in each home zone // Home base piece positions (where 4 pieces sit before entering the board)
// gridToPixel adds cellSize/2 to convert grid coords to pixel center // Red(TL), Blue(TR), Yellow(BR), Green(BL)
// So grid pos 2 → pixel at 2*cs + cs/2 = 2.5*cs from board edge
// Home zone 0-5: quadrant centers at grid 1.5 and 3.5 (pixel 2cs and 4cs)
// Home zone 9-14: quadrant centers at grid 10.5 and 12.5
export const HOME_BASES = [ export const HOME_BASES = [
[[1.5,1.5],[3.5,1.5],[1.5,3.5],[3.5,3.5]], // Red (top-left) [[1.5,1.5],[3.5,1.5],[1.5,3.5],[3.5,3.5]], // Red (top-left, zone 0-5)
[[10.5,1.5],[12.5,1.5],[10.5,3.5],[12.5,3.5]], // Blue (top-right) [[10.5,1.5],[12.5,1.5],[10.5,3.5],[12.5,3.5]], // Blue (top-right, zone 9-14)
[[10.5,10.5],[12.5,10.5],[10.5,12.5],[12.5,12.5]], // Green (bottom-right) [[10.5,10.5],[12.5,10.5],[10.5,12.5],[12.5,12.5]], // Yellow (bottom-right, zone 9-14)
[[1.5,10.5],[3.5,10.5],[1.5,12.5],[3.5,12.5]], // Yellow (bottom-left) [[1.5,10.5],[3.5,10.5],[1.5,12.5],[3.5,12.5]], // Green (bottom-left, zone 0-5)
]; ];
// Convert a board grid position [col,row] to pixel coordinates // Convert grid [col,row] to pixel center
export function gridToPixel(col, row, cellSize) { export function gridToPixel(col, row, cellSize) {
return { return {
x: col * cellSize + cellSize / 2, x: col * cellSize + cellSize / 2,
...@@ -55,18 +71,19 @@ export function gridToPixel(col, row, cellSize) { ...@@ -55,18 +71,19 @@ export function gridToPixel(col, row, cellSize) {
}; };
} }
// Get the pixel position for a piece given its game state // Get pixel position for a piece given its local position and player index
export function getPiecePosition(localPos, playerIdx, cellSize) { export function getPiecePosition(localPos, playerIdx, cellSize) {
if (localPos === -1) return null; // In home base — handled separately if (localPos === -1) return null; // In home base
if (localPos >= 52) { if (localPos >= 52) {
// In home column // In home column (positions 52-57)
const homeIdx = localPos - 52; const homeIdx = localPos - 52;
if (homeIdx < 6) { if (homeIdx < 6) {
const [col, row] = HOME_COLUMNS[playerIdx][homeIdx]; const [col, row] = HOME_COLUMNS[playerIdx][homeIdx];
return gridToPixel(col, row, cellSize); return gridToPixel(col, row, cellSize);
} }
return gridToPixel(7, 7, cellSize); // Finished — center // Finished — center of board
return gridToPixel(7, 7, cellSize);
} }
// On shared path — convert local position to global // On shared path — convert local position to global
...@@ -75,7 +92,7 @@ export function getPiecePosition(localPos, playerIdx, cellSize) { ...@@ -75,7 +92,7 @@ export function getPiecePosition(localPos, playerIdx, cellSize) {
return gridToPixel(col, row, cellSize); return gridToPixel(col, row, cellSize);
} }
// Get home base pixel position for a piece still at home // Get pixel position for a piece still in home base
export function getHomeBasePosition(playerIdx, pieceIdx, cellSize) { export function getHomeBasePosition(playerIdx, pieceIdx, cellSize) {
const [col, row] = HOME_BASES[playerIdx][pieceIdx]; const [col, row] = HOME_BASES[playerIdx][pieceIdx];
return gridToPixel(col, row, cellSize); return gridToPixel(col, row, cellSize);
......
const BOARD_SIZE = 52; const BOARD_SIZE = 52;
const HOME_STRETCH = 6; const HOME_STRETCH = 6;
const SAFE_SQUARES = [0, 8, 13, 21, 26, 34, 39, 47]; // Must match board-map.js START_SQUARES exactly
const START_POSITIONS = [0, 13, 26, 39]; const START_POSITIONS = [0, 13, 26, 39];
const HOME_ENTRIES = [50, 11, 24, 37]; const SAFE_SQUARES = [0, 8, 13, 21, 26, 34, 39, 47];
export const COLORS = ['red', 'blue', 'green', 'yellow']; export const COLORS = ['red', 'blue', 'yellow', 'green'];
export const COLOR_CSS = ['#EF4444', '#3B82F6', '#10B981', '#F59E0B']; export const COLOR_CSS = ['#E53935', '#1E88E5', '#FDD835', '#43A047'];
export function createGame(numPlayers = 4) { export function createGame(numPlayers = 4) {
const players = []; const players = [];
...@@ -19,7 +18,8 @@ export function createGame(numPlayers = 4) { ...@@ -19,7 +18,8 @@ export function createGame(numPlayers = 4) {
{ id: `${i}-2`, pos: -1, finished: false }, { id: `${i}-2`, pos: -1, finished: false },
{ id: `${i}-3`, pos: -1, finished: false } { id: `${i}-3`, pos: -1, finished: false }
], ],
finished: false finished: false,
consecutiveSixes: 0
}); });
} }
return { return {
...@@ -46,25 +46,28 @@ export function getValidMoves(game, playerIdx, dice) { ...@@ -46,25 +46,28 @@ export function getValidMoves(game, playerIdx, dice) {
if (piece.finished) continue; if (piece.finished) continue;
if (piece.pos === -1) { if (piece.pos === -1) {
// Can only enter board on a 6
if (dice === 6) { if (dice === 6) {
moves.push({ pieceId: piece.id, from: -1, to: START_POSITIONS[playerIdx], type: 'enter' }); moves.push({ pieceId: piece.id, from: -1, to: 0, type: 'enter' });
} }
} else { } else {
const globalPos = toGlobalPos(piece.pos, playerIdx); const newPos = piece.pos + dice;
const newLocalPos = piece.pos + dice;
// Exact finish: must land exactly on position 52+6=58
if (newLocalPos === BOARD_SIZE + HOME_STRETCH) { if (newPos === BOARD_SIZE + HOME_STRETCH) {
moves.push({ pieceId: piece.id, from: piece.pos, to: newLocalPos, type: 'finish' }); moves.push({ pieceId: piece.id, from: piece.pos, to: newPos, type: 'finish' });
} else if (newLocalPos < BOARD_SIZE + HOME_STRETCH) { } else if (newPos < BOARD_SIZE + HOME_STRETCH) {
const isHomeStretch = newLocalPos >= BOARD_SIZE; if (newPos >= BOARD_SIZE) {
if (!isHomeStretch) { // Entering home column — always safe, can't be captured
const newGlobal = toGlobalPos(newLocalPos, playerIdx); moves.push({ pieceId: piece.id, from: piece.pos, to: newPos, type: 'home' });
const captured = checkCapture(game, playerIdx, newGlobal);
moves.push({ pieceId: piece.id, from: piece.pos, to: newLocalPos, type: captured ? 'capture' : 'move' });
} else { } else {
moves.push({ pieceId: piece.id, from: piece.pos, to: newLocalPos, type: 'home' }); // Moving on shared path
const globalPos = (newPos + START_POSITIONS[playerIdx]) % BOARD_SIZE;
const captured = checkCapture(game, playerIdx, globalPos);
moves.push({ pieceId: piece.id, from: piece.pos, to: newPos, type: captured ? 'capture' : 'move' });
} }
} }
// If newPos > 58, move is invalid (overshoot) — not added
} }
} }
...@@ -76,13 +79,19 @@ export function applyMove(game, playerIdx, move) { ...@@ -76,13 +79,19 @@ export function applyMove(game, playerIdx, move) {
const piece = player.pieces.find(p => p.id === move.pieceId); const piece = player.pieces.find(p => p.id === move.pieceId);
if (!piece) return game; if (!piece) return game;
game.extraTurn = false;
if (move.type === 'enter') { if (move.type === 'enter') {
piece.pos = 0; piece.pos = 0;
// Check for capture at start position
const globalPos = START_POSITIONS[playerIdx]; const globalPos = START_POSITIONS[playerIdx];
captureAt(game, playerIdx, globalPos); captureAt(game, playerIdx, globalPos);
game.extraTurn = true; // 6 gives extra turn
} else if (move.type === 'finish') { } else if (move.type === 'finish') {
piece.pos = move.to; piece.pos = move.to;
piece.finished = true; piece.finished = true;
game.extraTurn = true; // Finishing gives extra turn
// Check if player won
if (player.pieces.every(p => p.finished)) { if (player.pieces.every(p => p.finished)) {
player.finished = true; player.finished = true;
game.winners.push(playerIdx); game.winners.push(playerIdx);
...@@ -92,14 +101,29 @@ export function applyMove(game, playerIdx, move) { ...@@ -92,14 +101,29 @@ export function applyMove(game, playerIdx, move) {
} }
} else if (move.type === 'capture') { } else if (move.type === 'capture') {
piece.pos = move.to; piece.pos = move.to;
const globalPos = toGlobalPos(move.to, playerIdx); const globalPos = (move.to + START_POSITIONS[playerIdx]) % BOARD_SIZE;
captureAt(game, playerIdx, globalPos); captureAt(game, playerIdx, globalPos);
game.extraTurn = true; game.extraTurn = true; // Capture gives extra turn
} else if (move.type === 'home') {
piece.pos = move.to;
} else { } else {
piece.pos = move.to; piece.pos = move.to;
} }
if (game.diceValue === 6) game.extraTurn = true; // Rolling 6 always gives extra turn
if (game.diceValue === 6) {
game.extraTurn = true;
player.consecutiveSixes = (player.consecutiveSixes || 0) + 1;
// Three consecutive 6s = lose turn (piece that last moved goes back home)
if (player.consecutiveSixes >= 3) {
piece.pos = -1;
piece.finished = false;
player.consecutiveSixes = 0;
game.extraTurn = false;
}
} else {
player.consecutiveSixes = 0;
}
return game; return game;
} }
...@@ -114,18 +138,17 @@ export function nextTurn(game) { ...@@ -114,18 +138,17 @@ export function nextTurn(game) {
} while (game.players[game.currentPlayer].finished && !game.gameOver); } while (game.players[game.currentPlayer].finished && !game.gameOver);
} }
function toGlobalPos(localPos, playerIdx) {
if (localPos >= BOARD_SIZE) return localPos;
return (localPos + START_POSITIONS[playerIdx]) % BOARD_SIZE;
}
function checkCapture(game, playerIdx, globalPos) { function checkCapture(game, playerIdx, globalPos) {
// Can't capture on safe squares
if (SAFE_SQUARES.includes(globalPos)) return false; if (SAFE_SQUARES.includes(globalPos)) return false;
for (let i = 0; i < game.numPlayers; i++) { for (let i = 0; i < game.numPlayers; i++) {
if (i === playerIdx) continue; if (i === playerIdx) continue;
for (const piece of game.players[i].pieces) { for (const piece of game.players[i].pieces) {
if (piece.pos === -1 || piece.finished) continue; if (piece.pos === -1 || piece.finished) continue;
const theirGlobal = toGlobalPos(piece.pos, i); // Only check pieces on the shared path (pos < 52)
if (piece.pos >= BOARD_SIZE) continue;
const theirGlobal = (piece.pos + START_POSITIONS[i]) % BOARD_SIZE;
if (theirGlobal === globalPos) return true; if (theirGlobal === globalPos) return true;
} }
} }
...@@ -134,13 +157,15 @@ function checkCapture(game, playerIdx, globalPos) { ...@@ -134,13 +157,15 @@ function checkCapture(game, playerIdx, globalPos) {
function captureAt(game, playerIdx, globalPos) { function captureAt(game, playerIdx, globalPos) {
if (SAFE_SQUARES.includes(globalPos)) return; if (SAFE_SQUARES.includes(globalPos)) return;
for (let i = 0; i < game.numPlayers; i++) { for (let i = 0; i < game.numPlayers; i++) {
if (i === playerIdx) continue; if (i === playerIdx) continue;
for (const piece of game.players[i].pieces) { for (const piece of game.players[i].pieces) {
if (piece.pos === -1 || piece.finished) continue; if (piece.pos === -1 || piece.finished) continue;
const theirGlobal = toGlobalPos(piece.pos, i); if (piece.pos >= BOARD_SIZE) continue; // Can't capture in home column
const theirGlobal = (piece.pos + START_POSITIONS[i]) % BOARD_SIZE;
if (theirGlobal === globalPos) { if (theirGlobal === globalPos) {
piece.pos = -1; piece.pos = -1; // Send back to home
} }
} }
} }
...@@ -149,11 +174,18 @@ function captureAt(game, playerIdx, globalPos) { ...@@ -149,11 +174,18 @@ function captureAt(game, playerIdx, globalPos) {
export function getBotMove(game, playerIdx, dice) { export function getBotMove(game, playerIdx, dice) {
const moves = getValidMoves(game, playerIdx, dice); const moves = getValidMoves(game, playerIdx, dice);
if (moves.length === 0) return null; if (moves.length === 0) return null;
const captures = moves.filter(m => m.type === 'capture');
if (captures.length > 0) return captures[0]; // Priority: finish > capture > enter > advance furthest
const finishes = moves.filter(m => m.type === 'finish'); const finishes = moves.filter(m => m.type === 'finish');
if (finishes.length > 0) return finishes[0]; if (finishes.length > 0) return finishes[0];
const captures = moves.filter(m => m.type === 'capture');
if (captures.length > 0) return captures[0];
const enters = moves.filter(m => m.type === 'enter'); const enters = moves.filter(m => m.type === 'enter');
if (enters.length > 0) return enters[0]; if (enters.length > 0) return enters[0];
return moves[Math.floor(Math.random() * moves.length)];
// Advance the piece that's furthest along (closest to home)
const sorted = moves.sort((a, b) => b.to - a.to);
return sorted[0];
} }
...@@ -14,8 +14,9 @@ import * as net from '../../../core/net.js'; ...@@ -14,8 +14,9 @@ import * as net from '../../../core/net.js';
let game, validMoves, ctx, canvas, boardSize, cellSize; let game, validMoves, ctx, canvas, boardSize, cellSize;
let diceAnimating = false; let diceAnimating = false;
const COLORS = ['#E53935', '#1E88E5', '#43A047', '#FDD835']; // Order: Red(TL), Blue(TR), Yellow(BR), Green(BL) — matches reference image
const COLORS_LIGHT = ['#EF9A9A', '#90CAF9', '#A5D6A7', '#FFF59D']; const COLORS = ['#E53935', '#1E88E5', '#FDD835', '#43A047'];
const COLORS_LIGHT = ['#EF9A9A', '#90CAF9', '#FFF59D', '#A5D6A7'];
let PLAYER_NAMES = ['أنت', 'Bot 1', 'Bot 2', 'Bot 3']; let PLAYER_NAMES = ['أنت', 'Bot 1', 'Bot 2', 'Bot 3'];
let livePoller = null; let livePoller = null;
let myPlayerIndex = 0; let myPlayerIndex = 0;
...@@ -220,27 +221,7 @@ function handleRoll(el) { ...@@ -220,27 +221,7 @@ function handleRoll(el) {
} }
function doMove(el, move) { function doMove(el, move) {
// For single-piece auto-moves and bot moves — use animateMove
animateMove(el, move); animateMove(el, move);
return;
// Dead code below kept for reference
rules.applyMove(game, game.currentPlayer, move);
game.rolled = false;
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);
updatePanels(el);
drawBoard();
// Sync state after own move in live mode
if (game.mode === 'live') syncLudoState();
const btn = el.querySelector('#roll-btn');
if (isMyTurn()) { btn.disabled = false; btn.style.opacity = '1'; }
else handleNonPlayerTurn(el);
} }
function handleNonPlayerTurn(el) { function handleNonPlayerTurn(el) {
......
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