Nine Men's Morris (local)

by Ben Gillbanks

HTML

<h2>Nine Men's Morris (local two-player)</h2>
<div class="info">
	<span id="status">Placing phase: Player X to move (X has placed 0/9)</span>
</div>
<div id="board"></div>
<div>
	<button id="reset">Reset</button>
	<span class="small">Click empty dot to place/move. When you form a mill, click opponent piece to capture.</span>
</div>

CSS

body {
		font-family: system-ui,-apple-system,BlinkMacSystemFont,sans-serif;
		background:#f3f3f3;
		margin:0;
		display:flex;
		flex-direction:column;
		align-items:center;
		padding:1rem;
	}
	#board {
		position:relative;
		width:480px;
		height:480px;
		background:#fff;
		box-shadow:0 4px 16px rgba(0,0,0,0.1);
		margin-bottom:1rem;
	}
	svg {
		width:100%;
		height:100%;
		user-select:none;
	}
	.point {
		cursor:pointer;
	}
	.piece {
		pointer-events:none;
	}
	.info {
		margin-bottom:0.5rem;
	}
	button {
		padding:0.5rem 1rem;
		margin-right:0.5rem;
	}
	.small {
		font-size:0.85rem;
		color:#555;
	}

JavaScript

// Board points with coords (normalized 0..1) and adjacency list
const points = {
	A1: {x:0,   y:0,   adj:['D1','A4']},
	D1: {x:0.5, y:0,   adj:['A1','G1','D2']},
	G1: {x:1,   y:0,   adj:['D1','G4']},
	B2: {x:0.166, y:0.166, adj:['D2','B4']},
	D2: {x:0.5, y:0.166, adj:['B2','F2','D1','D3']},
	F2: {x:0.833, y:0.166, adj:['D2','F4']},
	C3: {x:0.333, y:0.333, adj:['D3','C4']},
	D3: {x:0.5, y:0.333, adj:['C3','E3','D2']},
	E3: {x:0.666, y:0.333, adj:['D3','E4']},
	A4: {x:0,   y:0.5, adj:['A1','B4','A7']},
	B4: {x:0.166, y:0.5, adj:['A4','B2','C4','B6']},
	C4: {x:0.333, y:0.5, adj:['C3','B4','E4','C6']},
	E4: {x:0.666, y:0.5, adj:['E3','C4','F4','E6']},
	F4: {x:0.833, y:0.5, adj:['F2','E4','F6','G4']},
	G4: {x:1,   y:0.5, adj:['G1','F4','G7']},
	C6: {x:0.333, y:0.666, adj:['C4','D6']},
	D6: {x:0.5, y:0.666, adj:['C6','F6','D7']},
	F6: {x:0.833, y:0.666, adj:['D6','F4']},
	B6: {x:0.166, y:0.833, adj:['B4','D7']},
	D7: {x:0.5, y:0.833, adj:['B6','D6','G7']},
	G7: {x:1,   y:0.833, adj:['D7','G4']},
	A7: {x:0,   y:1,   adj:['A4','D7']},
};

// Predefined mills (sets of 3)
const mills = [
	['A1','D1','G1'],
	['B2','D2','F2'],
	['C3','D3','E3'],
	['A4','B4','C4'],
	['E4','F4','G4'],
	['C6','D6','E6'], // note E6 missing from points; fix: E6 isn't defined - adjust according to standard: actually mills include C6-D6-E6 and B6-D7-G7? Wait. We'll derive dynamically
	['A7','D7','G7'],
	['A1','A4','A7'],
	['B2','B4','B6'],
	['C3','C4','C6'],
	['D1','D2','D3'],
	['E3','E4','E6'],
	['F2','F4','F6'],
	['G1','G4','G7'],
	['G7','D7','A7'],
];

// Correction: define missing E6 and F6 etc? The earlier structure had E6 implicit; we should add them
// Let's add E6 to points and adjust adjacency for C6, E6, F6 etc to match standard board.
points.E6 = {x:0.666, y:0.666, adj:['E4','D6','F6']};
points.F6.adj = ['D6','F4','E6'];
points.C6.adj = ['C4','D6'];
points.D6.adj = ['C6','F6','D7'];
points.D7.adj = ['B6','D6','G7'];
points.B6.adj =...