diff --git a/src/App.tsx b/src/App.tsx
index 285d192..1e3ee72 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,8 +1,14 @@
import { useState, useEffect, useMemo } from 'react';
import { Client } from 'boardgame.io/react';
import { Local } from 'boardgame.io/multiplayer';
+import { RandomBot, MCTSBot } from 'boardgame.io/ai';
import { games, gameIds, type GameDefinition } from './registry';
+type AISettings = {
+ player0: boolean;
+ player1: boolean;
+};
+
function GameSelector({ onSelect }: { onSelect: (id: string) => void }) {
return (
@@ -77,16 +83,77 @@ function Rules({ rules }: { rules: string }) {
);
}
-function GameView({ gameId, definition }: { gameId: string; definition: GameDefinition }) {
- const GameClient = useMemo(
- () =>
- Client({
- game: definition.game,
- board: definition.Board,
- multiplayer: Local(),
- }),
- [gameId, definition]
+function AIToggle({
+ label,
+ checked,
+ onChange,
+}: {
+ label: string;
+ checked: boolean;
+ onChange: (checked: boolean) => void;
+}) {
+ return (
+
);
+}
+
+// Factory to create MCTSBot with game-specific options
+function createMCTSBot(game: GameDefinition['game']) {
+ return class extends MCTSBot {
+ constructor(opts: ConstructorParameters
[0]) {
+ super({
+ ...opts,
+ game,
+ iterations: 500,
+ playoutDepth: 20,
+ });
+ }
+ };
+}
+
+function GameView({ gameId, definition }: { gameId: string; definition: GameDefinition }) {
+ const [aiSettings, setAiSettings] = useState({ player0: false, player1: false });
+
+ const GameClient = useMemo(() => {
+ // Use MCTSBot for tic-tac-toe (turn-based), RandomBot for others (like simultaneous RPS)
+ const useMCTS = gameId === 'tic-tac-toe';
+ const BotClass = useMCTS ? createMCTSBot(definition.game) : RandomBot;
+
+ // Build the bots configuration based on AI settings
+ const bots: Record = {};
+ if (aiSettings.player0) {
+ bots['0'] = BotClass;
+ }
+ if (aiSettings.player1) {
+ bots['1'] = BotClass;
+ }
+
+ return Client({
+ game: definition.game,
+ board: definition.Board,
+ multiplayer: Local({ bots: Object.keys(bots).length > 0 ? bots : undefined }),
+ });
+ }, [gameId, definition, aiSettings]);
return (
@@ -102,13 +169,41 @@ function GameView({ gameId, definition }: { gameId: string; definition: GameDefi
← Back to games
-
+
+
+
AI Players:
+
setAiSettings((s) => ({ ...s, player0: checked }))}
+ />
+ setAiSettings((s) => ({ ...s, player1: checked }))}
+ />
+
+
+
-
Player 1
+
+ Player 1 {aiSettings.player0 && (AI)}
+
-
Player 2
+
+ Player 2 {aiSettings.player1 && (AI)}
+
diff --git a/src/games/rock-paper-scissors/Game.ts b/src/games/rock-paper-scissors/Game.ts
index 92b121c..399fdd2 100644
--- a/src/games/rock-paper-scissors/Game.ts
+++ b/src/games/rock-paper-scissors/Game.ts
@@ -73,4 +73,15 @@ export const RPS: Game
= {
if (G.scores['0'] >= 2) return { winner: '0' };
if (G.scores['1'] >= 2) return { winner: '1' };
},
+
+ ai: {
+ enumerate: (G: RPSState, _ctx: unknown, playerID: string) => {
+ // Player can only make a move if they haven't chosen yet this round
+ if (G.moves[playerID as '0' | '1'] !== null) {
+ return [];
+ }
+ const choices: Move[] = ['rock', 'paper', 'scissors'];
+ return choices.map((choice) => ({ move: 'choose', args: [choice] }));
+ },
+ },
};
diff --git a/src/games/tic-tac-toe/Game.ts b/src/games/tic-tac-toe/Game.ts
index 1358fb1..3122809 100644
--- a/src/games/tic-tac-toe/Game.ts
+++ b/src/games/tic-tac-toe/Game.ts
@@ -33,6 +33,18 @@ export const TicTacToe: Game = {
return { draw: true };
}
},
+
+ ai: {
+ enumerate: (G: TicTacToeState) => {
+ const moves: Array<{ move: string; args: [number] }> = [];
+ for (let i = 0; i < G.cells.length; i++) {
+ if (G.cells[i] === null) {
+ moves.push({ move: 'clickCell', args: [i] });
+ }
+ }
+ return moves;
+ },
+ },
};
export function isVictory(cells: (string | null)[]): boolean {