MMD-Coder's picture
Initial DeepSite commit
0ac6782 verified
Raw
History Blame Contribute Delete
25.3 kB
// ============================================
// 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('🔴 قرمز | 🔵 آبی | 🟢 سبز | 🟡 زرد');