Skip to content

Commit e2ab29c

Browse files
Update so that either player can go first
1 parent 3389c07 commit e2ab29c

File tree

9 files changed

+93
-21
lines changed

9 files changed

+93
-21
lines changed

mcp-server/src/handlers/elicitation-handlers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export async function elicitGameCreationPreferences(
3030
type: "string",
3131
enum: ["X", "O"],
3232
title: "Your Symbol",
33-
description: "Do you want to be X (goes first) or O (goes second)?"
33+
description: "Do you want to be X (goes first) or O (goes second)?",
34+
default: "X"
3435
},
3536
playerName: {
3637
type: "string",

mcp-server/src/handlers/game-operations.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,8 @@ export async function createGame(
318318
gameType: string,
319319
playerName: string = 'Player',
320320
gameId?: string,
321-
aiDifficulty: string = 'medium'
321+
aiDifficulty: string = 'medium',
322+
gameSpecificOptions?: Record<string, any>
322323
) {
323324
// Check if game already exists (for games that support custom IDs)
324325
if (gameId && gameType === 'tic-tac-toe') {
@@ -341,7 +342,7 @@ export async function createGame(
341342
}
342343

343344
// Create new game via API
344-
const gameSession = await createGameViaAPI(gameType, playerName, gameId, aiDifficulty)
345+
const gameSession = await createGameViaAPI(gameType, playerName, gameId, aiDifficulty, gameSpecificOptions)
345346

346347
const response: any = {
347348
gameId: gameSession.gameState.id,

mcp-server/src/handlers/tool-handlers.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ export const TOOL_DEFINITIONS = [
103103
description: 'AI difficulty level',
104104
default: 'medium',
105105
},
106+
playerSymbol: {
107+
type: 'string',
108+
enum: ['X', 'O'],
109+
description: 'Your symbol: X (goes first) or O (goes second)',
110+
default: 'X',
111+
},
106112
},
107113
required: [],
108114
},
@@ -202,9 +208,12 @@ export async function handleToolCall(name: string, args: any, server?: any) {
202208
const {
203209
playerName: ticTacToePlayerName = 'Player',
204210
gameId: ticTacToeNewGameId,
205-
aiDifficulty: ticTacToeAiDifficulty = 'medium'
211+
aiDifficulty: ticTacToeAiDifficulty = 'medium',
212+
playerSymbol: ticTacToePlayerSymbol = 'X'
206213
} = args
207-
return await createGame('tic-tac-toe', ticTacToePlayerName, ticTacToeNewGameId, ticTacToeAiDifficulty)
214+
215+
const ticTacToeGameOptions = ticTacToePlayerSymbol ? { playerSymbol: ticTacToePlayerSymbol } : undefined
216+
return await createGame('tic-tac-toe', ticTacToePlayerName, ticTacToeNewGameId, ticTacToeAiDifficulty, ticTacToeGameOptions)
208217

209218
case 'create_rock_paper_scissors_game':
210219
const {
@@ -262,8 +271,17 @@ async function createGameInteractive(gameType: string, gameId?: string, server?:
262271
const finalPlayerName = playerName || 'Player'
263272
const finalDifficulty = difficulty || 'medium'
264273

274+
// Prepare game-specific options
275+
const gameSpecificOptions: Record<string, any> = {}
276+
if (gameType === 'tic-tac-toe' && playerSymbol) {
277+
gameSpecificOptions.playerSymbol = playerSymbol
278+
}
279+
if (gameType === 'rock-paper-scissors' && maxRounds) {
280+
gameSpecificOptions.maxRounds = maxRounds
281+
}
282+
265283
// Create the game with elicited preferences
266-
const gameResult = await createGame(gameType, finalPlayerName, gameId, finalDifficulty)
284+
const gameResult = await createGame(gameType, finalPlayerName, gameId, finalDifficulty, gameSpecificOptions)
267285

268286
// Add elicitation information to the response
269287
gameResult.elicitation = {
@@ -274,6 +292,11 @@ async function createGameInteractive(gameType: string, gameId?: string, server?:
274292
// Add game-specific messages
275293
if (gameType === 'tic-tac-toe' && playerSymbol) {
276294
gameResult.message += ` You are playing as ${playerSymbol}.`
295+
if (playerSymbol === 'X') {
296+
gameResult.message += ' You go first!'
297+
} else {
298+
gameResult.message += ' AI goes first!'
299+
}
277300
}
278301
if (gameType === 'rock-paper-scissors' && maxRounds) {
279302
gameResult.message += ` Playing ${maxRounds} rounds.`

mcp-server/src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const server = new Server(
2626
tools: {},
2727
resources: {},
2828
prompts: {},
29-
sampling: {},
29+
elicitation: {},
3030
},
3131
}
3232
)

shared/src/games/rock-paper-scissors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ export class RockPaperScissorsGame implements Game<RPSGameState, RPSMove> {
221221
* - Game status set to 'playing'
222222
* - Current round set to 0
223223
*/
224-
getInitialState(players: Player[]): RPSGameState {
224+
getInitialState(players: Player[], options?: any): RPSGameState {
225225
const maxRounds = 3; // Best of 3
226226
const rounds = Array.from({ length: maxRounds }, () => ({}));
227227

shared/src/games/tic-tac-toe.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -189,32 +189,53 @@ export class TicTacToeGame implements Game<TicTacToeGameState, TicTacToeMove> {
189189
/**
190190
* Creates the initial game state for a new tic-tac-toe game
191191
*
192-
* @param players - Array of exactly 2 players (first player gets X, second gets O)
193-
* @returns Initial TicTacToeGameState with empty board and first player's turn
192+
* @param players - Array of exactly 2 players
193+
* @param options - Optional game configuration
194+
* @returns Initial TicTacToeGameState with empty board
194195
*
195196
* @description
196197
* Sets up a new game with:
197198
* - Empty 3x3 board (all cells null)
198-
* - Player 1 assigned 'X' symbol and goes first
199-
* - Player 2 assigned 'O' symbol
199+
* - Player symbol assignment (X always goes first, O goes second)
200+
* - Configurable turn order (who gets X and goes first)
200201
* - Game status set to 'playing'
201202
* - Timestamps initialized to current time
202203
*/
203-
getInitialState(players: Player[]): TicTacToeGameState {
204+
getInitialState(players: Player[], options?: { firstPlayerId?: string }): TicTacToeGameState {
204205
const board: Board = [
205206
[null, null, null],
206207
[null, null, null],
207208
[null, null, null]
208209
];
209210

211+
// Determine who gets X (goes first) and who gets O (goes second)
212+
let firstPlayer: Player;
213+
let secondPlayer: Player;
214+
215+
if (options?.firstPlayerId) {
216+
const firstPlayerIndex = players.findIndex(p => p.id === options.firstPlayerId);
217+
if (firstPlayerIndex !== -1) {
218+
firstPlayer = players[firstPlayerIndex];
219+
secondPlayer = players[1 - firstPlayerIndex];
220+
} else {
221+
// Fallback to default order if specified player not found
222+
firstPlayer = players[0];
223+
secondPlayer = players[1];
224+
}
225+
} else {
226+
// Default: first player in array goes first
227+
firstPlayer = players[0];
228+
secondPlayer = players[1];
229+
}
230+
210231
const playerSymbols: Record<string, 'X' | 'O'> = {};
211-
playerSymbols[players[0].id] = 'X';
212-
playerSymbols[players[1].id] = 'O';
232+
playerSymbols[firstPlayer.id] = 'X';
233+
playerSymbols[secondPlayer.id] = 'O';
213234

214235
return {
215236
id: crypto.randomUUID(),
216237
players,
217-
currentPlayerId: players[0].id,
238+
currentPlayerId: firstPlayer.id,
218239
status: 'playing',
219240
createdAt: new Date(),
220241
updatedAt: new Date(),

shared/src/types/game.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export interface Game<TGameState extends BaseGameState, TMove> {
3636
applyMove(gameState: TGameState, move: TMove, playerId: PlayerId): TGameState;
3737
checkGameEnd(gameState: TGameState): GameResult | null;
3838
getValidMoves(gameState: TGameState, playerId: PlayerId): TMove[];
39-
getInitialState(players: Player[]): TGameState;
39+
getInitialState(players: Player[], options?: any): TGameState;
4040
}
4141

4242
export type GameType = 'tic-tac-toe' | 'rock-paper-scissors';

web/src/app/api/games/tic-tac-toe/route.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,25 @@ const ticTacToeGame = new TicTacToeGame()
88

99
export async function POST(request: NextRequest) {
1010
try {
11-
const { playerName, gameId, aiDifficulty } = await request.json()
11+
const { playerName, gameId, aiDifficulty, playerSymbol } = await request.json()
1212

1313
const players: Player[] = [
1414
{ id: 'player1', name: playerName || 'Player', isAI: false },
1515
{ id: 'ai', name: 'AI', isAI: true }
1616
]
1717

18-
const gameState = ticTacToeGame.getInitialState(players)
18+
// Determine who goes first based on symbol choice
19+
// X always goes first, O goes second
20+
let options: { firstPlayerId?: string } | undefined;
21+
if (playerSymbol === 'O') {
22+
// Player chose O, so AI (who gets X) goes first
23+
options = { firstPlayerId: 'ai' };
24+
} else {
25+
// Player chose X (default) or no preference, so player goes first
26+
options = { firstPlayerId: 'player1' };
27+
}
28+
29+
const gameState = ticTacToeGame.getInitialState(players, options)
1930

2031
// Use custom gameId if provided
2132
if (gameId) {

web/src/app/games/tic-tac-toe/page.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default function TicTacToePage() {
1717
const [showCreateForm, setShowCreateForm] = useState(false)
1818
const [showJoinForm, setShowJoinForm] = useState(false)
1919
const [aiDifficulty, setAiDifficulty] = useState<'easy' | 'medium' | 'hard'>('medium')
20+
const [playerSymbol, setPlayerSymbol] = useState<'X' | 'O'>('X')
2021
const [gamesToShow, setGamesToShow] = useState(5)
2122
const [showDeleteModal, setShowDeleteModal] = useState(false)
2223
const [gameToDelete, setGameToDelete] = useState<string | null>(null)
@@ -79,9 +80,10 @@ export default function TicTacToePage() {
7980
setError(null)
8081

8182
try {
82-
const body: { playerName: string; gameId?: string; aiDifficulty: string } = {
83+
const body: { playerName: string; gameId?: string; aiDifficulty: string; playerSymbol: string } = {
8384
playerName: 'Player',
84-
aiDifficulty
85+
aiDifficulty,
86+
playerSymbol
8587
}
8688
if (customGameId) {
8789
body.gameId = customGameId
@@ -433,6 +435,19 @@ export default function TicTacToePage() {
433435
<option value="hard">🔴 Hard - Optimal play (never loses)</option>
434436
</select>
435437
</div>
438+
<div>
439+
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">
440+
Your Symbol
441+
</label>
442+
<select
443+
value={playerSymbol}
444+
onChange={(e) => setPlayerSymbol(e.target.value as 'X' | 'O')}
445+
className="w-full px-4 py-3 bg-white/60 dark:bg-slate-700/60 border border-slate-200 dark:border-slate-600 rounded-xl focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all duration-200"
446+
>
447+
<option value="X">❌ X - You go first</option>
448+
<option value="O">⭕ O - AI goes first</option>
449+
</select>
450+
</div>
436451
<div>
437452
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">
438453
Custom Game ID (optional)

0 commit comments

Comments
 (0)