import { defineBot, simulate } from "@openturn/bot";
import { ticTacToe } from "@my/tic-tac-toe-game";
const OTHER: Record<string, string> = { "0": "1", "1": "0" };
interface ResultLike { winner?: string; draw?: boolean }
function legalForSnapshot(snapshot: { G: { board: (string | null)[][] } }, playerID: string) {
const out = [];
for (let row = 0; row < snapshot.G.board.length; row += 1) {
const cells = snapshot.G.board[row]!;
for (let col = 0; col < cells.length; col += 1) {
if (cells[col] === null) {
out.push({ event: "placeMark", payload: { row, col } });
}
}
}
return out;
}
function evaluate(snapshot: { meta: { result: ResultLike | null } }, me: string): number | null {
const r = snapshot.meta.result;
if (r === null || r === undefined) return null;
if (r.draw === true) return 0;
if (r.winner === me) return 10;
if (typeof r.winner === "string") return -10;
return 0;
}
function search(snapshot: never, toMove: string, me: string, depth: number, alpha: number, beta: number): number {
const terminal = evaluate(snapshot as never, me);
if (terminal !== null) return terminal - Math.sign(terminal) * depth;
const moves = legalForSnapshot(snapshot as never, toMove);
if (moves.length === 0) return 0;
if (toMove === me) {
let best = -Infinity;
for (const action of moves) {
const sim = simulate(ticTacToe, snapshot, toMove as never, action);
if (!sim.ok) continue;
best = Math.max(best, search(sim.next as never, OTHER[toMove]!, me, depth + 1, alpha, beta));
alpha = Math.max(alpha, best);
if (alpha >= beta) break;
}
return best;
}
let best = Infinity;
for (const action of moves) {
const sim = simulate(ticTacToe, snapshot, toMove as never, action);
if (!sim.ok) continue;
best = Math.min(best, search(sim.next as never, OTHER[toMove]!, me, depth + 1, alpha, beta));
beta = Math.min(beta, best);
if (alpha >= beta) break;
}
return best;
}
export const minimaxBot = defineBot<typeof ticTacToe>({
name: "minimax",
thinkingBudgetMs: 5_000,
decide({ legalActions, snapshot, playerID }) {
if (legalActions.length === 0) throw new Error("minimaxBot: no legal actions");
if (snapshot === null) return legalActions[0]!; // hosted host: simulate unavailable
let bestAction = legalActions[0]!;
let bestScore = -Infinity;
for (const action of legalActions) {
const sim = simulate(ticTacToe, snapshot, playerID, action);
if (!sim.ok) continue;
const score = search(sim.next as never, OTHER[playerID]!, playerID, 1, -Infinity, Infinity);
if (score > bestScore) { bestScore = score; bestAction = action; }
}
return bestAction;
},
});