Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Nine Men's Morris</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 20px; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| flex-direction: column; | |
| } | |
| .game-container { | |
| background: rgba(255, 255, 255, 0.95); | |
| border-radius: 20px; | |
| padding: 30px; | |
| box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); | |
| backdrop-filter: blur(10px); | |
| } | |
| h1 { | |
| text-align: center; | |
| color: #333; | |
| margin-bottom: 20px; | |
| font-size: 2.5rem; | |
| text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1); | |
| } | |
| .game-info { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| gap: 20px; | |
| } | |
| .player-info { | |
| background: rgba(0, 0, 0, 0.05); | |
| padding: 15px; | |
| border-radius: 10px; | |
| min-width: 150px; | |
| text-align: center; | |
| } | |
| .player-info.active { | |
| background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); | |
| box-shadow: 0 4px 15px rgba(252, 182, 159, 0.4); | |
| } | |
| .board-container { | |
| display: flex; | |
| justify-content: center; | |
| margin-bottom: 20px; | |
| } | |
| .board { | |
| width: 400px; | |
| height: 400px; | |
| position: relative; | |
| background: #f8f9fa; | |
| border-radius: 10px; | |
| border: 3px solid #333; | |
| } | |
| .board-line { | |
| position: absolute; | |
| background-color: #333; | |
| } | |
| .horizontal { | |
| height: 3px; | |
| } | |
| .vertical { | |
| width: 3px; | |
| } | |
| .position { | |
| position: absolute; | |
| width: 30px; | |
| height: 30px; | |
| border-radius: 50%; | |
| border: 3px solid #333; | |
| background: #fff; | |
| cursor: pointer; | |
| transform: translate(-50%, -50%); | |
| transition: all 0.2s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .position:hover { | |
| box-shadow: 0 0 20px rgba(102, 126, 234, 0.6); | |
| transform: translate(-50%, -50%) scale(1.1); | |
| } | |
| .position.occupied { | |
| cursor: default; | |
| } | |
| .position.red { | |
| background: #dc3545; | |
| border-color: #a02834; | |
| box-shadow: 0 4px 10px rgba(220, 53, 69, 0.5); | |
| } | |
| .position.black { | |
| background: #333; | |
| border-color: #666; | |
| box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5); | |
| } | |
| .position.selected { | |
| box-shadow: 0 0 25px #ffd700; | |
| border-color: #ffd700; | |
| border-width: 4px; | |
| } | |
| .position.valid-move { | |
| background: rgba(144, 238, 144, 0.8); | |
| animation: pulse 1s infinite; | |
| } | |
| .position.removable { | |
| background: rgba(255, 182, 193, 0.8); | |
| animation: pulse 1s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { transform: translate(-50%, -50%) scale(1); } | |
| 50% { transform: translate(-50%, -50%) scale(1.1); } | |
| } | |
| .game-status { | |
| text-align: center; | |
| padding: 15px; | |
| background: rgba(0, 0, 0, 0.05); | |
| border-radius: 10px; | |
| margin-top: 20px; | |
| font-size: 1.1rem; | |
| } | |
| .status-placing { color: #2196F3; } | |
| .status-moving { color: #4CAF50; } | |
| .status-removing { color: #FF5722; } | |
| .status-won { color: #9C27B0; font-weight: bold; } | |
| .controls { | |
| display: flex; | |
| justify-content: center; | |
| gap: 15px; | |
| margin-top: 20px; | |
| } | |
| button { | |
| padding: 12px 24px; | |
| border: none; | |
| border-radius: 25px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| font-weight: 500; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); | |
| } | |
| button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); | |
| } | |
| .rules { | |
| margin-top: 30px; | |
| padding: 20px; | |
| background: rgba(0, 0, 0, 0.05); | |
| border-radius: 10px; | |
| max-width: 600px; | |
| } | |
| .rules h3 { | |
| margin-top: 0; | |
| color: #333; | |
| } | |
| .rules ul { | |
| text-align: left; | |
| color: #555; | |
| line-height: 1.6; | |
| } | |
| .phase-indicator { | |
| font-weight: bold; | |
| margin-bottom: 10px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="game-container"> | |
| <h1>Nine Men's Morris</h1> | |
| <div class="game-info"> | |
| <div class="player-info" id="player1-info"> | |
| <div><strong>Player 1 (Red)</strong></div> | |
| <div>Pieces to place: <span id="red-pieces">9</span></div> | |
| <div>Pieces on board: <span id="red-on-board">0</span></div> | |
| </div> | |
| <div class="player-info" id="player2-info"> | |
| <div><strong>Player 2 (Black)</strong></div> | |
| <div>Pieces to place: <span id="black-pieces">9</span></div> | |
| <div>Pieces on board: <span id="black-on-board">0</span></div> | |
| </div> | |
| </div> | |
| <div class="board-container"> | |
| <div class="board" id="board"> | |
| <!-- Board lines --> | |
| <!-- Outer square --> | |
| <div class="board-line horizontal" style="top: 50px; left: 50px; width: 300px;"></div> | |
| <div class="board-line horizontal" style="top: 350px; left: 50px; width: 300px;"></div> | |
| <div class="board-line vertical" style="top: 50px; left: 50px; height: 300px;"></div> | |
| <div class="board-line vertical" style="top: 50px; left: 350px; height: 300px;"></div> | |
| <!-- Middle square --> | |
| <div class="board-line horizontal" style="top: 100px; left: 100px; width: 200px;"></div> | |
| <div class="board-line horizontal" style="top: 300px; left: 100px; width: 200px;"></div> | |
| <div class="board-line vertical" style="top: 100px; left: 100px; height: 200px;"></div> | |
| <div class="board-line vertical" style="top: 100px; left: 300px; height: 200px;"></div> | |
| <!-- Inner square --> | |
| <div class="board-line horizontal" style="top: 150px; left: 150px; width: 100px;"></div> | |
| <div class="board-line horizontal" style="top: 250px; left: 150px; width: 100px;"></div> | |
| <div class="board-line vertical" style="top: 150px; left: 150px; height: 100px;"></div> | |
| <div class="board-line vertical" style="top: 150px; left: 250px; height: 100px;"></div> | |
| <!-- Connection lines --> | |
| <div class="board-line horizontal" style="top: 200px; left: 50px; width: 100px;"></div> | |
| <div class="board-line horizontal" style="top: 200px; left: 250px; width: 100px;"></div> | |
| <div class="board-line vertical" style="top: 50px; left: 200px; height: 100px;"></div> | |
| <div class="board-line vertical" style="top: 250px; left: 200px; height: 100px;"></div> | |
| </div> | |
| </div> | |
| <div class="game-status" id="game-status"> | |
| <div class="phase-indicator">Phase 1: Placing Pieces</div> | |
| <div class="status-placing">Player 1's turn - Place a piece</div> | |
| </div> | |
| <div class="controls"> | |
| <button onclick="resetGame()">New Game</button> | |
| <button onclick="toggleRules()">Rules</button> | |
| </div> | |
| <div class="rules" id="rules" style="display: none;"> | |
| <h3>Nine Men's Morris Rules:</h3> | |
| <ul> | |
| <li><strong>Phase 1 - Placing:</strong> Take turns placing your 9 pieces on any empty intersection. Form mills (3 pieces in a row) to capture opponent pieces.</li> | |
| <li><strong>Phase 2 - Moving:</strong> Move pieces to adjacent empty positions along the lines. Continue forming mills to capture pieces.</li> | |
| <li><strong>Phase 3 - Flying:</strong> When you have only 3 pieces left, you can move to any empty position (not just adjacent).</li> | |
| <li><strong>Capturing:</strong> When you form a mill, remove one opponent piece. Pieces in mills can only be captured if no other pieces are available.</li> | |
| <li><strong>Mill Breaking:</strong> You can break your own mill and reform it to capture again.</li> | |
| <li><strong>Winning:</strong> Reduce your opponent to 2 pieces or block all their moves.</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <script> | |
| class NineMensMorris { | |
| constructor() { | |
| this.board = new Array(24).fill(null); | |
| this.currentPlayer = 'red'; | |
| this.gamePhase = 'placing'; // 'placing', 'moving', 'game-over' | |
| this.redPieces = 9; | |
| this.blackPieces = 9; | |
| this.redPiecesOnBoard = 0; | |
| this.blackPiecesOnBoard = 0; | |
| this.selectedPosition = null; | |
| this.mustRemovePiece = false; | |
| this.millFormed = false; | |
| // Define board positions (x, y coordinates) | |
| this.positions = [ | |
| [50, 50], [200, 50], [350, 50], // 0, 1, 2 | |
| [100, 100], [200, 100], [300, 100], // 3, 4, 5 | |
| [150, 150], [200, 150], [250, 150], // 6, 7, 8 | |
| [50, 200], [100, 200], [150, 200], // 9, 10, 11 | |
| [250, 200], [300, 200], [350, 200], // 12, 13, 14 | |
| [150, 250], [200, 250], [250, 250], // 15, 16, 17 | |
| [100, 300], [200, 300], [300, 300], // 18, 19, 20 | |
| [50, 350], [200, 350], [350, 350] // 21, 22, 23 | |
| ]; | |
| // Define adjacency list | |
| this.adjacentPositions = { | |
| 0: [1, 9], 1: [0, 2, 4], 2: [1, 14], | |
| 3: [4, 10], 4: [1, 3, 5, 7], 5: [4, 13], | |
| 6: [7, 11], 7: [4, 6, 8], 8: [7, 12], | |
| 9: [0, 10, 21], 10: [3, 9, 11, 18], 11: [6, 10, 15], | |
| 12: [8, 13, 17], 13: [5, 12, 14, 20], 14: [2, 13, 23], | |
| 15: [11, 16, 18], 16: [15, 17, 19], 17: [12, 16, 20], | |
| 18: [10, 15, 19, 21], 19: [16, 18, 20, 22], 20: [13, 17, 19, 23], | |
| 21: [9, 18, 22], 22: [19, 21, 23], 23: [14, 20, 22] | |
| }; | |
| // Define mill combinations | |
| this.mills = [ | |
| [0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11], | |
| [12, 13, 14], [15, 16, 17], [18, 19, 20], [21, 22, 23], | |
| [0, 9, 21], [3, 10, 18], [6, 11, 15], [1, 4, 7], | |
| [16, 19, 22], [8, 12, 17], [5, 13, 20], [2, 14, 23] | |
| ]; | |
| this.initializeBoard(); | |
| this.updateDisplay(); | |
| } | |
| initializeBoard() { | |
| const board = document.getElementById('board'); | |
| // Remove existing position elements | |
| const existingPositions = board.querySelectorAll('.position'); | |
| existingPositions.forEach(pos => pos.remove()); | |
| // Create position elements | |
| for (let i = 0; i < 24; i++) { | |
| const position = document.createElement('div'); | |
| position.className = 'position'; | |
| position.style.left = this.positions[i][0] + 'px'; | |
| position.style.top = this.positions[i][1] + 'px'; | |
| position.dataset.index = i; | |
| position.addEventListener('click', () => this.handlePositionClick(i)); | |
| board.appendChild(position); | |
| } | |
| } | |
| handlePositionClick(index) { | |
| if (this.gamePhase === 'game-over') return; | |
| if (this.mustRemovePiece) { | |
| this.removeOpponentPiece(index); | |
| return; | |
| } | |
| if (this.gamePhase === 'placing') { | |
| this.placePiece(index); | |
| } else if (this.gamePhase === 'moving') { | |
| if (this.selectedPosition === null) { | |
| this.selectPiece(index); | |
| } else if (this.selectedPosition === index) { | |
| this.deselectPiece(); | |
| } else { | |
| this.movePiece(index); | |
| } | |
| } | |
| } | |
| placePiece(index) { | |
| if (this.board[index] !== null) return; | |
| this.board[index] = this.currentPlayer; | |
| if (this.currentPlayer === 'red') { | |
| this.redPieces--; | |
| this.redPiecesOnBoard++; | |
| } else { | |
| this.blackPieces--; | |
| this.blackPiecesOnBoard++; | |
| } | |
| if (this.checkForMill(index)) { | |
| this.mustRemovePiece = true; | |
| this.millFormed = true; | |
| } else { | |
| // Check if placing phase is complete | |
| if (this.redPieces === 0 && this.blackPieces === 0) { | |
| this.gamePhase = 'moving'; | |
| } | |
| this.switchPlayer(); | |
| } | |
| this.updateDisplay(); | |
| } | |
| selectPiece(index) { | |
| if (this.board[index] !== this.currentPlayer) return; | |
| // Check if piece can move | |
| const piecesOnBoard = this.currentPlayer === 'red' ? this.redPiecesOnBoard : this.blackPiecesOnBoard; | |
| const canFly = piecesOnBoard === 3; | |
| if (!canFly) { | |
| const canMove = this.adjacentPositions[index].some(adj => this.board[adj] === null); | |
| if (!canMove) return; | |
| } | |
| this.selectedPosition = index; | |
| this.updateDisplay(); | |
| } | |
| deselectPiece() { | |
| this.selectedPosition = null; | |
| this.updateDisplay(); | |
| } | |
| movePiece(index) { | |
| if (this.board[index] !== null) return; | |
| if (this.selectedPosition === null) return; | |
| const piecesOnBoard = this.currentPlayer === 'red' ? this.redPiecesOnBoard : this.blackPiecesOnBoard; | |
| const canFly = piecesOnBoard === 3; | |
| // Check if move is valid | |
| if (!canFly && !this.adjacentPositions[this.selectedPosition].includes(index)) { | |
| return; | |
| } | |
| // Move the piece | |
| this.board[index] = this.currentPlayer; | |
| this.board[this.selectedPosition] = null; | |
| const movedFromPosition = this.selectedPosition; | |
| this.selectedPosition = null; | |
| if (this.checkForMill(index)) { | |
| this.mustRemovePiece = true; | |
| this.millFormed = true; | |
| } else { | |
| this.switchPlayer(); | |
| } | |
| this.updateDisplay(); | |
| this.checkGameEnd(); | |
| } | |
| removeOpponentPiece(index) { | |
| const opponent = this.currentPlayer === 'red' ? 'black' : 'red'; | |
| if (this.board[index] !== opponent) return; | |
| // Check if piece is part of a mill - can only remove if no non-mill pieces available | |
| if (this.isPartOfMill(index)) { | |
| if (this.hasNonMillPieces(opponent)) { | |
| return; // Cannot remove piece from mill when other pieces available | |
| } | |
| } | |
| this.board[index] = null; | |
| if (opponent === 'red') { | |
| this.redPiecesOnBoard--; | |
| } else { | |
| this.blackPiecesOnBoard--; | |
| } | |
| this.mustRemovePiece = false; | |
| this.millFormed = false; | |
| // Check if placing phase is complete after removal | |
| if (this.gamePhase === 'placing' && this.redPieces === 0 && this.blackPieces === 0) { | |
| this.gamePhase = 'moving'; | |
| } | |
| this.switchPlayer(); | |
| this.updateDisplay(); | |
| this.checkGameEnd(); | |
| } | |
| checkForMill(index) { | |
| return this.mills.some(mill => { | |
| return mill.includes(index) && | |
| mill.every(pos => this.board[pos] === this.currentPlayer); | |
| }); | |
| } | |
| isPartOfMill(index) { | |
| if (this.board[index] === null) return false; | |
| return this.mills.some(mill => { | |
| return mill.includes(index) && | |
| mill.every(pos => this.board[pos] === this.board[index]); | |
| }); | |
| } | |
| hasNonMillPieces(player) { | |
| for (let i = 0; i < 24; i++) { | |
| if (this.board[i] === player && !this.isPartOfMill(i)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| getRemovablePieces() { | |
| const opponent = this.currentPlayer === 'red' ? 'black' : 'red'; | |
| const removable = []; | |
| // First, try to find non-mill pieces | |
| for (let i = 0; i < 24; i++) { | |
| if (this.board[i] === opponent && !this.isPartOfMill(i)) { | |
| removable.push(i); | |
| } | |
| } | |
| // If no non-mill pieces, all opponent pieces are removable | |
| if (removable.length === 0) { | |
| for (let i = 0; i < 24; i++) { | |
| if (this.board[i] === opponent) { | |
| removable.push(i); | |
| } | |
| } | |
| } | |
| return removable; | |
| } | |
| switchPlayer() { | |
| this.currentPlayer = this.currentPlayer === 'red' ? 'black' : 'red'; | |
| } | |
| checkGameEnd() { | |
| if (this.gamePhase === 'placing') return; // Don't check end conditions during placing phase | |
| const redCount = this.redPiecesOnBoard; | |
| const blackCount = this.blackPiecesOnBoard; | |
| // Check if someone has only 2 pieces left | |
| if (redCount < 3) { | |
| this.currentPlayer = 'black'; // Black wins | |
| this.gamePhase = 'game-over'; | |
| return; | |
| } | |
| if (blackCount < 3) { | |
| this.currentPlayer = 'red'; // Red wins | |
| this.gamePhase = 'game-over'; | |
| return; | |
| } | |
| // Check if current player has no valid moves (is blocked) | |
| if (!this.hasValidMoves()) { | |
| this.switchPlayer(); // Other player wins | |
| this.gamePhase = 'game-over'; | |
| } | |
| } | |
| hasValidMoves() { | |
| const piecesOnBoard = this.currentPlayer === 'red' ? this.redPiecesOnBoard : this.blackPiecesOnBoard; | |
| const canFly = piecesOnBoard === 3; | |
| // If can fly, just check if any empty position exists | |
| if (canFly) { | |
| return this.board.some(pos => pos === null); | |
| } | |
| // Otherwise, check if any piece can move to adjacent empty space | |
| for (let i = 0; i < 24; i++) { | |
| if (this.board[i] === this.currentPlayer) { | |
| if (this.adjacentPositions[i].some(adj => this.board[adj] === null)) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| updateDisplay() { | |
| // Update board positions | |
| for (let i = 0; i < 24; i++) { | |
| const positionEl = document.querySelector(`.position[data-index="${i}"]`); | |
| positionEl.className = 'position'; | |
| if (this.board[i] === 'red') { | |
| positionEl.classList.add('red', 'occupied'); | |
| } else if (this.board[i] === 'black') { | |
| positionEl.classList.add('black', 'occupied'); | |
| } | |
| if (this.selectedPosition === i) { | |
| positionEl.classList.add('selected'); | |
| } | |
| // Show valid moves for selected piece | |
| if (this.selectedPosition !== null && this.board[i] === null && !this.mustRemovePiece) { | |
| const piecesOnBoard = this.currentPlayer === 'red' ? this.redPiecesOnBoard : this.blackPiecesOnBoard; | |
| const canFly = piecesOnBoard === 3; | |
| if (canFly || this.adjacentPositions[this.selectedPosition].includes(i)) { | |
| positionEl.classList.add('valid-move'); | |
| } | |
| } | |
| // Show removable pieces | |
| if (this.mustRemovePiece) { | |
| const removablePieces = this.getRemovablePieces(); | |
| if (removablePieces.includes(i)) { | |
| positionEl.classList.add('removable'); | |
| } | |
| } | |
| } | |
| // Update player info | |
| document.getElementById('red-pieces').textContent = this.redPieces; | |
| document.getElementById('black-pieces').textContent = this.blackPieces; | |
| document.getElementById('red-on-board').textContent = this.redPiecesOnBoard; | |
| document.getElementById('black-on-board').textContent = this.blackPiecesOnBoard; | |
| // Update active player | |
| document.getElementById('player1-info').classList.toggle('active', this.currentPlayer === 'red'); | |
| document.getElementById('player2-info').classList.toggle('active', this.currentPlayer === 'black'); | |
| // Update game status | |
| const statusEl = document.getElementById('game-status'); | |
| statusEl.className = 'game-status'; | |
| let phaseText = ''; | |
| if (this.gamePhase === 'placing') { | |
| phaseText = 'Phase 1: Placing Pieces'; | |
| } else if (this.gamePhase === 'moving') { | |
| const redCanFly = this.redPiecesOnBoard === 3; | |
| const blackCanFly = this.blackPiecesOnBoard === 3; | |
| if (redCanFly || blackCanFly) { | |
| phaseText = 'Phase 3: Flying (3 pieces remaining)'; | |
| } else { | |
| phaseText = 'Phase 2: Moving Pieces'; | |
| } | |
| } else { | |
| phaseText = 'Game Over'; | |
| } | |
| if (this.gamePhase === 'game-over') { | |
| statusEl.innerHTML = ` | |
| <div class="phase-indicator">${phaseText}</div> | |
| <div class="status-won">${this.currentPlayer === 'red' ? 'Player 1 (Red)' : 'Player 2 (Black)'} Wins!</div> | |
| `; | |
| } else if (this.mustRemovePiece) { | |
| statusEl.innerHTML = ` | |
| <div class="phase-indicator">${phaseText}</div> | |
| <div class="status-removing">${this.currentPlayer === 'red' ? 'Player 1' : 'Player 2'} formed a mill! Remove an opponent's piece</div> | |
| `; | |
| } else if (this.gamePhase === 'placing') { | |
| statusEl.innerHTML = ` | |
| <div class="phase-indicator">${phaseText}</div> | |
| <div class="status-placing">${this.currentPlayer === 'red' ? 'Player 1' : 'Player 2'}'s turn - Place a piece</div> | |
| `; | |
| } else { | |
| const piecesOnBoard = this.currentPlayer === 'red' ? this.redPiecesOnBoard : this.blackPiecesOnBoard; | |
| const canFly = piecesOnBoard === 3; | |
| const actionText = canFly ? 'Move to any empty position' : 'Move a piece to adjacent position'; | |
| statusEl.innerHTML = ` | |
| <div class="phase-indicator">${phaseText}</div> | |
| <div class="status-moving">${this.currentPlayer === 'red' ? 'Player 1' : 'Player 2'}'s turn - ${actionText}</div> | |
| `; | |
| } | |
| } | |
| } | |
| let game; | |
| function initGame() { | |
| game = new NineMensMorris(); | |
| } | |
| function resetGame() { | |
| initGame(); | |
| } | |
| function toggleRules() { | |
| const rules = document.getElementById('rules'); | |
| rules.style.display = rules.style.display === 'none' ? 'block' : 'none'; | |
| } | |
| // Initialize the game when page loads | |
| window.addEventListener('load', initGame); | |
| </script> | |
| </body> | |
| </html> |