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 {