From 4ef07605468c11190825f8b3b3c943a394db20e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Jan 2026 13:40:58 +0000 Subject: [PATCH 1/2] feat: add AI player toggle for boardgame.io games Add support for toggling AI (RandomBot) for either or both players: - Add ai.enumerate functions to Tic-Tac-Toe and Rock-Paper-Scissors games - Create AI toggle UI controls in the game view header - Configure Local() multiplayer with bots based on AI settings - Show (AI) indicator next to player names when AI is enabled https://claude.ai/code/session_018cNpftMV4MyLtbm7k5jSSh --- src/App.tsx | 101 +++++++++++++++++++++++--- src/games/rock-paper-scissors/Game.ts | 11 +++ src/games/tic-tac-toe/Game.ts | 12 +++ 3 files changed, 112 insertions(+), 12 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 285d192..570fae9 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 } 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,59 @@ 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 ( + ); +} + +function GameView({ gameId, definition }: { gameId: string; definition: GameDefinition }) { + const [aiSettings, setAiSettings] = useState({ player0: false, player1: false }); + + const GameClient = useMemo(() => { + // Build the bots configuration based on AI settings + const bots: Record = {}; + if (aiSettings.player0) { + bots['0'] = RandomBot; + } + if (aiSettings.player1) { + bots['1'] = RandomBot; + } + + return Client({ + game: definition.game, + board: definition.Board, + multiplayer: Local({ bots: Object.keys(bots).length > 0 ? bots : undefined }), + }); + }, [gameId, definition, aiSettings]); return (
@@ -102,13 +151,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 { From f43c93db4ea925e1ef367dcf6cc077d309bdeef1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Jan 2026 14:36:00 +0000 Subject: [PATCH 2/2] feat: use MCTSBot for Tic-Tac-Toe AI Switch from RandomBot to MCTSBot (Monte Carlo Tree Search) for Tic-Tac-Toe to provide smarter AI gameplay. RandomBot is still used for Rock-Paper-Scissors since MCTS doesn't work well with simultaneous move games. MCTSBot configuration: - 500 iterations per move decision - 20 playout depth for simulation https://claude.ai/code/session_018cNpftMV4MyLtbm7k5jSSh --- src/App.tsx | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 570fae9..1e3ee72 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useMemo } from 'react'; import { Client } from 'boardgame.io/react'; import { Local } from 'boardgame.io/multiplayer'; -import { RandomBot } from 'boardgame.io/ai'; +import { RandomBot, MCTSBot } from 'boardgame.io/ai'; import { games, gameIds, type GameDefinition } from './registry'; type AISettings = { @@ -117,17 +117,35 @@ function AIToggle({ ); } +// 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'] = RandomBot; + bots['0'] = BotClass; } if (aiSettings.player1) { - bots['1'] = RandomBot; + bots['1'] = BotClass; } return Client({