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
// Board is 15x15 grid. Each position is [col, row] from top-left (0,0)
// Ludo Board Map — CORRECT layout matching official Ludo board
// 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 = [
[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
// Red's start column (top, going down) — squares 0-4
[6,1],[6,2],[6,3],[6,4],[6,5],
// Top-left row (going left) — squares 5-10
[5,6],[4,6],[3,6],[2,6],[1,6],[0,6],
// Left corner turn — square 11
[0,7],
// Bottom-left row (going right) — squares 12-17
[0,8],[1,8],[2,8],[3,8],[4,8],[5,8],
// Green's start column (left-bottom, going down) — squares 18-23
[6,9],[6,10],[6,11],[6,12],[6,13],[6,14],
// Bottom corner turn — square 24
[7,14],
// 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)
export const START_SQUARES = [1, 14, 27, 40]; // Red, Blue, Green, Yellow
// Starting squares for each player (where piece enters the shared path)
// 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)
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
[[7,1],[7,2],[7,3],[7,4],[7,5],[7,6]], // Red: center column going down
[[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]], // Yellow: center column going up
[[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)
export const HOME_ENTRY = [0, 13, 26, 39]; // The square just before home column
// The square BEFORE entering home column (last shared square before home)
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];
// Home base positions — 4 pieces in 2x2 grid, centered in each home zone
// gridToPixel adds cellSize/2 to convert grid coords to pixel center
// 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
// Home base piece positions (where 4 pieces sit before entering the board)
// Red(TL), Blue(TR), Yellow(BR), Green(BL)
export const HOME_BASES = [
[[1.5,1.5],[3.5,1.5],[1.5,3.5],[3.5,3.5]], // Red (top-left)
[[10.5,1.5],[12.5,1.5],[10.5,3.5],[12.5,3.5]], // Blue (top-right)
[[10.5,10.5],[12.5,10.5],[10.5,12.5],[12.5,12.5]], // Green (bottom-right)
[[1.5,10.5],[3.5,10.5],[1.5,12.5],[3.5,12.5]], // Yellow (bottom-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, zone 9-14)
[[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]], // 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) {
return {
x: col * cellSize + cellSize / 2,
......@@ -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) {
if (localPos === -1) return null; // In home base — handled separately
if (localPos === -1) return null; // In home base
if (localPos >= 52) {
// In home column
// In home column (positions 52-57)
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
// Finished — center of board
return gridToPixel(7, 7, cellSize);
}
// On shared path — convert local position to global
......@@ -75,7 +92,7 @@ export function getPiecePosition(localPos, playerIdx, 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) {
const [col, row] = HOME_BASES[playerIdx][pieceIdx];
return gridToPixel(col, row, cellSize);
......
const BOARD_SIZE = 52;
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 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 COLOR_CSS = ['#EF4444', '#3B82F6', '#10B981', '#F59E0B'];
export const COLORS = ['red', 'blue', 'yellow', 'green'];
export const COLOR_CSS = ['#E53935', '#1E88E5', '#FDD835', '#43A047'];
export function createGame(numPlayers = 4) {
const players = [];
......@@ -19,7 +18,8 @@ export function createGame(numPlayers = 4) {
{ id: `${i}-2`, pos: -1, finished: false },
{ id: `${i}-3`, pos: -1, finished: false }
],
finished: false
finished: false,
consecutiveSixes: 0
});
}
return {
......@@ -46,25 +46,28 @@ export function getValidMoves(game, playerIdx, dice) {
if (piece.finished) continue;
if (piece.pos === -1) {
// Can only enter board on a 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 {
const globalPos = toGlobalPos(piece.pos, playerIdx);
const newLocalPos = piece.pos + dice;
if (newLocalPos === BOARD_SIZE + HOME_STRETCH) {
moves.push({ pieceId: piece.id, from: piece.pos, to: newLocalPos, type: 'finish' });
} else if (newLocalPos < BOARD_SIZE + HOME_STRETCH) {
const isHomeStretch = newLocalPos >= BOARD_SIZE;
if (!isHomeStretch) {
const newGlobal = toGlobalPos(newLocalPos, playerIdx);
const captured = checkCapture(game, playerIdx, newGlobal);
moves.push({ pieceId: piece.id, from: piece.pos, to: newLocalPos, type: captured ? 'capture' : 'move' });
const newPos = piece.pos + dice;
// Exact finish: must land exactly on position 52+6=58
if (newPos === BOARD_SIZE + HOME_STRETCH) {
moves.push({ pieceId: piece.id, from: piece.pos, to: newPos, type: 'finish' });
} else if (newPos < BOARD_SIZE + HOME_STRETCH) {
if (newPos >= BOARD_SIZE) {
// Entering home column — always safe, can't be captured
moves.push({ pieceId: piece.id, from: piece.pos, to: newPos, type: 'home' });
} 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) {
const piece = player.pieces.find(p => p.id === move.pieceId);
if (!piece) return game;
game.extraTurn = false;
if (move.type === 'enter') {
piece.pos = 0;
// Check for capture at start position
const globalPos = START_POSITIONS[playerIdx];
captureAt(game, playerIdx, globalPos);
game.extraTurn = true; // 6 gives extra turn
} else if (move.type === 'finish') {
piece.pos = move.to;
piece.finished = true;
game.extraTurn = true; // Finishing gives extra turn
// Check if player won
if (player.pieces.every(p => p.finished)) {
player.finished = true;
game.winners.push(playerIdx);
......@@ -92,14 +101,29 @@ export function applyMove(game, playerIdx, move) {
}
} else if (move.type === 'capture') {
piece.pos = move.to;
const globalPos = toGlobalPos(move.to, playerIdx);
const globalPos = (move.to + START_POSITIONS[playerIdx]) % BOARD_SIZE;
captureAt(game, playerIdx, globalPos);
game.extraTurn = true;
game.extraTurn = true; // Capture gives extra turn
} else if (move.type === 'home') {
piece.pos = move.to;
} else {
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;
}
......@@ -114,18 +138,17 @@ export function nextTurn(game) {
} 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) {
// Can't capture on safe squares
if (SAFE_SQUARES.includes(globalPos)) return false;
for (let i = 0; i < game.numPlayers; i++) {
if (i === playerIdx) continue;
for (const piece of game.players[i].pieces) {
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;
}
}
......@@ -134,13 +157,15 @@ function checkCapture(game, playerIdx, globalPos) {
function captureAt(game, playerIdx, globalPos) {
if (SAFE_SQUARES.includes(globalPos)) return;
for (let i = 0; i < game.numPlayers; i++) {
if (i === playerIdx) continue;
for (const piece of game.players[i].pieces) {
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) {
piece.pos = -1;
piece.pos = -1; // Send back to home
}
}
}
......@@ -149,11 +174,18 @@ function captureAt(game, playerIdx, globalPos) {
export function getBotMove(game, playerIdx, dice) {
const moves = getValidMoves(game, playerIdx, dice);
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');
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');
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';
let game, validMoves, ctx, canvas, boardSize, cellSize;
let diceAnimating = false;
const COLORS = ['#E53935', '#1E88E5', '#43A047', '#FDD835'];
const COLORS_LIGHT = ['#EF9A9A', '#90CAF9', '#A5D6A7', '#FFF59D'];
// Order: Red(TL), Blue(TR), Yellow(BR), Green(BL) — matches reference image
const COLORS = ['#E53935', '#1E88E5', '#FDD835', '#43A047'];
const COLORS_LIGHT = ['#EF9A9A', '#90CAF9', '#FFF59D', '#A5D6A7'];
let PLAYER_NAMES = ['أنت', 'Bot 1', 'Bot 2', 'Bot 3'];
let livePoller = null;
let myPlayerIndex = 0;
......@@ -220,27 +221,7 @@ function handleRoll(el) {
}
function doMove(el, move) {
// For single-piece auto-moves and bot moves — use animateMove
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) {
......
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