Commit c1234161 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: proper game graphics — chess SVG pieces, ludo cross board, domino felt table

- Chess: vector piece paths (king, queen, rook, bishop, knight, pawn) rendered
  as proper filled shapes with stroke, not unicode characters
- Ludo: actual cross-shaped board with colored home zones, triangular center,
  home columns, and round pieces with highlights
- Domino: green felt table background, proper tiles with pip dots rendered
  correctly for each value, ivory tile color with borders
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 53a7468e
import { createCanvas, clear, getCanvasCoords } from '../../../core/canvas.js'; import { createCanvas, clear, getCanvasCoords } from '../../../core/canvas.js';
const PIECE_CHARS = { const LIGHT_SQ = '#F0D9B5';
wp: '♙', wn: '♘', wb: '♗', wr: '♖', wq: '♕', wk: '♔', const DARK_SQ = '#B58863';
bp: '♟', bn: '♞', bb: '♝', br: '♜', bq: '♛', bk: '♚' const HIGHLIGHT_LIGHT = '#CDD16A';
const HIGHLIGHT_DARK = '#AAA23A';
const CHECK_COLOR = '#FF6B6B88';
const MOVE_DOT = 'rgba(0,0,0,0.25)';
const CAPTURE_RING = 'rgba(0,0,0,0.25)';
const PIECE_PATHS = {
K: (ctx, x, y, s) => { drawPiece(ctx, x, y, s, '#fff', '#000', kingPath); },
Q: (ctx, x, y, s) => { drawPiece(ctx, x, y, s, '#fff', '#000', queenPath); },
R: (ctx, x, y, s) => { drawPiece(ctx, x, y, s, '#fff', '#000', rookPath); },
B: (ctx, x, y, s) => { drawPiece(ctx, x, y, s, '#fff', '#000', bishopPath); },
N: (ctx, x, y, s) => { drawPiece(ctx, x, y, s, '#fff', '#000', knightPath); },
P: (ctx, x, y, s) => { drawPiece(ctx, x, y, s, '#fff', '#000', pawnPath); },
k: (ctx, x, y, s) => { drawPiece(ctx, x, y, s, '#333', '#000', kingPath); },
q: (ctx, x, y, s) => { drawPiece(ctx, x, y, s, '#333', '#000', queenPath); },
r: (ctx, x, y, s) => { drawPiece(ctx, x, y, s, '#333', '#000', rookPath); },
b: (ctx, x, y, s) => { drawPiece(ctx, x, y, s, '#333', '#000', bishopPath); },
n: (ctx, x, y, s) => { drawPiece(ctx, x, y, s, '#333', '#000', knightPath); },
p: (ctx, x, y, s) => { drawPiece(ctx, x, y, s, '#333', '#000', pawnPath); },
}; };
function drawPiece(ctx, x, y, s, fill, stroke, pathFn) {
ctx.save();
ctx.translate(x, y);
ctx.scale(s / 45, s / 45);
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = 1.5;
pathFn(ctx);
ctx.restore();
}
function kingPath(ctx) {
ctx.beginPath();
ctx.moveTo(22.5, 11.63);
ctx.lineTo(22.5, 6);
ctx.moveTo(20, 8);
ctx.lineTo(25, 8);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(22.5, 25);
ctx.bezierCurveTo(22.5, 25, 27, 17.5, 25.5, 14.5);
ctx.bezierCurveTo(25.5, 14.5, 24.5, 12, 22.5, 12);
ctx.bezierCurveTo(20.5, 12, 19.5, 14.5, 19.5, 14.5);
ctx.bezierCurveTo(18, 17.5, 22.5, 25, 22.5, 25);
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(12.5, 37);
ctx.bezierCurveTo(18, 40.5, 27, 40.5, 32.5, 37);
ctx.lineTo(32.5, 30);
ctx.bezierCurveTo(32.5, 30, 41.5, 25.5, 38.5, 19.5);
ctx.bezierCurveTo(34.5, 13, 25, 16, 22.5, 23.5);
ctx.lineTo(22.5, 27);
ctx.lineTo(22.5, 23.5);
ctx.bezierCurveTo(20, 16, 10.5, 13, 6.5, 19.5);
ctx.bezierCurveTo(3.5, 25.5, 12.5, 30, 12.5, 30);
ctx.lineTo(12.5, 37);
ctx.fill();
ctx.stroke();
}
function queenPath(ctx) {
ctx.beginPath();
ctx.arc(6, 12, 2.5, 0, Math.PI * 2);
ctx.arc(14, 9, 2.5, 0, Math.PI * 2);
ctx.arc(22.5, 8, 2.5, 0, Math.PI * 2);
ctx.arc(31, 9, 2.5, 0, Math.PI * 2);
ctx.arc(39, 12, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(9, 26);
ctx.bezierCurveTo(17.5, 24.5, 30, 24.5, 36, 26);
ctx.lineTo(38.5, 13.5);
ctx.lineTo(31, 25);
ctx.lineTo(30.7, 10.9);
ctx.lineTo(25.5, 24.5);
ctx.lineTo(22.5, 10);
ctx.lineTo(19.5, 24.5);
ctx.lineTo(14.3, 10.9);
ctx.lineTo(14, 25);
ctx.lineTo(6.5, 13.5);
ctx.lineTo(9, 26);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(9, 26);
ctx.bezierCurveTo(9, 28, 10.5, 31.5, 12.5, 33);
ctx.bezierCurveTo(16, 35.5, 23, 36, 23, 36);
ctx.bezierCurveTo(23, 36, 30, 35.5, 33.5, 33);
ctx.bezierCurveTo(35.5, 31.5, 36, 28, 36, 26);
ctx.bezierCurveTo(28, 24.5, 17.5, 24.5, 9, 26);
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(12.5, 37);
ctx.bezierCurveTo(18, 40.5, 27, 40.5, 32.5, 37);
ctx.lineTo(32.5, 33.5);
ctx.bezierCurveTo(27, 36, 18, 36, 12.5, 33.5);
ctx.lineTo(12.5, 37);
ctx.fill();
ctx.stroke();
}
function rookPath(ctx) {
ctx.beginPath();
ctx.moveTo(9, 39);
ctx.lineTo(36, 39);
ctx.lineTo(36, 36);
ctx.lineTo(9, 36);
ctx.lineTo(9, 39);
ctx.fill(); ctx.stroke();
ctx.beginPath();
ctx.moveTo(12, 36);
ctx.lineTo(12, 32);
ctx.lineTo(33, 32);
ctx.lineTo(33, 36);
ctx.lineTo(12, 36);
ctx.fill(); ctx.stroke();
ctx.beginPath();
ctx.moveTo(11, 14);
ctx.lineTo(11, 9);
ctx.lineTo(15, 9);
ctx.lineTo(15, 11);
ctx.lineTo(20, 11);
ctx.lineTo(20, 9);
ctx.lineTo(25, 9);
ctx.lineTo(25, 11);
ctx.lineTo(30, 11);
ctx.lineTo(30, 9);
ctx.lineTo(34, 9);
ctx.lineTo(34, 14);
ctx.fill(); ctx.stroke();
ctx.beginPath();
ctx.moveTo(14, 32);
ctx.lineTo(11, 14);
ctx.lineTo(34, 14);
ctx.lineTo(31, 32);
ctx.lineTo(14, 32);
ctx.fill(); ctx.stroke();
}
function bishopPath(ctx) {
ctx.beginPath();
ctx.moveTo(9, 36);
ctx.bezierCurveTo(12.4, 35.4, 19.5, 34.1, 22.5, 34);
ctx.bezierCurveTo(25.5, 34.1, 32.6, 35.4, 36, 36);
ctx.bezierCurveTo(36, 36, 37.7, 36.8, 39, 38);
ctx.bezierCurveTo(38.1, 40.5, 14, 40.5, 6, 38);
ctx.bezierCurveTo(7.3, 36.8, 9, 36, 9, 36);
ctx.fill(); ctx.stroke();
ctx.beginPath();
ctx.moveTo(15, 32);
ctx.bezierCurveTo(17.5, 34.5, 27.5, 34.5, 30, 32);
ctx.bezierCurveTo(30.5, 30.5, 30, 30, 30, 30);
ctx.bezierCurveTo(30, 27.5, 27.5, 26, 27.5, 26);
ctx.bezierCurveTo(33, 24.5, 33.5, 14.5, 22.5, 10.5);
ctx.bezierCurveTo(11.5, 14.5, 12, 24.5, 17.5, 26);
ctx.bezierCurveTo(17.5, 26, 15, 27.5, 15, 30);
ctx.bezierCurveTo(15, 30, 14.5, 30.5, 15, 32);
ctx.fill(); ctx.stroke();
ctx.beginPath();
ctx.arc(22.5, 8, 2.5, 0, Math.PI * 2);
ctx.fill(); ctx.stroke();
}
function knightPath(ctx) {
ctx.beginPath();
ctx.moveTo(22, 10);
ctx.bezierCurveTo(32.5, 11, 38.5, 18, 38, 39);
ctx.lineTo(15, 39);
ctx.bezierCurveTo(15, 30, 25, 32.5, 23, 18);
ctx.fill(); ctx.stroke();
ctx.beginPath();
ctx.moveTo(24, 18);
ctx.bezierCurveTo(24.38, 20.91, 18.45, 25.37, 16, 27);
ctx.bezierCurveTo(13, 29, 13.18, 31.34, 11, 31);
ctx.bezierCurveTo(9.96, 30.06, 12.41, 27.96, 11, 28);
ctx.bezierCurveTo(10, 28, 11.19, 29.23, 10, 30);
ctx.bezierCurveTo(9, 30, 5.97, 31.34, 8, 28);
ctx.bezierCurveTo(10.91, 23.67, 12.37, 19.87, 11, 16);
ctx.bezierCurveTo(10, 13, 13, 7.5, 18, 7);
ctx.bezierCurveTo(18, 7, 20, 7.5, 22, 10);
ctx.fill(); ctx.stroke();
}
function pawnPath(ctx) {
ctx.beginPath();
ctx.arc(22.5, 12, 4, 0, Math.PI * 2);
ctx.fill(); ctx.stroke();
ctx.beginPath();
ctx.moveTo(15.5, 26);
ctx.bezierCurveTo(17, 22.5, 20, 19.5, 22.5, 18);
ctx.bezierCurveTo(25, 19.5, 28, 22.5, 29.5, 26);
ctx.bezierCurveTo(30.5, 28, 28.3, 31, 22.5, 31);
ctx.bezierCurveTo(16.7, 31, 14.5, 28, 15.5, 26);
ctx.fill(); ctx.stroke();
ctx.beginPath();
ctx.moveTo(10, 39);
ctx.bezierCurveTo(10, 36, 14, 33, 22.5, 33);
ctx.bezierCurveTo(31, 33, 35, 36, 35, 39);
ctx.lineTo(10, 39);
ctx.fill(); ctx.stroke();
}
export class ChessBoard { export class ChessBoard {
constructor(container, options = {}) { constructor(container, options = {}) {
this.flipped = options.flipped || false; this.flipped = options.flipped || false;
...@@ -31,7 +235,7 @@ export class ChessBoard { ...@@ -31,7 +235,7 @@ export class ChessBoard {
this.canvas = canvas; this.canvas = canvas;
this.ctx = ctx; this.ctx = ctx;
this.size = size; this.size = size;
this.canvas.style.cssText = 'width:100%;height:100%;display:block;border-radius:var(--r-md);'; this.canvas.style.cssText = 'width:100%;height:100%;display:block;border-radius:4px;box-shadow:0 4px 20px rgba(0,0,0,0.5);';
} }
bindEvents() { bindEvents() {
...@@ -41,19 +245,18 @@ export class ChessBoard { ...@@ -41,19 +245,18 @@ export class ChessBoard {
this.canvas.addEventListener('pointerup', (e) => this.onPointerUp(e)); this.canvas.addEventListener('pointerup', (e) => this.onPointerUp(e));
} }
setPosition(fenBoard) { setPosition(fenStr) {
this.pieces = {}; this.pieces = {};
const rows = fenBoard.split(' ')[0].split('/'); const fenBoard = fenStr.split(' ')[0];
const rows = fenBoard.split('/');
for (let r = 0; r < 8; r++) { for (let r = 0; r < 8; r++) {
let col = 0; let col = 0;
for (const ch of rows[r]) { for (const ch of rows[r]) {
if (ch >= '1' && ch <= '8') { col += parseInt(ch); } if (ch >= '1' && ch <= '8') { col += parseInt(ch); }
else { else {
const color = ch === ch.toUpperCase() ? 'w' : 'b';
const piece = color + ch.toLowerCase();
const file = String.fromCharCode(97 + col); const file = String.fromCharCode(97 + col);
const rank = String(8 - r); const rank = String(8 - r);
this.pieces[file + rank] = piece; this.pieces[file + rank] = ch;
col++; col++;
} }
} }
...@@ -66,76 +269,81 @@ export class ChessBoard { ...@@ -66,76 +269,81 @@ export class ChessBoard {
const sq = this.squareSize; const sq = this.squareSize;
clear(ctx, this.size, this.size); clear(ctx, this.size, this.size);
// Draw squares
for (let r = 0; r < 8; r++) { for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) { for (let c = 0; c < 8; c++) {
const x = c * sq; const x = c * sq;
const y = r * sq; const y = r * sq;
const isLight = (r + c) % 2 === 0; const isLight = (r + c) % 2 === 0;
ctx.fillStyle = isLight ? '#E8D5B5' : '#B58863';
const file = this.flipped ? 7 - c : c; const file = this.flipped ? 7 - c : c;
const rank = this.flipped ? r : 7 - r; const rank = this.flipped ? r : 7 - r;
const square = String.fromCharCode(97 + file) + String(rank + 1); const square = String.fromCharCode(97 + file) + String(rank + 1);
let color = isLight ? LIGHT_SQ : DARK_SQ;
if (this.highlights.lastMove && (square === this.highlights.lastMove.from || square === this.highlights.lastMove.to)) { if (this.highlights.lastMove && (square === this.highlights.lastMove.from || square === this.highlights.lastMove.to)) {
ctx.fillStyle = isLight ? '#F5F682' : '#BACA44'; color = isLight ? HIGHLIGHT_LIGHT : HIGHLIGHT_DARK;
} }
if (this.highlights.selected === square) { if (this.highlights.selected === square) {
ctx.fillStyle = isLight ? '#F5F682' : '#BACA44'; color = isLight ? HIGHLIGHT_LIGHT : HIGHLIGHT_DARK;
}
if (this.highlights.check === square) {
ctx.fillStyle = '#FF6B6B';
} }
ctx.fillStyle = color;
ctx.fillRect(x, y, sq, sq); ctx.fillRect(x, y, sq, sq);
if (this.highlights.check === square) {
ctx.fillStyle = CHECK_COLOR;
ctx.fillRect(x, y, sq, sq);
}
} }
} }
// Legal move indicators // Draw legal move indicators
for (const move of this.highlights.legalMoves) { for (const move of this.highlights.legalMoves) {
const { x, y } = this.squareToXY(move.to); const { x, y } = this.squareToXY(move.to);
ctx.beginPath();
if (this.pieces[move.to]) { if (this.pieces[move.to]) {
ctx.arc(x + sq/2, y + sq/2, sq/2 - 4, 0, Math.PI * 2); ctx.beginPath();
ctx.strokeStyle = 'rgba(0,0,0,0.3)'; ctx.arc(x + sq / 2, y + sq / 2, sq * 0.45, 0, Math.PI * 2);
ctx.lineWidth = 3; ctx.lineWidth = sq * 0.08;
ctx.strokeStyle = CAPTURE_RING;
ctx.stroke(); ctx.stroke();
} else { } else {
ctx.arc(x + sq/2, y + sq/2, sq * 0.15, 0, Math.PI * 2); ctx.beginPath();
ctx.fillStyle = 'rgba(0,0,0,0.2)'; ctx.arc(x + sq / 2, y + sq / 2, sq * 0.15, 0, Math.PI * 2);
ctx.fillStyle = MOVE_DOT;
ctx.fill(); ctx.fill();
} }
} }
// Pieces // Draw pieces
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = `${sq * 0.75}px serif`;
for (const [square, piece] of Object.entries(this.pieces)) { for (const [square, piece] of Object.entries(this.pieces)) {
if (this.dragging && this.dragging.square === square) continue; if (this.dragging && this.dragging.square === square) continue;
const { x, y } = this.squareToXY(square); const { x, y } = this.squareToXY(square);
ctx.fillStyle = '#000'; const padding = sq * 0.05;
ctx.fillText(PIECE_CHARS[piece] || '', x + sq/2, y + sq/2); if (PIECE_PATHS[piece]) {
PIECE_PATHS[piece](ctx, x + padding, y + padding, sq - padding * 2);
}
} }
// Dragging piece // Draw dragging piece
if (this.dragging) { if (this.dragging && PIECE_PATHS[this.dragging.piece]) {
ctx.font = `${sq * 0.85}px serif`; const ds = sq * 1.2;
ctx.fillStyle = '#000'; PIECE_PATHS[this.dragging.piece](ctx, this.dragging.x - ds / 2, this.dragging.y - ds / 2, ds);
ctx.fillText(PIECE_CHARS[this.dragging.piece] || '', this.dragging.x, this.dragging.y);
} }
// Coordinates // Coordinates
ctx.font = '10px Inter, sans-serif'; ctx.font = `bold ${sq * 0.22}px sans-serif`;
ctx.fillStyle = 'rgba(0,0,0,0.5)';
for (let i = 0; i < 8; i++) { for (let i = 0; i < 8; i++) {
const file = this.flipped ? 7 - i : i; const file = this.flipped ? 7 - i : i;
ctx.textAlign = 'center';
ctx.fillText(String.fromCharCode(97 + file), i * sq + sq/2, this.size - 2);
const rank = this.flipped ? i + 1 : 8 - i; const rank = this.flipped ? i + 1 : 8 - i;
const isLight = (7 + i) % 2 === 0;
ctx.fillStyle = isLight ? DARK_SQ : LIGHT_SQ;
ctx.textAlign = 'left'; ctx.textAlign = 'left';
ctx.fillText(String(rank), 2, i * sq + 12); ctx.textBaseline = 'bottom';
ctx.fillText(String.fromCharCode(97 + file), i * sq + 2, this.size - 2);
ctx.textAlign = 'right';
ctx.textBaseline = 'top';
ctx.fillText(String(rank), this.size - 2, i * sq + 2);
} }
} }
...@@ -164,7 +372,7 @@ export class ChessBoard { ...@@ -164,7 +372,7 @@ export class ChessBoard {
const piece = this.pieces[square]; const piece = this.pieces[square];
if (piece && this.canSelect(piece)) { if (piece && this.canSelect(piece)) {
this.dragging = { square, piece, x, y, startX: x, startY: y }; this.dragging = { square, piece, x, y };
this.highlights.selected = square; this.highlights.selected = square;
this.showLegalMoves(square); this.showLegalMoves(square);
this.canvas.setPointerCapture(e.pointerId); this.canvas.setPointerCapture(e.pointerId);
......
...@@ -3,10 +3,16 @@ import * as scene from '../../../core/scene.js'; ...@@ -3,10 +3,16 @@ import * as scene from '../../../core/scene.js';
import * as audio from '../../../core/audio.js'; import * as audio from '../../../core/audio.js';
import * as store from '../../../core/store.js'; import * as store from '../../../core/store.js';
import { t } from '../../../core/i18n.js'; import { t } from '../../../core/i18n.js';
import { createCanvas, clear, drawRoundRect } from '../../../core/canvas.js'; import { createCanvas, clear } from '../../../core/canvas.js';
import * as rules from '../logic/rules.js'; import * as rules from '../logic/rules.js';
let state; let state, tableCtx, tableCanvas, tableW, tableH;
const TABLE_COLOR = '#1B5E20';
const TABLE_FELT = '#2E7D32';
const TILE_BG = '#FFFFF0';
const TILE_BORDER = '#333';
const PIP_COLOR = '#111';
export function mountGame(el, params) { export function mountGame(el, params) {
const { mode = 'bot', numPlayers = 2 } = params; const { mode = 'bot', numPlayers = 2 } = params;
...@@ -24,22 +30,35 @@ export function mountGame(el, params) { ...@@ -24,22 +30,35 @@ export function mountGame(el, params) {
}; };
el.innerHTML = ` el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);"> <div style="display:flex;flex-direction:column;height:100%;background:#0d3311;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:var(--s-2) var(--s-3);background:var(--bg-base);"> <div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:#0a2a0e;border-bottom:1px solid rgba(255,255,255,0.1);">
<span style="font-size:14px;font-weight:600;">${t('game.domino')}</span> <span id="domino-turn" style="font-size:13px;font-weight:600;color:#4CAF50;">${t('game.your_turn')}</span>
<span id="domino-turn" style="font-size:13px;color:var(--domino-primary);">${t('game.your_turn')}</span> <span style="font-size:14px;font-weight:700;color:#fff;">${t('game.domino')}</span>
</div>
<div style="display:flex;justify-content:space-between;padding:4px 12px;background:#0a2a0e;">
<span style="font-size:11px;color:#81C784;">الخصم: <span id="opp-count">${hands[1]?.length || 0}</span> قطع</span>
<span style="font-size:11px;color:#81C784;">المخزن: <span id="bone-count">${boneyard.length}</span></span>
</div> </div>
<div id="domino-table" style="flex:1;position:relative;overflow:hidden;"></div> <div id="domino-table" style="flex:1;display:flex;align-items:center;justify-content:center;padding:8px;overflow:hidden;"></div>
<div id="domino-hand" style="display:flex;gap:var(--s-2);padding:var(--s-3);overflow-x:auto;background:var(--bg-base);border-top:1px solid var(--border);min-height:80px;align-items:center;"></div> <div id="domino-hand" style="display:flex;gap:6px;padding:10px 8px;overflow-x:auto;background:#0a2a0e;border-top:1px solid rgba(255,255,255,0.1);min-height:90px;align-items:center;justify-content:center;flex-wrap:wrap;"></div>
<div style="display:flex;gap:var(--s-2);padding:var(--s-2) var(--s-3);background:var(--bg-base);"> <div style="display:flex;gap:8px;padding:8px 12px;background:#0a2a0e;">
<button class="btn btn-secondary" id="domino-draw" style="flex:1;font-size:13px;">سحب</button> <button class="btn btn-secondary" id="domino-draw" style="flex:1;font-size:13px;background:#1B5E20;border-color:#388E3C;color:#fff;">سحب</button>
<button class="btn btn-secondary" id="domino-pass" style="flex:1;font-size:13px;">تمرير</button> <button class="btn btn-secondary" id="domino-pass" style="flex:1;font-size:13px;background:#1B5E20;border-color:#388E3C;color:#fff;">تمرير</button>
</div> </div>
</div> </div>
`; `;
// Setup table canvas
const tableContainer = el.querySelector('#domino-table');
tableW = tableContainer.clientWidth || 360;
tableH = tableContainer.clientHeight || 300;
const { canvas, ctx } = createCanvas(tableContainer, tableW, tableH);
canvas.style.cssText = 'width:100%;height:100%;border-radius:8px;';
tableCtx = ctx;
tableCanvas = canvas;
renderTable();
renderHand(el); renderHand(el);
renderTable(el);
el.querySelector('#domino-draw').addEventListener('click', () => drawTile(el)); el.querySelector('#domino-draw').addEventListener('click', () => drawTile(el));
el.querySelector('#domino-pass').addEventListener('click', () => passTurn(el)); el.querySelector('#domino-pass').addEventListener('click', () => passTurn(el));
...@@ -47,6 +66,92 @@ export function mountGame(el, params) { ...@@ -47,6 +66,92 @@ export function mountGame(el, params) {
bus.emit('game:started', { gameKey: 'domino', mode }); bus.emit('game:started', { gameKey: 'domino', mode });
} }
function renderTable() {
const ctx = tableCtx;
clear(ctx, tableW, tableH);
// Felt background
ctx.fillStyle = TABLE_COLOR;
ctx.fillRect(0, 0, tableW, tableH);
// Subtle texture
ctx.fillStyle = TABLE_FELT;
for (let i = 0; i < tableW; i += 20) {
for (let j = 0; j < tableH; j += 20) {
if ((i + j) % 40 === 0) ctx.fillRect(i, j, 10, 10);
}
}
// Draw chain
if (state.chain.length === 0) {
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('ابدأ اللعب', tableW / 2, tableH / 2);
return;
}
const tileW = 24;
const tileH = 44;
const gap = 2;
const totalWidth = state.chain.length * (tileW + gap);
let startX = Math.max(4, (tableW - totalWidth) / 2);
const cy = tableH / 2;
state.chain.forEach((tile, i) => {
const x = startX + i * (tileW + gap);
const y = cy - tileH / 2;
drawTileOnTable(ctx, x, y, tileW, tileH, tile);
});
}
function drawTileOnTable(ctx, x, y, w, h, tile) {
// Tile background
ctx.fillStyle = TILE_BG;
ctx.strokeStyle = TILE_BORDER;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x, y, w, h, 3);
ctx.fill();
ctx.stroke();
// Divider line
ctx.beginPath();
ctx.moveTo(x + 2, y + h / 2);
ctx.lineTo(x + w - 2, y + h / 2);
ctx.strokeStyle = '#999';
ctx.lineWidth = 1;
ctx.stroke();
// Draw pips
drawPips(ctx, x + w / 2, y + h / 4, w * 0.7, tile.left);
drawPips(ctx, x + w / 2, y + h * 3 / 4, w * 0.7, tile.right);
}
function drawPips(ctx, cx, cy, size, value) {
const r = size * 0.1;
ctx.fillStyle = PIP_COLOR;
const positions = getPipPositions(value, size * 0.3);
positions.forEach(([dx, dy]) => {
ctx.beginPath();
ctx.arc(cx + dx, cy + dy, r, 0, Math.PI * 2);
ctx.fill();
});
}
function getPipPositions(value, spread) {
switch (value) {
case 0: return [];
case 1: return [[0, 0]];
case 2: return [[-spread, -spread], [spread, spread]];
case 3: return [[-spread, -spread], [0, 0], [spread, spread]];
case 4: return [[-spread, -spread], [spread, -spread], [-spread, spread], [spread, spread]];
case 5: return [[-spread, -spread], [spread, -spread], [0, 0], [-spread, spread], [spread, spread]];
case 6: return [[-spread, -spread], [spread, -spread], [-spread, 0], [spread, 0], [-spread, spread], [spread, spread]];
default: return [];
}
}
function renderHand(el) { function renderHand(el) {
const handEl = el.querySelector('#domino-hand'); const handEl = el.querySelector('#domino-hand');
const hand = state.hands[0]; const hand = state.hands[0];
...@@ -55,15 +160,22 @@ function renderHand(el) { ...@@ -55,15 +160,22 @@ function renderHand(el) {
: rules.getValidMoves(hand, state.leftEnd, state.rightEnd); : rules.getValidMoves(hand, state.leftEnd, state.rightEnd);
const validIds = new Set(validMoves.map(m => m.tile.id)); const validIds = new Set(validMoves.map(m => m.tile.id));
handEl.innerHTML = hand.map(tile => ` handEl.innerHTML = hand.map(tile => {
<div class="domino-tile ${validIds.has(tile.id) ? 'playable' : ''} ${state.selectedTile?.id === tile.id ? 'selected' : ''}" const playable = validIds.has(tile.id);
data-id="${tile.id}" data-left="${tile.left}" data-right="${tile.right}" const selected = state.selectedTile?.id === tile.id;
style="min-width:40px;height:70px;background:${validIds.has(tile.id) ? 'var(--bg-elevated)' : 'var(--bg-card)'};border:2px solid ${state.selectedTile?.id === tile.id ? 'var(--domino-primary)' : validIds.has(tile.id) ? 'var(--border-hover)' : 'var(--border)'};border-radius:var(--r-sm);display:flex;flex-direction:column;align-items:center;justify-content:space-around;padding:4px;cursor:pointer;font-family:var(--font-lat);font-weight:700;transition:transform 0.15s;"> return `
<span style="font-size:14px;">${tile.left}</span> <div class="domino-tile ${playable ? 'playable' : ''} ${selected ? 'selected' : ''}"
<div style="width:70%;height:1px;background:var(--text-muted);"></div> data-id="${tile.id}"
<span style="font-size:14px;">${tile.right}</span> style="width:36px;height:64px;background:${TILE_BG};border:2px solid ${selected ? '#4CAF50' : playable ? '#81C784' : '#555'};border-radius:4px;display:flex;flex-direction:column;align-items:center;justify-content:space-around;padding:3px;cursor:${playable ? 'pointer' : 'default'};opacity:${playable ? '1' : '0.5'};transition:transform 0.15s;${selected ? 'transform:translateY(-6px);box-shadow:0 4px 12px rgba(76,175,80,0.4);' : ''}">
</div> <div style="display:flex;flex-wrap:wrap;width:20px;height:20px;align-items:center;justify-content:center;">
`).join(''); ${renderPipDots(tile.left)}
</div>
<div style="width:80%;height:1px;background:#999;"></div>
<div style="display:flex;flex-wrap:wrap;width:20px;height:20px;align-items:center;justify-content:center;">
${renderPipDots(tile.right)}
</div>
</div>`;
}).join('');
handEl.querySelectorAll('.domino-tile.playable').forEach(tileEl => { handEl.querySelectorAll('.domino-tile.playable').forEach(tileEl => {
tileEl.addEventListener('click', () => { tileEl.addEventListener('click', () => {
...@@ -71,7 +183,6 @@ function renderHand(el) { ...@@ -71,7 +183,6 @@ function renderHand(el) {
const id = tileEl.dataset.id; const id = tileEl.dataset.id;
const tile = hand.find(t => t.id === id); const tile = hand.find(t => t.id === id);
if (!tile) return; if (!tile) return;
if (state.selectedTile?.id === id) { if (state.selectedTile?.id === id) {
playTile(el, tile); playTile(el, tile);
} else { } else {
...@@ -82,6 +193,15 @@ function renderHand(el) { ...@@ -82,6 +193,15 @@ function renderHand(el) {
}); });
} }
function renderPipDots(value) {
if (value === 0) return '';
let dots = '';
for (let i = 0; i < value; i++) {
dots += '<div style="width:4px;height:4px;border-radius:50%;background:#111;margin:1px;"></div>';
}
return dots;
}
function playTile(el, tile) { function playTile(el, tile) {
if (state.currentPlayer !== 0 || state.gameOver) return; if (state.currentPlayer !== 0 || state.gameOver) return;
...@@ -99,8 +219,7 @@ function playTile(el, tile) { ...@@ -99,8 +219,7 @@ function playTile(el, tile) {
audio.play('place', 'game'); audio.play('place', 'game');
if (state.hands[0].length === 0 || rules.checkRoundEnd(state.hands, state.boneyard, state.chain)) { if (state.hands[0].length === 0 || rules.checkRoundEnd(state.hands, state.boneyard, state.chain)) {
endRound(el); endRound(el); return;
return;
} }
nextTurn(el); nextTurn(el);
...@@ -111,13 +230,12 @@ function drawTile(el) { ...@@ -111,13 +230,12 @@ function drawTile(el) {
const tile = state.boneyard.pop(); const tile = state.boneyard.pop();
state.hands[0].push(tile); state.hands[0].push(tile);
audio.play('click'); audio.play('click');
el.querySelector('#bone-count').textContent = state.boneyard.length;
renderHand(el); renderHand(el);
} }
function passTurn(el) { function passTurn(el) {
if (state.currentPlayer !== 0) return; if (state.currentPlayer !== 0) return;
const validMoves = rules.getValidMoves(state.hands[0], state.leftEnd, state.rightEnd);
if (validMoves.length > 0 && state.boneyard.length > 0) return;
nextTurn(el); nextTurn(el);
} }
...@@ -127,14 +245,14 @@ function nextTurn(el) { ...@@ -127,14 +245,14 @@ function nextTurn(el) {
if (state.currentPlayer === 0) { if (state.currentPlayer === 0) {
turnEl.textContent = t('game.your_turn'); turnEl.textContent = t('game.your_turn');
turnEl.style.color = 'var(--domino-primary)'; turnEl.style.color = '#4CAF50';
renderHand(el); renderHand(el);
} else { } else {
turnEl.textContent = t('game.opponent_turn'); turnEl.textContent = t('game.opponent_turn');
turnEl.style.color = 'var(--text-secondary)'; turnEl.style.color = '#81C784';
setTimeout(() => botTurn(el), 1000); setTimeout(() => botTurn(el), 800);
} }
renderTable(el); renderTable();
} }
function botTurn(el) { function botTurn(el) {
...@@ -145,11 +263,11 @@ function botTurn(el) { ...@@ -145,11 +263,11 @@ function botTurn(el) {
if (validMoves.length === 0) { if (validMoves.length === 0) {
if (state.boneyard.length > 0) { if (state.boneyard.length > 0) {
hand.push(state.boneyard.pop()); hand.push(state.boneyard.pop());
setTimeout(() => botTurn(el), 500); el.querySelector('#bone-count').textContent = state.boneyard.length;
setTimeout(() => botTurn(el), 400);
return; return;
} }
nextTurn(el); nextTurn(el); return;
return;
} }
const move = validMoves[Math.floor(Math.random() * validMoves.length)]; const move = validMoves[Math.floor(Math.random() * validMoves.length)];
...@@ -160,33 +278,18 @@ function botTurn(el) { ...@@ -160,33 +278,18 @@ function botTurn(el) {
state.hands[state.currentPlayer] = hand.filter(t => t.id !== move.tile.id); state.hands[state.currentPlayer] = hand.filter(t => t.id !== move.tile.id);
audio.play('place', 'game'); audio.play('place', 'game');
el.querySelector('#opp-count').textContent = state.hands[1]?.length || 0;
if (state.hands[state.currentPlayer].length === 0 || rules.checkRoundEnd(state.hands, state.boneyard, state.chain)) { if (state.hands[state.currentPlayer].length === 0 || rules.checkRoundEnd(state.hands, state.boneyard, state.chain)) {
endRound(el); endRound(el); return;
return;
} }
nextTurn(el); nextTurn(el);
} }
function renderTable(el) {
const tableEl = el.querySelector('#domino-table');
const chainStr = state.chain.map(t => `[${t.left}|${t.right}]`).join(' ');
tableEl.innerHTML = `
<div style="display:flex;align-items:center;justify-content:center;height:100%;padding:var(--s-4);flex-wrap:wrap;gap:4px;">
<div style="color:var(--text-secondary);font-family:var(--font-lat);font-size:13px;letter-spacing:1px;">${chainStr || 'ابدأ اللعب'}</div>
</div>
<div style="position:absolute;top:var(--s-2);right:var(--s-3);font-size:12px;color:var(--text-muted);">
المخزن: ${state.boneyard.length}
</div>
${state.numPlayers > 1 ? `<div style="position:absolute;top:var(--s-2);left:var(--s-3);font-size:12px;color:var(--text-muted);">الخصم: ${state.hands[1]?.length || 0} قطع</div>` : ''}
`;
}
function endRound(el) { function endRound(el) {
state.gameOver = true; state.gameOver = true;
const { winner, scores } = rules.getRoundWinner(state.hands); const { winner, scores } = rules.getRoundWinner(state.hands);
const result = winner === 0 ? 'win' : 'loss'; const result = winner === 0 ? 'win' : 'loss';
audio.play(result === 'win' ? 'win' : 'lose', 'game'); audio.play(result === 'win' ? 'win' : 'lose', 'game');
setTimeout(() => { setTimeout(() => {
......
...@@ -3,9 +3,16 @@ import * as scene from '../../../core/scene.js'; ...@@ -3,9 +3,16 @@ import * as scene from '../../../core/scene.js';
import * as audio from '../../../core/audio.js'; import * as audio from '../../../core/audio.js';
import * as store from '../../../core/store.js'; import * as store from '../../../core/store.js';
import { t } from '../../../core/i18n.js'; import { t } from '../../../core/i18n.js';
import { createCanvas, clear } from '../../../core/canvas.js';
import * as rules from '../logic/rules.js'; import * as rules from '../logic/rules.js';
let game, validMoves; let game, validMoves, canvasCtx, canvasEl, boardSize;
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';
export function mountGame(el, params) { export function mountGame(el, params) {
const { mode = 'bot', numPlayers = 4 } = params; const { mode = 'bot', numPlayers = 4 } = params;
...@@ -15,24 +22,30 @@ export function mountGame(el, params) { ...@@ -15,24 +22,30 @@ export function mountGame(el, params) {
validMoves = []; validMoves = [];
el.innerHTML = ` el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);"> <div style="display:flex;flex-direction:column;height:100%;background:#1a2440;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:var(--s-2) var(--s-3);background:var(--bg-base);"> <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;">${t('game.ludo')}</span> <span style="font-size:14px;font-weight:600;color:#f8fafc;">${t('game.ludo')}</span>
<span id="ludo-turn" style="font-size:13px;"></span> <span id="ludo-turn" style="font-size:13px;color:${COLORS[0]};">${t('game.your_turn')}</span>
</div> </div>
<div id="ludo-board" style="flex:1;display:flex;align-items:center;justify-content:center;padding:var(--s-3);"></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:var(--s-4);padding:var(--s-3);background:var(--bg-base);border-top:1px solid var(--border);justify-content:center;"> <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:var(--bg-elevated);border-radius:var(--r-md);display:flex;align-items:center;justify-content:center;font-size:28px;font-weight:800;font-family:var(--font-lat);border:2px solid var(--border);"></div> <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:var(--s-3) var(--s-6);">🎲 ارمِ النرد</button> <button class="btn btn-primary" id="roll-btn" style="font-size:16px;padding:12px 32px;">🎲 ${t('game.ludo') === 'لودو' ? 'ارمِ النرد' : 'Roll Dice'}</button>
</div> </div>
</div> </div>
`; `;
renderBoard(el); 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;
drawBoard();
updateTurnDisplay(el); updateTurnDisplay(el);
el.querySelector('#roll-btn').addEventListener('click', () => handleRoll(el)); el.querySelector('#roll-btn').addEventListener('click', () => handleRoll(el));
bus.emit('game:started', { gameKey: 'ludo', mode }); bus.emit('game:started', { gameKey: 'ludo', mode });
} }
...@@ -47,7 +60,7 @@ function handleRoll(el) { ...@@ -47,7 +60,7 @@ function handleRoll(el) {
const diceEl = el.querySelector('#dice-display'); const diceEl = el.querySelector('#dice-display');
diceEl.textContent = dice; diceEl.textContent = dice;
diceEl.style.borderColor = 'var(--gold)'; diceEl.style.color = '#E4AC38';
validMoves = rules.getValidMoves(game, 0, dice); validMoves = rules.getValidMoves(game, 0, dice);
...@@ -55,9 +68,9 @@ function handleRoll(el) { ...@@ -55,9 +68,9 @@ function handleRoll(el) {
setTimeout(() => { setTimeout(() => {
game.rolled = false; game.rolled = false;
rules.nextTurn(game); rules.nextTurn(game);
diceEl.style.borderColor = 'var(--border)'; diceEl.style.color = '#f8fafc';
updateTurnDisplay(el); updateTurnDisplay(el);
renderBoard(el); drawBoard();
if (game.currentPlayer !== 0) botSequence(el); if (game.currentPlayer !== 0) botSequence(el);
}, 800); }, 800);
return; return;
...@@ -68,7 +81,13 @@ function handleRoll(el) { ...@@ -68,7 +81,13 @@ function handleRoll(el) {
return; return;
} }
renderBoard(el); 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);
} }
function applyPlayerMove(el, move) { function applyPlayerMove(el, move) {
...@@ -76,20 +95,16 @@ function applyPlayerMove(el, move) { ...@@ -76,20 +95,16 @@ function applyPlayerMove(el, move) {
game.rolled = false; game.rolled = false;
audio.play(move.type === 'capture' ? 'capture' : 'move', 'game'); audio.play(move.type === 'capture' ? 'capture' : 'move', 'game');
if (game.gameOver || game.players[0].finished) { if (game.gameOver || game.players[0].finished) { endGame(el); return; }
endGame(el);
return;
}
rules.nextTurn(game); rules.nextTurn(game);
updateTurnDisplay(el); updateTurnDisplay(el);
renderBoard(el); drawBoard();
const diceEl = el.querySelector('#dice-display'); el.querySelector('#dice-display').style.color = '#f8fafc';
diceEl.style.borderColor = 'var(--border)';
if (game.currentPlayer !== 0) { if (game.currentPlayer !== 0) {
setTimeout(() => botSequence(el), 800); setTimeout(() => botSequence(el), 600);
} }
} }
...@@ -98,8 +113,7 @@ function botSequence(el) { ...@@ -98,8 +113,7 @@ function botSequence(el) {
const dice = rules.rollDice(); const dice = rules.rollDice();
game.diceValue = dice; game.diceValue = dice;
const diceEl = el.querySelector('#dice-display'); el.querySelector('#dice-display').textContent = dice;
diceEl.textContent = dice;
audio.play('dice', 'game'); audio.play('dice', 'game');
const move = rules.getBotMove(game, game.currentPlayer, dice); const move = rules.getBotMove(game, game.currentPlayer, dice);
...@@ -112,62 +126,192 @@ function botSequence(el) { ...@@ -112,62 +126,192 @@ function botSequence(el) {
rules.nextTurn(game); rules.nextTurn(game);
updateTurnDisplay(el); updateTurnDisplay(el);
renderBoard(el); drawBoard();
if (game.currentPlayer !== 0) { if (game.currentPlayer !== 0) {
setTimeout(() => botSequence(el), 600); setTimeout(() => botSequence(el), 400);
} }
} }
function renderBoard(el) { function drawBoard() {
const boardEl = el.querySelector('#ludo-board'); const ctx = canvasCtx;
const pieceInfo = []; const s = boardSize;
const cell = s / 15;
clear(ctx, s, s);
// 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
// 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);
// Draw grid lines on paths
ctx.strokeStyle = '#E0E0E0';
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();
}
}
// 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();
}
// 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);
}
// 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();
// Draw pieces
game.players.forEach((player, pIdx) => { game.players.forEach((player, pIdx) => {
player.pieces.forEach((piece, pieceIdx) => { player.pieces.forEach((piece) => {
if (!piece.finished) { if (piece.finished) return;
pieceInfo.push({ color: rules.COLOR_CSS[pIdx], pos: piece.pos, home: piece.pos === -1, id: piece.id, playerIdx: pIdx }); 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);
} }
}); });
}); });
}
const movableIds = new Set(validMoves.map(m => m.pieceId)); function drawHomeZone(ctx, x, y, size, colorIdx) {
ctx.fillStyle = COLORS[colorIdx];
boardEl.innerHTML = ` ctx.fillRect(x, y, size, size);
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:var(--s-3);width:100%;max-width:320px;"> ctx.fillStyle = BG_COLOR;
${game.players.map((player, i) => ` const inset = size * 0.15;
<div style="background:var(--bg-card);border-radius:var(--r-md);padding:var(--s-2);text-align:center;border:2px solid ${i === game.currentPlayer ? rules.COLOR_CSS[i] : 'var(--border)'};"> ctx.fillRect(x + inset, y + inset, size - inset * 2, size - inset * 2);
<div style="font-size:10px;color:var(--text-secondary);margin-bottom:4px;">${i === 0 ? 'أنت' : 'Bot ' + i}</div>
<div style="display:flex;gap:4px;justify-content:center;flex-wrap:wrap;"> // Draw home circles (starting positions)
${player.pieces.map(p => ` const cx = x + size / 2;
<div data-piece="${p.id}" style="width:20px;height:20px;border-radius:50%;background:${p.finished ? 'var(--success)' : p.pos === -1 ? rules.COLOR_CSS[i] + '66' : rules.COLOR_CSS[i]};border:${movableIds.has(p.id) ? '2px solid var(--gold)' : '1px solid rgba(255,255,255,0.2)'};cursor:${movableIds.has(p.id) ? 'pointer' : 'default'};transition:transform 0.15s;${movableIds.has(p.id) ? 'animation:pulse 1s infinite;' : ''}"></div> const cy = y + size / 2;
`).join('')} const spread = size * 0.22;
</div> const positions = [[-1,-1],[1,-1],[-1,1],[1,1]];
</div> positions.forEach(([dx, dy]) => {
`).join('')} ctx.beginPath();
</div> 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();
});
}
if (validMoves.length > 0 && game.currentPlayer === 0) { function drawPieceInHome(ctx, playerIdx, piece, cell, color) {
boardEl.querySelectorAll('[data-piece]').forEach(pieceEl => { const zones = [
const id = pieceEl.dataset.piece; { x: 0, y: 0 }, { x: 9, y: 0 }, { x: 9, y: 9 }, { x: 0, y: 9 }
if (movableIds.has(id)) { ];
pieceEl.addEventListener('click', () => { const zone = zones[playerIdx];
const move = validMoves.find(m => m.pieceId === id); const zoneSize = cell * 6;
if (move) applyPlayerMove(el, move); 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) { function updateTurnDisplay(el) {
const turnEl = el.querySelector('#ludo-turn'); const turnEl = el.querySelector('#ludo-turn');
if (game.currentPlayer === 0) { if (game.currentPlayer === 0) {
turnEl.textContent = t('game.your_turn'); turnEl.textContent = t('game.your_turn');
turnEl.style.color = rules.COLOR_CSS[0]; turnEl.style.color = COLORS[0];
} else { } else {
turnEl.textContent = `دور ${rules.COLORS[game.currentPlayer]}`; turnEl.textContent = `Bot ${game.currentPlayer}`;
turnEl.style.color = rules.COLOR_CSS[game.currentPlayer]; turnEl.style.color = COLORS[game.currentPlayer];
} }
} }
......
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