// ============================================ // Mensch (منچ) - Complete Game Logic // ============================================ const canvas = document.getElementById('mensch-board'); const ctx = canvas.getContext('2d'); // --- Game Constants --- const PLAYERS = { RED: { name: 'قرمز', color: '#e74c3c', homeColor: '#ff6b6b', lightColor: '#ffcccc' }, BLUE: { name: 'آبی', color: '#2980b9', homeColor: '#5dade2', lightColor: '#cce5ff' }, GREEN: { name: 'سبز', color: '#27ae60', homeColor: '#58d68d', lightColor: '#ccffcc' }, YELLOW: { name: 'زرد', color: '#f39c12', homeColor: '#f7dc6f', lightColor: '#fff9cc' } }; const PLAYER_ORDER = ['RED', 'BLUE', 'GREEN', 'YELLOW']; const TOTAL_PATH = 40; // Total cells in the main path const HOME_STRETCH = 4; // Final home stretch cells // Each player's path: start position on main track, and home entry position const PLAYER_CONFIG = { RED: { startPath: 0, homeEntry: 39, startCell: { x: 6, y: 6 }, direction: 1 }, BLUE: { startPath: 10, homeEntry: 9, startCell: { x: 6, y: 0 }, direction: 1 }, GREEN: { startPath: 20, homeEntry: 19, startCell: { x: 0, y: 0 }, direction: 1 }, YELLOW: { startPath: 30, homeEntry: 29, startCell: { x: 0, y: 6 }, direction: 1 } }; // --- Game State --- const gameState = { players: { RED: { pieces: [-1, -1, -1, -1], finished: 0 }, // -1 = in start house BLUE: { pieces: [-1, -1, -1, -1], finished: 0 }, GREEN: { pieces: [-1, -1, -1, -1], finished: 0 }, YELLOW: { pieces: [-1, -1, -1, -1], finished: 0 } }, currentPlayerIndex: 0, diceValue: null, gamePhase: 'rolling', // 'rolling' | 'moving' | 'gameOver' selectedPiece: null, sixCount: 0, // Consecutive 6s winner: null, validMoves: [] }; // --- Main Path Coordinates --- // Define the 40-cell main path as grid positions on an 11x11 board const mainPath = []; // Build the main path (simplified: outer track) function buildMainPath() { // Outer loop: cells 0-39 going around a 11x11 grid // Top row: right to left (cols 10 to 0) = 11 cells for (let col = 10; col >= 0; col--) mainPath.push({ x: col, y: 0 }); // Left column: top to bottom (rows 1 to 10) = 10 cells for (let row = 1; row <= 10; row++) mainPath.push({ x: 0, y: row }); // Bottom row: left to right (cols 1 to 10) = 10 cells for (let col = 1; col <= 10; col++) mainPath.push({ x: col, y: 10 }); // Right column: bottom to top (rows 9 to 1) = 9 cells for (let row = 9; row >= 1; row--) mainPath.push({ x: 10, y: row }); // Total: 11 + 10 + 10 + 9 = 40 cells ✓ } // --- Home Stretch Coordinates --- const homeStretches = {}; function buildHomeStretches() { // RED home: from path end into center, going upward homeStretches.RED = [ { x: 9, y: 5 }, { x: 8, y: 5 }, { x: 7, y: 5 }, { x: 6, y: 5 } ]; // BLUE home: from path into center, going right homeStretches.BLUE = [ { x: 5, y: 1 }, { x: 5, y: 2 }, { x: 5, y: 3 }, { x: 5, y: 4 } ]; // GREEN home: from path into center, going downward homeStretches.GREEN = [ { x: 1, y: 5 }, { x: 2, y: 5 }, { x: 3, y: 5 }, { x: 4, y: 5 } ]; // YELLOW home: from path into center, going left homeStretches.YELLOW = [ { x: 5, y: 9 }, { x: 5, y: 8 }, { x: 5, y: 7 }, { x: 5, y: 6 } ]; } // --- Safe Cells (Stars) --- const safeCells = [0, 8, 13, 21, 26, 34, 39]; // Approximate safe positions // --- Initialize --- function initGame() { buildMainPath(); buildHomeStretches(); gameState.players = { RED: { pieces: [-1, -1, -1, -1], finished: 0 }, BLUE: { pieces: [-1, -1, -1, -1], finished: 0 }, GREEN: { pieces: [-1, -1, -1, -1], finished: 0 }, YELLOW: { pieces: [-1, -1, -1, -1], finished: 0 } }; gameState.currentPlayerIndex = 0; gameState.diceValue = null; gameState.gamePhase = 'rolling'; gameState.selectedPiece = null; gameState.sixCount = 0; gameState.winner = null; gameState.validMoves = []; updateUI(); updateStatusMessage(`نوبت بازیکن ${PLAYERS[getCurrentPlayer()].name} - تاس بریزید`); document.getElementById('roll-btn').disabled = false; document.getElementById('extra-turn-indicator').classList.add('hidden'); document.getElementById('moves-remaining').classList.add('hidden'); hideWinModal(); drawBoard(); } function getCurrentPlayer() { return PLAYER_ORDER[gameState.currentPlayerIndex]; } // --- Dice --- function rollDice() { if (gameState.gamePhase !== 'rolling') return; if (gameState.winner) return; const value = Math.floor(Math.random() * 6) + 1; gameState.diceValue = value; // Animate dice animateDice(value); const player = getCurrentPlayer(); // Track consecutive 6s if (value === 6) { gameState.sixCount++; if (gameState.sixCount === 3) { // Penalty: send last moved piece back to start penaltyThreeSixes(player); gameState.sixCount = 0; switchTurn(); return; } } else { gameState.sixCount = 0; } // Find valid moves gameState.validMoves = findValidMoves(player, value); gameState.selectedPiece = null; if (gameState.validMoves.length === 0) { updateStatusMessage(`بازیکن ${PLAYERS[player].name}: هیچ حرکت معتبری نیست - نوبت عوض شد`); if (value !== 6) { setTimeout(() => switchTurn(), 1200); } else { // On 6 with no moves, still get extra turn (but no moves to make) updateStatusMessage(`تاس ۶ ولی حرکتی نیست! دوباره تاس بریزید`); gameState.gamePhase = 'rolling'; } document.getElementById('roll-btn').disabled = true; drawBoard(); return; } gameState.gamePhase = 'moving'; document.getElementById('roll-btn').disabled = true; if (value === 6) { document.getElementById('extra-turn-indicator').classList.remove('hidden'); } updateStatusMessage(`بازیکن ${PLAYERS[player].name}: مهره را انتخاب کنید (تاس: ${value})`); drawBoard(); } function findValidMoves(player, dice) { const moves = []; const pieces = gameState.players[player].pieces; pieces.forEach((pos, idx) => { // Piece in start house - can only move out on 6 if (pos === -1 && dice === 6) { moves.push({ piece: idx, from: 'start', to: getStartPath(player) }); } // Piece on main path else if (pos >= 0 && pos < TOTAL_PATH) { let newPos = (pos + dice) % TOTAL_PATH; // Check if this wraps past the home entry const homeEntry = PLAYER_CONFIG[player].homeEntry; // If piece is before home entry and new position goes past it if (pos <= homeEntry && (pos + dice) > homeEntry + 1) { // Try to enter home stretch const homeSteps = (pos + dice) - (homeEntry + 1); if (homeSteps >= 0 && homeSteps < HOME_STRETCH) { moves.push({ piece: idx, from: pos, to: 'home-' + homeSteps, isHome: true }); } } // Normal move on main path if (canMoveTo(player, newPos)) { moves.push({ piece: idx, from: pos, to: newPos }); } } // Piece in home stretch else if (pos >= 100) { const homeStep = pos - 100; const newHomeStep = homeStep + dice; if (newHomeStep < HOME_STRETCH) { moves.push({ piece: idx, from: pos, to: 100 + newHomeStep, isHome: true }); } else if (newHomeStep === HOME_STRETCH) { moves.push({ piece: idx, from: pos, to: 'finish', isWin: true }); } } }); // Sort: prefer moving pieces already on board, prefer entering home moves.sort((a, b) => { if (a.isWin) return -1; if (b.isWin) return 1; if (a.isHome && !b.isHome) return -1; if (!a.isHome && b.isHome) return 1; if (a.from === 'start') return 1; if (b.from === 'start') return -1; return 0; }); return moves; } function canMoveTo(player, pathIndex) { // Check if cell is occupied by own piece for (const p of PLAYER_ORDER) { const pieces = gameState.players[p].pieces; for (const pos of pieces) { if (pos === pathIndex) { if (p === player) return false; // Can't land on own piece // Can hit opponent's piece if not on safe cell if (safeCells.includes(pathIndex)) return false; return true; // Can hit } } } return true; // Empty cell } function getStartPath(player) { return PLAYER_CONFIG[player].startPath; } function penaltyThreeSixes(player) { // Find the piece closest to home and send it back to start const pieces = gameState.players[player].pieces; let maxPos = -1; let maxIdx = -1; pieces.forEach((pos, idx) => { if (pos > maxPos && pos < 100) { // On main track, not in home stretch maxPos = pos; maxIdx = idx; } }); if (maxIdx >= 0) { pieces[maxIdx] = -1; updateStatusMessage(`⚠️ بازیکن ${PLAYERS[player].name}: ۳ تا تاس ۶! مهره به خانه شروع برگشت!`); } } // --- Move Execution --- function executeMove(move) { const player = getCurrentPlayer(); const pieces = gameState.players[player].pieces; if (move.to === 'finish') { pieces[move.piece] = 'finished'; gameState.players[player].finished++; } else if (typeof move.to === 'string' && move.to.startsWith('home-')) { const step = parseInt(move.to.split('-')[1]); pieces[move.piece] = 100 + step; } else { // Check if hitting opponent for (const p of PLAYER_ORDER) { if (p !== player) { const opPieces = gameState.players[p].pieces; opPieces.forEach((pos, idx) => { if (pos === move.to) { opPieces[idx] = -1; // Send back to start } }); } } pieces[move.piece] = move.to; } // Check win if (gameState.players[player].finished === 4) { endGame(player); return; } // Handle extra turn on 6 if (gameState.diceValue === 6) { gameState.gamePhase = 'rolling'; gameState.validMoves = []; gameState.selectedPiece = null; gameState.diceValue = null; document.getElementById('roll-btn').disabled = false; document.getElementById('extra-turn-indicator').classList.add('hidden'); updateStatusMessage(`🎯 تاس ۶! بازیکن ${PLAYERS[player].name} دوباره تاس بریز`); } else { switchTurn(); } drawBoard(); } function switchTurn() { gameState.currentPlayerIndex = (gameState.currentPlayerIndex + 1) % PLAYER_ORDER.length; gameState.diceValue = null; gameState.gamePhase = 'rolling'; gameState.selectedPiece = null; gameState.validMoves = []; gameState.sixCount = 0; document.getElementById('roll-btn').disabled = false; document.getElementById('extra-turn-indicator').classList.add('hidden'); document.getElementById('moves-remaining').classList.add('hidden'); updateUI(); updateStatusMessage(`نوبت بازیکن ${PLAYERS[getCurrentPlayer()].name} - تاس بریزید`); drawBoard(); } function endGame(player) { gameState.winner = player; gameState.gamePhase = 'gameOver'; document.getElementById('roll-btn').disabled = true; updateStatusMessage(`🎉 بازیکن ${PLAYERS[player].name} برنده شد!`); showWinModal(player); drawBoard(); } // --- Animation --- function animateDice(value) { const diceEl = document.getElementById('dice-display'); diceEl.classList.add('dice-rolling'); const interval = setInterval(() => { const r = Math.floor(Math.random() * 6) + 1; renderDiceFace(r); }, 80); setTimeout(() => { clearInterval(interval); diceEl.classList.remove('dice-rolling'); renderDiceFace(value); }, 500); } function renderDiceFace(value) { const container = document.getElementById('dice-dots'); container.innerHTML = ''; const dotPositions = { 1: ['center'], 2: ['top-right', 'bottom-left'], 3: ['top-right', 'center', 'bottom-left'], 4: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], 5: ['top-left', 'top-right', 'center', 'bottom-left', 'bottom-right'], 6: ['top-left', 'top-right', 'mid-left', 'mid-right', 'bottom-left', 'bottom-right'] }; const positions = dotPositions[value] || []; positions.forEach(pos => { const dot = document.createElement('div'); dot.className = `dot ${pos}`; container.appendChild(dot); }); } // --- Canvas Drawing --- function drawBoard() { const w = canvas.width; const h = canvas.height; const cellSize = w / 11; // Clear ctx.fillStyle = '#f5e6ca'; ctx.fillRect(0, 0, w, h); // Draw grid ctx.strokeStyle = '#d4c5a0'; ctx.lineWidth = 1; for (let i = 0; i <= 11; i++) { ctx.beginPath(); ctx.moveTo(i * cellSize, 0); ctx.lineTo(i * cellSize, h); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, i * cellSize); ctx.lineTo(w, i * cellSize); ctx.stroke(); } // Draw main path cells (outer ring) mainPath.forEach((pos, idx) => { const x = pos.x * cellSize; const y = pos.y * cellSize; // Cell background if (safeCells.includes(idx)) { ctx.fillStyle = '#fff3cd'; } else { ctx.fillStyle = '#e8dcc8'; } ctx.fillRect(x + 2, y + 2, cellSize - 4, cellSize - 4); // Cell border ctx.strokeStyle = '#c4b896'; ctx.lineWidth = 1; ctx.strokeRect(x + 2, y + 2, cellSize - 4, cellSize - 4); // Safe cell star if (safeCells.includes(idx)) { ctx.fillStyle = '#ffc107'; ctx.font = `${cellSize * 0.5}px Vazirmatn`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('⭐', x + cellSize / 2, y + cellSize / 2); } }); // Draw home stretches for (const [player, cells] of Object.entries(homeStretches)) { cells.forEach((pos, idx) => { const x = pos.x * cellSize; const y = pos.y * cellSize; ctx.fillStyle = PLAYERS[player].lightColor; ctx.fillRect(x + 2, y + 2, cellSize - 4, cellSize - 4); ctx.strokeStyle = PLAYERS[player].color; ctx.lineWidth = 2; ctx.strokeRect(x + 2, y + 2, cellSize - 4, cellSize - 4); }); } // Draw start houses const startHouses = { RED: { x: 7, y: 7, w: 4, h: 4 }, BLUE: { x: 7, y: 0, w: 4, h: 4 }, GREEN: { x: 0, y: 0, w: 4, h: 4 }, YELLOW: { x: 0, y: 7, w: 4, h: 4 } }; for (const [player, rect] of Object.entries(startHouses)) { const x = rect.x * cellSize; const y = rect.y * cellSize; const rw = rect.w * cellSize; const rh = rect.h * cellSize; ctx.fillStyle = PLAYERS[player].lightColor; ctx.fillRect(x + 4, y + 4, rw - 8, rh - 8); ctx.strokeStyle = PLAYERS[player].color; ctx.lineWidth = 3; ctx.strokeRect(x + 4, y + 4, rw - 8, rh - 8); } // Draw center const centerX = 4 * cellSize; const centerY = 4 * cellSize; ctx.fillStyle = '#e8dcc8'; ctx.fillRect(centerX + 4, centerY + 4, 3 * cellSize - 8, 3 * cellSize - 8); ctx.strokeStyle = '#c4b896'; ctx.lineWidth = 2; ctx.strokeRect(centerX + 4, centerY + 4, 3 * cellSize - 8, 3 * cellSize - 8); // Write "HOME" in center ctx.fillStyle = '#8b7355'; ctx.font = `bold ${cellSize * 0.5}px Vazirmatn`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('🏠', centerX + 1.5 * cellSize, centerY + 1.5 * cellSize); // Draw pieces for (const [player, data] of Object.entries(gameState.players)) { const config = PLAYER_CONFIG[player]; data.pieces.forEach((pos, idx) => { if (pos === 'finished') return; // Don't draw finished pieces let px, py; if (pos === -1) { // In start house const startHouse = startHouses[player]; const offsetX = (idx % 2) * cellSize * 0.5; const offsetY = Math.floor(idx / 2) * cellSize * 0.5; px = startHouse.x * cellSize + cellSize * 0.7 + offsetX; py = startHouse.y * cellSize + cellSize * 0.7 + offsetY; } else if (pos >= 100) { // In home stretch const step = pos - 100; const homeCell = homeStretches[player][step]; px = homeCell.x * cellSize + cellSize / 2; py = homeCell.y * cellSize + cellSize / 2; } else { // On main path const pathCell = mainPath[pos]; px = pathCell.x * cellSize + cellSize / 2; py = pathCell.y * cellSize + cellSize / 2; // Offset if multiple pieces on same cell let sameCellCount = 0; for (const p of PLAYER_ORDER) { for (const pp of gameState.players[p].pieces) { if (pp === pos) sameCellCount++; } } if (sameCellCount > 1) { const offset = (idx - sameCellCount / 2) * (cellSize * 0.18); px += offset; } } // Draw piece ctx.beginPath(); ctx.arc(px, py, cellSize * 0.3, 0, Math.PI * 2); ctx.fillStyle = PLAYERS[player].color; ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke(); ctx.fillStyle = '#fff'; ctx.font = `bold ${cellSize * 0.25}px Vazirmatn`; ctx.fillText(idx + 1, px, py); }); } // Highlight valid moves if (gameState.gamePhase === 'moving' && gameState.selectedPiece !== null) { // Highlight selected piece ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 3; // ... would need redraw logic } else if (gameState.gamePhase === 'moving') { // Highlight valid pieces const highlightedPieces = new Set(); gameState.validMoves.forEach(m => highlightedPieces.add(m.piece)); const player = getCurrentPlayer(); const pieces = gameState.players[player].pieces; highlightedPieces.forEach(pieceIdx => { const pos = pieces[pieceIdx]; if (pos === -1) { const startHouse = startHouses[player]; const px = startHouse.x * cellSize + cellSize * 0.7 + (pieceIdx % 2) * cellSize * 0.5; const py = startHouse.y * cellSize + cellSize * 0.7 + Math.floor(pieceIdx / 2) * cellSize * 0.5; ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 4; ctx.beginPath(); ctx.arc(px, py, cellSize * 0.35, 0, Math.PI * 2); ctx.stroke(); } else if (pos >= 0 && pos < TOTAL_PATH) { const pathCell = mainPath[pos]; const px = pathCell.x * cellSize + cellSize / 2; const py = pathCell.y * cellSize + cellSize / 2; ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 4; ctx.beginPath(); ctx.arc(px, py, cellSize * 0.35, 0, Math.PI * 2); ctx.stroke(); } else if (pos >= 100) { const step = pos - 100; const homeCell = homeStretches[player][step]; const px = homeCell.x * cellSize + cellSize / 2; const py = homeCell.y * cellSize + cellSize / 2; ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 4; ctx.beginPath(); ctx.arc(px, py, cellSize * 0.35, 0, Math.PI * 2); ctx.stroke(); } }); } } // --- Canvas Click Handling --- canvas.addEventListener('click', (e) => { if (gameState.gamePhase !== 'moving') return; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const mouseX = (e.clientX - rect.left) * scaleX; const mouseY = (e.clientY - rect.top) * scaleY; const cellSize = canvas.width / 11; const col = Math.floor(mouseX / cellSize); const row = Math.floor(mouseY / cellSize); const player = getCurrentPlayer(); const pieces = gameState.players[player].pieces; // Find which piece was clicked let clickedPiece = -1; let minDist = cellSize * 0.4; gameState.validMoves.forEach(move => { const pieceIdx = move.piece; const pos = pieces[pieceIdx]; let px, py; if (pos === -1) { const startHouse = { RED: { x: 7, y: 7 }, BLUE: { x: 7, y: 0 }, GREEN: { x: 0, y: 0 }, YELLOW: { x: 0, y: 7 } }[player]; px = startHouse.x * cellSize + cellSize * 0.7 + (pieceIdx % 2) * cellSize * 0.5; py = startHouse.y * cellSize + cellSize * 0.7 + Math.floor(pieceIdx / 2) * cellSize * 0.5; } else if (pos >= 100) { const step = pos - 100; const homeCell = homeStretches[player][step]; px = homeCell.x * cellSize + cellSize / 2; py = homeCell.y * cellSize + cellSize / 2; } else { const pathCell = mainPath[pos]; px = pathCell.x * cellSize + cellSize / 2; py = pathCell.y * cellSize + cellSize / 2; } const dist = Math.sqrt((mouseX - px) ** 2 + (mouseY - py) ** 2); if (dist < minDist) { minDist = dist; clickedPiece = pieceIdx; } }); if (clickedPiece >= 0) { const move = gameState.validMoves.find(m => m.piece === clickedPiece); if (move) { executeMove(move); } } }); // --- UI Updates --- function updateUI() { const player = getCurrentPlayer(); const emojis = { RED: '🔴', BLUE: '🔵', GREEN: '🟢', YELLOW: '🟡' }; document.getElementById('current-player-display').textContent = emojis[player]; } function updateStatusMessage(message) { document.getElementById('game-status').textContent = message; } // --- Win Modal --- function showWinModal(player) { const modal = document.getElementById('win-modal'); const content = document.getElementById('win-modal-content'); const message = document.getElementById('win-message'); const detail = document.getElementById('win-detail'); message.textContent = `بازیکن ${PLAYERS[player].name} برنده شد! 🏆`; detail.textContent = 'بازی فوق‌العاده‌ای بود! آماده‌ای برای یه دور دیگه؟'; modal.classList.remove('hidden'); setTimeout(() => content.classList.add('show'), 100); } function hideWinModal() { const modal = document.getElementById('win-modal'); const content = document.getElementById('win-modal-content'); content.classList.remove('show'); setTimeout(() => modal.classList.add('hidden'), 300); } // --- Event Listeners --- document.getElementById('roll-btn').addEventListener('click', rollDice); document.getElementById('new-game-btn').addEventListener('click', initGame); document.getElementById('win-new-game').addEventListener('click', () => { hideWinModal(); initGame(); }); document.getElementById('win-modal').addEventListener('click', function(e) { if (e.target === this) { hideWinModal(); initGame(); } }); // --- Keyboard Shortcuts --- document.addEventListener('keydown', (e) => { if (e.key === ' ' || e.key === 'Spacebar') { e.preventDefault(); rollDice(); } else if (e.key === 'n' || e.key === 'N') { initGame(); } }); // --- Initialize Game --- initGame(); renderDiceFace(1); document.getElementById('dice-dots').innerHTML = ''; console.log('🎲 منچ آماده است!'); console.log('⌨️ کلید میانبر: Space = تاس بریز | N = بازی جدید'); console.log('🔴 قرمز | 🔵 آبی | 🟢 سبز | 🟡 زرد');