NineMensMorris / index.html
SorrelC's picture
Update index.html
a3f84f2 verified
<!DOCTYPE html>
<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>