https://svonberg.org/wp-content/uploads/2025/12/roamanc2.html
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no”>
<title>Star City Mancala: Grandmaster AI</title>
<script src=”https://cdn.tailwindcss.com”></script>
<link href=”https://fonts.googleapis.com/css2?family=Rye&family=Montserrat:wght@400;700;900&display=swap” rel=”stylesheet”>
<style>
:root {
–neon-pink: #ff00ff;
–neon-cyan: #00ffff;
–bright-orange: #ff9900;
}
body {
font-family: ‘Montserrat’, sans-serif;
background: #0f172a; /* Deep Slate Base */
min-height: 100vh;
color: white;
overflow-x: hidden;
touch-action: manipulation;
}
/* — THEME STYLES — */
.board-container {
background: rgba(30, 41, 59, 0.95);
border-radius: 20px;
box-shadow: 0 0 40px rgba(0,0,0,0.9);
position: relative;
overflow: hidden;
border: 3px solid #64748b;
}
/* Player 1: 611 (Bright Orange/Steel) */
.text-rail { color: #fbbf24; font-family: ‘Rye’, serif; text-shadow: 0 0 5px #d97706; }
.pit-rail {
background: linear-gradient(135deg, #451a03, #7c2d12);
border: 2px solid #fdba74;
box-shadow: inset 0 0 10px rgba(251, 146, 60, 0.3);
}
.pit-rail.active-turn {
background: linear-gradient(135deg, #7c2d12, #9a3412);
box-shadow: 0 0 20px #fb923c;
border-color: #ffffff;
}
/* Player 2: Star (Neon Cyan/Pink) */
.text-star { color: #22d3ee; text-transform: uppercase; letter-spacing: 1px; font-weight: 900; text-shadow: 0 0 5px #0891b2; }
.pit-star {
background: linear-gradient(135deg, #312e81, #1e1b4b);
border: 2px solid #22d3ee;
box-shadow: inset 0 0 10px rgba(34, 211, 238, 0.3);
}
.pit-star.active-turn {
background: linear-gradient(135deg, #4338ca, #3730a3);
box-shadow: 0 0 20px #22d3ee;
border-color: #ffffff;
}
/* STONES – High Visibility */
.stone {
position: absolute;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 10;
}
/* Hot Coal */
.stone-coal {
width: 10px; height: 10px;
background: #475569;
border: 2px solid #fca5a5;
border-radius: 3px;
transform: rotate(45deg);
box-shadow: 0 0 5px #f87171;
}
/* Neon Star */
.stone-star {
width: 10px; height: 10px;
background: #fef08a;
border: 1px solid #fff;
border-radius: 50%;
box-shadow: 0 0 6px #facc15;
}
/* Utilities */
.pit-container { position: relative; width: 100%; display: flex; flex-direction: column; align-items: center; }
.pit {
width: 100%;
aspect-ratio: 1/1;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
position: relative;
transition: transform 0.1s;
}
.pit:active { transform: scale(0.95); }
/* Label Fit Adjustments for Mobile */
.pit-label {
font-size: 0.6rem; /* Reduced font size */
font-weight: 700;
color: white;
margin-top: 2px; /* Reduced margin */
text-shadow: 0 1px 2px black;
max-width: 100%;
letter-spacing: 0.2px; /* Tighter spacing */
line-height: 1.1;
text-align: center;
}
.modal-bg {
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(5px);
}
/* Thinking Indicator */
.thinking-pulse {
animation: pulse-glow 1s infinite alternate;
}
@keyframes pulse-glow {
from { box-shadow: 0 0 5px white; }
to { box-shadow: 0 0 20px white; border-color: white; }
}
@media (min-width: 768px) {
.stone-coal, .stone-star { width: 14px; height: 14px; }
.pit-label { font-size: 0.8rem; margin-top: 4px; letter-spacing: 0.5px; } /* Larger on desktop */
}
</style>
</head>
<body class=”flex flex-col items-center justify-between p-2 h-screen overflow-hidden bg-slate-900″>
<!– Header –>
<header class=”text-center mt-2 mb-1 z-10 shrink-0 w-full bg-slate-800/50 p-2 rounded-xl border border-slate-700 relative”>
<h1 class=”text-3xl font-black tracking-tighter uppercase text-white drop-shadow-md”>
<span class=”text-yellow-400″>STAR</span> <span class=”text-cyan-400″>CITY</span>
</h1>
<div class=”flex justify-center gap-4 items-center text-xs font-bold tracking-widest mt-1″>
<span class=”text-orange-400 font-[Rye] text-sm”>611 RAIL</span>
<span class=”text-slate-500″>|</span>
<span class=”text-cyan-300 text-sm”>NEON STAR</span>
</div>
<!– Rules Button –>
<button onclick=”toggleRulesModal(true)” class=”absolute right-3 top-1/2 -translate-y-1/2 p-2 rounded-full text-slate-400 hover:text-white transition duration-200″>
<svg xmlns=”http://www.w3.org/2000/svg” width=”24″ height=”24″ viewBox=”0 0 24 24″ fill=”none” stroke=”currentColor” stroke-width=”2″ stroke-linecap=”round” stroke-linejoin=”round” class=”lucide lucide-help-circle”><circle cx=”12″ cy=”12″ r=”10″/><path d=”M9.09 9a3 3 0 0 1 5.8 1c0 2-3 2-3 4″/><path d=”M12 17h.01″/></svg>
</button>
</header>
<!– Setup Panel –>
<div id=”setup-panel” class=”absolute inset-0 z-50 modal-bg flex items-center justify-center p-4″>
<div class=”w-full max-w-sm bg-slate-800 border-2 border-slate-600 rounded-xl p-6 shadow-2xl”>
<h3 class=”text-2xl font-black mb-6 text-center text-white uppercase tracking-widest”>Game Setup</h3>
<div class=”space-y-4″>
<!– P1 –>
<div class=”bg-slate-700 p-3 rounded-lg border-2 border-orange-500″>
<label class=”block text-xs font-bold text-orange-400 font-[Rye] mb-1 uppercase”>Bottom Player (Rail)</label>
<select id=”p1-type” class=”w-full bg-slate-900 text-white font-bold rounded p-3 border border-slate-500 focus:border-orange-500 outline-none”>
<option value=”human”>Human</option>
<option value=”cpu”>Grandmaster AI</option>
</select>
</div>
<!– P2 –>
<div class=”bg-slate-700 p-3 rounded-lg border-2 border-cyan-500″>
<label class=”block text-xs font-bold text-cyan-300 mb-1 uppercase”>Top Player (Star)</label>
<select id=”p2-type” class=”w-full bg-slate-900 text-white font-bold rounded p-3 border border-slate-500 focus:border-cyan-500 outline-none”>
<option value=”cpu”>Grandmaster AI</option>
<option value=”human”>Human</option>
</select>
</div>
<!– Difficulty Note –>
<p class=”text-[10px] text-slate-400 text-center italic mt-2″>
AI uses Minimax with Alpha-Beta pruning (Depth 6). Moves may take a second to calculate.
</p>
</div>
<button onclick=”startGame()” class=”mt-8 w-full bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-400 hover:to-red-500 text-white font-black py-4 rounded-xl shadow-lg uppercase tracking-wider text-sm border-2 border-white/20″>
START MATCH
</button>
</div>
</div>
<!– Rules Modal –>
<div id=”rules-modal” class=”hidden fixed inset-0 z-50 flex items-center justify-center modal-bg p-4″>
<div class=”bg-slate-800 border-2 border-slate-600 p-6 rounded-xl w-full max-w-md shadow-2xl max-h-[90vh] overflow-y-auto”>
<h2 class=”text-2xl font-black text-cyan-400 mb-4 uppercase text-center tracking-widest”>Mancala Rules</h2>
<div class=”space-y-4 text-slate-200 text-sm”>
<h3 class=”text-lg font-bold text-orange-400 border-b border-slate-600 pb-1″>The Goal</h3>
<p>Capture more stones than your opponent. Your store is on the far right (Pit 6).</p>
<h3 class=”text-lg font-bold text-orange-400 border-b border-slate-600 pb-1″>The Turn</h3>
<ol class=”list-decimal list-inside space-y-2 ml-2″>
<li>On your turn, choose any pit on your side (pits 0-5 for Rail, 7-12 for Star) that has stones.</li>
<li>You distribute the stones counter-clockwise, dropping one stone into each pit, including your own store (but skipping the opponent’s store).</li>
</ol>
<h3 class=”text-lg font-bold text-orange-400 border-b border-slate-600 pb-1″>Special Moves</h3>
<ul class=”list-disc list-inside space-y-2 ml-2″>
<li><span class=”font-bold text-green-400″>Free Turn:</span> If the last stone you drop lands in your own store, you get to take another turn immediately.</li>
<li><span class=”font-bold text-red-400″>Capture:</span> If the last stone you drop lands in an empty pit on your side of the board, you capture that stone **and** all stones in the pit directly opposite it (on the opponent’s side). All captured stones go straight into your store.</li>
</ul>
<h3 class=”text-lg font-bold text-orange-400 border-b border-slate-600 pb-1″>End of Game</h3>
<p>The game ends when all six pits on one side of the board are empty. The player who still has stones on their side moves all remaining stones into their own store. The player with the most stones in their store wins!</p>
</div>
<button onclick=”toggleRulesModal(false)” class=”mt-6 w-full bg-slate-700 hover:bg-slate-600 text-white font-bold py-3 rounded-lg text-sm uppercase shadow-lg border border-slate-500″>
Got It, Let’s Play
</button>
</div>
</div>
<!– Game Area –>
<div id=”game-area” class=”hidden flex-col items-center w-full max-w-lg flex-1 justify-center z-10″>
<!– Status Bar –>
<div class=”flex justify-between items-center w-full px-2 mb-2″>
<!– P2 Score –>
<div id=”p2-indicator” class=”flex flex-col items-center opacity-50 transition-all duration-300″>
<div class=”text-3xl font-black text-cyan-400 drop-shadow-[0_0_5px_rgba(34,211,238,0.8)]” id=”score-p2-display”>0</div>
<div class=”text-[10px] font-bold uppercase text-cyan-200″>The Star</div>
</div>
<!– Turn Message Box –>
<div id=”msg-box” class=”bg-slate-800 px-6 py-2 rounded-full border-2 border-slate-500 mx-2 shadow-lg min-w-[120px] text-center transition-all duration-300″>
<span id=”game-message” class=”font-bold text-white text-sm uppercase tracking-wide”>Ready</span>
</div>
<!– P1 Score –>
<div id=”p1-indicator” class=”flex flex-col items-center opacity-100 transition-all duration-300″>
<div class=”text-3xl font-[Rye] text-orange-400 drop-shadow-[0_0_5px_rgba(251,146,60,0.8)]” id=”score-p1-display”>0</div>
<div class=”text-[10px] font-bold uppercase text-orange-200″>The 611</div>
</div>
</div>
<!– The Board –>
<div class=”board-container w-[98vw] p-2 aspect-[4/3] max-h-[55vh] flex flex-col justify-center”>
<div class=”relative z-10 flex justify-between items-stretch h-full gap-1″>
<!– Player 2 Store (Left) –>
<div class=”flex flex-col items-center justify-center w-[14%]”>
<div id=”pit-13″ class=”w-full h-[90%] bg-slate-800 rounded-l-2xl border-2 border-cyan-500 flex flex-wrap content-center justify-center gap-1 p-1 shadow-[inset_0_0_10px_rgba(34,211,238,0.2)] relative transition-all duration-300″></div>
</div>
<!– Rows –>
<div class=”flex-1 flex flex-col justify-between py-1″>
<!– Top Row (Player 2 – Star) –>
<div class=”grid grid-cols-6 gap-1 w-full”>
<div class=”pit-container”><div onclick=”handlePitClick(12)” id=”pit-12″ class=”pit pit-star”></div></div>
<div class=”pit-container”><div onclick=”handlePitClick(11)” id=”pit-11″ class=”pit pit-star”></div></div>
<div class=”pit-container”><div onclick=”handlePitClick(10)” id=”pit-10″ class=”pit pit-star”></div></div>
<div class=”pit-container”><div onclick=”handlePitClick(9)” id=”pit-9″ class=”pit pit-star”></div></div>
<div class=”pit-container”><div onclick=”handlePitClick(8)” id=”pit-8″ class=”pit pit-star”></div></div>
<div class=”pit-container”><div onclick=”handlePitClick(7)” id=”pit-7″ class=”pit pit-star”></div></div>
</div>
<!– Labels Top –>
<div class=”grid grid-cols-6 gap-1 w-full text-center mb-1″>
<span class=”pit-label”>Hollins</span>
<span class=”pit-label”>Wilson</span>
<span class=”pit-label”>Civic</span>
<span class=”pit-label”>Market</span>
<span class=”pit-label”>Elm</span>
<span class=”pit-label”>Hosp</span>
</div>
<!– Center Divider –>
<div class=”h-[2px] w-full bg-slate-600 my-1 rounded-full relative flex items-center justify-center”>
<div class=”bg-slate-800 px-3 text-[9px] text-white font-bold uppercase tracking-widest border border-slate-600 rounded-full”>Roanoke River</div>
</div>
<!– Labels Bottom –>
<div class=”grid grid-cols-6 gap-1 w-full text-center mt-1″>
<span class=”pit-label”>Salem</span>
<span class=”pit-label”>Grand</span>
<span class=”pit-label”>Wasena</span>
<span class=”pit-label”>Old SW</span>
<span class=”pit-label”>Center</span>
<span class=”pit-label”>Vinton</span>
</div>
<!– Bottom Row (Player 1 – Rail) –>
<div class=”grid grid-cols-6 gap-1 w-full”>
<div class=”pit-container”><div onclick=”handlePitClick(0)” id=”pit-0″ class=”pit pit-rail”></div></div>
<div class=”pit-container”><div onclick=”handlePitClick(1)” id=”pit-1″ class=”pit pit-rail”></div></div>
<div class=”pit-container”><div onclick=”handlePitClick(2)” id=”pit-2″ class=”pit pit-rail”></div></div>
<div class=”pit-container”><div onclick=”handlePitClick(3)” id=”pit-3″ class=”pit pit-rail”></div></div>
<div class=”pit-container”><div onclick=”handlePitClick(4)” id=”pit-4″ class=”pit pit-rail”></div></div>
<div class=”pit-container”><div onclick=”handlePitClick(5)” id=”pit-5″ class=”pit pit-rail”></div></div>
</div>
</div>
<!– Player 1 Store (Right) –>
<div class=”flex flex-col items-center justify-center w-[14%]”>
<div id=”pit-6″ class=”w-full h-[90%] bg-slate-800 rounded-r-2xl border-2 border-orange-500 flex flex-wrap content-center justify-center gap-1 p-1 shadow-[inset_0_0_10px_rgba(251,146,60,0.2)] relative transition-all duration-300″></div>
</div>
</div>
</div>
<button onclick=”resetSetup()” class=”mt-4 text-xs font-bold text-slate-400 hover:text-white uppercase tracking-widest border-b border-dotted border-slate-500 pb-1″>
End Game / Menu
</button>
</div>
<!– Winner Modal –>
<div id=”winner-modal” class=”hidden fixed inset-0 z-50 flex items-center justify-center modal-bg p-4″>
<div class=”bg-slate-800 border-2 border-white p-6 rounded-xl text-center w-full max-w-sm shadow-2xl”>
<h2 id=”winner-title” class=”text-3xl font-black text-white mb-2 uppercase italic”>GAME OVER</h2>
<p id=”winner-text” class=”text-lg text-slate-200 mb-6 font-bold”>Result</p>
<div class=”flex gap-3 justify-center”>
<button onclick=”startGame()” class=”flex-1 bg-green-600 hover:bg-green-500 text-white font-bold py-3 rounded-lg text-sm uppercase shadow-lg”>Rematch</button>
<button onclick=”resetSetup()” class=”flex-1 bg-slate-600 hover:bg-slate-500 text-white font-bold py-3 rounded-lg text-sm uppercase shadow-lg”>Menu</button>
</div>
</div>
</div>
<script>
// — GAME LOGIC —
let board = [], currentPlayer = 1;
let p1Type = ‘human’, p2Type = ‘human’;
let isProcessing = false;
const AI_DEPTH = 6; // Deep search
// Indices
const P1_PITS = [0,1,2,3,4,5], P1_STORE = 6;
const P2_PITS = [7,8,9,10,11,12], P2_STORE = 13;
function startGame() {
p1Type = document.getElementById(‘p1-type’).value;
p2Type = document.getElementById(‘p2-type’).value;
document.getElementById(‘setup-panel’).classList.add(‘hidden’);
document.getElementById(‘game-area’).classList.remove(‘hidden’);
document.getElementById(‘game-area’).classList.add(‘flex’);
document.getElementById(‘winner-modal’).classList.add(‘hidden’);
// Standard Mancala: 4 stones per pit
board = Array(14).fill(4);
board[6]=0; board[13]=0;
currentPlayer = 1;
isProcessing = false;
renderBoard();
updateStatus();
checkAI();
}
function resetSetup() {
document.getElementById(‘setup-panel’).classList.remove(‘hidden’);
document.getElementById(‘game-area’).classList.add(‘hidden’);
document.getElementById(‘game-area’).classList.remove(‘flex’);
document.getElementById(‘winner-modal’).classList.add(‘hidden’);
}
// Toggles the Rules Modal
function toggleRulesModal(show) {
const modal = document.getElementById(‘rules-modal’);
if (show) {
modal.classList.remove(‘hidden’);
} else {
modal.classList.add(‘hidden’);
}
}
function handlePitClick(idx) {
if (isProcessing || board[idx] === 0) return;
// Validate Turn
if (currentPlayer === 1 && (idx < 0 || idx > 5)) return;
if (currentPlayer === 2 && (idx < 7 || idx > 12)) return;
// Validate Human
if ((currentPlayer === 1 && p1Type === ‘cpu’) || (currentPlayer === 2 && p2Type === ‘cpu’)) return;
executeMove(idx, true); // True for real visual move
}
// The logic for moving stones (used by both UI and AI simulation)
// returns { board: [], repeatTurn: bool, captured: bool }
function simulateMoveLogic(simBoard, idx, player) {
let stones = simBoard[idx];
simBoard[idx] = 0;
let curr = idx;
while (stones > 0) {
curr = (curr + 1) % 14;
// Skip opponent store
if (player === 1 && curr === 13) continue;
if (player === 2 && curr === 6) continue;
simBoard[curr]++;
stones–;
}
let repeat = false;
let captured = false;
let captureAmt = 0;
// Free Turn Check
if (player === 1 && curr === 6) repeat = true;
if (player === 2 && curr === 13) repeat = true;
// Capture Check
// Rule: Land in own empty pit AND opposite pit has stones
const inOwnPit = (player === 1 && curr >= 0 && curr <= 5) ||
(player === 2 && curr >= 7 && curr <= 12);
if (!repeat && inOwnPit && simBoard[curr] === 1) {
const opp = 12 – curr;
if (simBoard[opp] > 0) {
captured = true;
captureAmt = simBoard[opp] + 1;
simBoard[opp] = 0;
simBoard[curr] = 0;
if (player === 1) simBoard[6] += captureAmt;
else simBoard[13] += captureAmt;
}
}
return { newBoard: simBoard, repeat: repeat, captured: captured, finalIdx: curr };
}
async function executeMove(idx, visual) {
isProcessing = true;
if (visual) {
// VISUAL MODE: Animate stones
let stones = board[idx]; board[idx] = 0;
renderBoard(); await sleep(100);
let curr = idx;
while (stones > 0) {
curr = (curr + 1) % 14;
if (currentPlayer === 1 && curr === 13) continue;
if (currentPlayer === 2 && curr === 6) continue;
board[curr]++; stones–;
renderBoard(); await sleep(50);
}
// Logic checks after animation
const inOwnStore = (currentPlayer===1 && curr===6) || (currentPlayer===2 && curr===13);
const inOwnPit = (currentPlayer===1 && curr>=0 && curr<=5) || (currentPlayer===2 && curr>=7 && curr<=12);
if (!inOwnStore && inOwnPit && board[curr]===1) {
const opp = 12 – curr;
if (board[opp] > 0) {
await sleep(150);
const loot = board[opp] + 1;
board[opp] = 0; board[curr] = 0;
if (currentPlayer===1) board[6] += loot; else board[13] += loot;
renderBoard(); showToast(“CAPTURED!”);
}
}
if (P1_PITS.every(i=>board[i]===0) || P2_PITS.every(i=>board[i]===0)) { endGame(); return; }
if (inOwnStore) {
showToast(“FREE TURN!”);
// Player stays same
} else {
currentPlayer = currentPlayer === 1 ? 2 : 1;
}
updateStatus();
isProcessing = false;
checkAI();
} else {
// PURE LOGIC MODE (for AI state updates, not usually called in main loop)
// We typically use simulateMoveLogic inside AI, and executeMove(true) for the real thing
}
}
function checkAI() {
if (P1_PITS.every(i=>board[i]===0) || P2_PITS.every(i=>board[i]===0)) return;
const isCpuTurn = (currentPlayer===1 && p1Type===’cpu’) || (currentPlayer===2 && p2Type===’cpu’);
if (isCpuTurn) {
// Show thinking UI
document.getElementById(‘msg-box’).classList.add(‘thinking-pulse’);
document.getElementById(‘game-message’).innerText = “CALCULATING…”;
// Allow UI to render “Thinking” before blocking thread
setTimeout(() => {
const move = getBestMoveMinimax(board, currentPlayer);
document.getElementById(‘msg-box’).classList.remove(‘thinking-pulse’);
executeMove(move, true);
}, 100);
}
}
// — SMART AI ENGINE (Minimax + Alpha Beta) —
function getBestMoveMinimax(currentBoard, player) {
// Root of the recursion
// Maximize for ‘player’
let validMoves = (player === 1 ? P1_PITS : P2_PITS).filter(i => currentBoard[i] > 0);
let bestScore = -Infinity;
let bestMove = validMoves[0];
// Heuristic for very late game (if only 1 move, take it)
if (validMoves.length === 1) return validMoves[0];
// Search Depth
const depth = AI_DEPTH;
for (let move of validMoves) {
let sim = simulateMoveLogic([…currentBoard], move, player);
// If repeat turn, we do not switch player and keep depth same (or decrement slightly to ensure convergence)
let score;
if (sim.repeat) {
score = minimax(sim.newBoard, depth, -Infinity, Infinity, true, player);
} else {
score = minimax(sim.newBoard, depth – 1, -Infinity, Infinity, false, player);
}
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
}
return bestMove;
}
function minimax(nodeBoard, depth, alpha, beta, isMaximizing, rootPlayer) {
// 1. Check Terminal State (Game Over) or Depth 0
if (depth <= 0) {
return evaluateBoard(nodeBoard, rootPlayer);
}
const p1Empty = P1_PITS.every(i => nodeBoard[i] === 0);
const p2Empty = P2_PITS.every(i => nodeBoard[i] === 0);
if (p1Empty || p2Empty) {
// Game Over State – Calculate final real score
let finalBoard = […nodeBoard];
let s1 = 0; P1_PITS.forEach(i => s1+=finalBoard[i]);
let s2 = 0; P2_PITS.forEach(i => s2+=finalBoard[i]);
finalBoard[6] += s1;
finalBoard[13] += s2;
return evaluateBoard(finalBoard, rootPlayer) * 100; // Heavily weight winning
}
// Determine whose turn it is in the simulation
// If isMaximizing, it’s rootPlayer’s turn.
// If !isMaximizing, it’s opponent’s turn.
const currentPlayerSim = isMaximizing ? rootPlayer : (rootPlayer === 1 ? 2 : 1);
const moves = (currentPlayerSim === 1 ? P1_PITS : P2_PITS).filter(i => nodeBoard[i] > 0);
if (isMaximizing) {
let maxEval = -Infinity;
for (let move of moves) {
let sim = simulateMoveLogic([…nodeBoard], move, currentPlayerSim);
let eval;
// Handling Free Turns in Recursion
if (sim.repeat) {
eval = minimax(sim.newBoard, depth, alpha, beta, true, rootPlayer);
} else {
eval = minimax(sim.newBoard, depth – 1, alpha, beta, false, rootPlayer);
}
maxEval = Math.max(maxEval, eval);
alpha = Math.max(alpha, eval);
if (beta <= alpha) break; // Prune
}
return maxEval;
} else {
let minEval = Infinity;
for (let move of moves) {
let sim = simulateMoveLogic([…nodeBoard], move, currentPlayerSim);
let eval;
if (sim.repeat) {
// If opponent gets free turn, it’s STILL minimizing (opponent still playing)
eval = minimax(sim.newBoard, depth, alpha, beta, false, rootPlayer);
} else {
// Turn switches back to Maximizer
eval = minimax(sim.newBoard, depth – 1, alpha, beta, true, rootPlayer);
}
minEval = Math.min(minEval, eval);
beta = Math.min(beta, eval);
if (beta <= alpha) break; // Prune
}
return minEval;
}
}
function evaluateBoard(b, rootPlayer) {
const p1Score = b[6];
const p2Score = b[13];
// Basic Score Diff
let score = rootPlayer === 1 ? (p1Score – p2Score) : (p2Score – p1Score);
// Weight: Actual score is huge
let heuristic = score * 100;
// Heuristic: Stones on my side (Hoarding / Defense)
// Having stones on your side is generally good as it gives you move options and denies capture
let p1Stones = 0; P1_PITS.forEach(i => p1Stones += b[i]);
let p2Stones = 0; P2_PITS.forEach(i => p2Stones += b[i]);
let stoneDiff = rootPlayer === 1 ? (p1Stones – p2Stones) : (p2Stones – p1Stones);
heuristic += stoneDiff * 2;
return heuristic;
}
// — VISUALS —
function endGame() {
let s1 = 0; P1_PITS.forEach(i => { s1+=board[i]; board[i]=0; });
let s2 = 0; P2_PITS.forEach(i => { s2+=board[i]; board[i]=0; });
board[6]+=s1; board[13]+=s2; renderBoard();
const title = document.getElementById(‘winner-title’);
const txt = document.getElementById(‘winner-text’);
if (board[6] > board[13]) {
title.innerText = “611 TRIUMPHS!”;
title.className = “text-2xl font-black text-orange-500 mb-2 uppercase italic”;
txt.innerText = `Final Score: ${board[6]} – ${board[13]}`;
} else if (board[13] > board[6]) {
title.innerText = “STAR SHINES!”;
title.className = “text-2xl font-black text-cyan-400 mb-2 uppercase italic”;
txt.innerText = `Final Score: ${board[13]} – ${board[6]}`;
} else {
title.innerText = “DRAW GAME”; title.className = “text-2xl font-black text-white mb-2”;
txt.innerText = `${board[6]} – ${board[13]}`;
}
document.getElementById(‘winner-modal’).classList.remove(‘hidden’);
}
function renderBoard() {
for (let i = 0; i < 14; i++) {
const el = document.getElementById(`pit-${i}`);
if (!el) continue;
el.innerHTML = ”;
const count = Math.min(board[i], 20);
for(let k=0; k<count; k++) {
const s = document.createElement(‘div’);
s.className = (i <= 6) ? ‘stone stone-coal’ : ‘stone stone-star’;
const rx = (Math.random()*16)-8; const ry = (Math.random()*16)-8;
s.style.transform = `translate(${rx}px, ${ry}px)`;
el.appendChild(s);
}
if (i!==6 && i!==13) {
if (board[i]===0) el.classList.add(‘opacity-40’, ‘cursor-default’);
else el.classList.remove(‘opacity-40’, ‘cursor-default’);
}
}
document.getElementById(‘score-p1-display’).innerText = board[6];
document.getElementById(‘score-p2-display’).innerText = board[13];
}
function updateStatus() {
const msg = document.getElementById(‘game-message’);
const p1 = document.getElementById(‘p1-indicator’);
const p2 = document.getElementById(‘p2-indicator’);
document.querySelectorAll(‘.pit’).forEach(p=>p.classList.remove(‘active-turn’));
if (currentPlayer === 1) {
msg.innerText = “611’S TURN”;
msg.className = “font-black text-sm uppercase tracking-wide text-orange-400”;
p1.classList.replace(‘opacity-50’, ‘opacity-100’); p1.style.transform = “scale(1.1)”;
p2.classList.replace(‘opacity-100’, ‘opacity-50’); p2.style.transform = “scale(0.9)”;
if(p1Type===’human’) document.querySelectorAll(‘.pit-rail’).forEach(p=>p.classList.add(‘active-turn’));
} else {
msg.innerText = “STAR’S TURN”;
msg.className = “font-black text-sm uppercase tracking-wide text-cyan-400”;
p2.classList.replace(‘opacity-50’, ‘opacity-100’); p2.style.transform = “scale(1.1)”;
p1.classList.replace(‘opacity-100’, ‘opacity-50’); p1.style.transform = “scale(0.9)”;
if(p2Type===’human’) document.querySelectorAll(‘.pit-star’).forEach(p=>p.classList.add(‘active-turn’));
}
}
function showToast(txt) {
const el = document.getElementById(‘game-message’);
const old = el.innerText;
el.innerText = txt; el.classList.add(‘text-white’);
setTimeout(()=>{ el.innerText = old; el.classList.remove(‘text-white’); updateStatus(); }, 1000);
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
</script>
</body>
</html>
