Skip to content

Add experimental 'Play vs Gemini' game mode - #58

Open
mterczynski wants to merge 2 commits into
mainfrom
feature/vs-gemini-mode
Open

Add experimental 'Play vs Gemini' game mode#58
mterczynski wants to merge 2 commits into
mainfrom
feature/vs-gemini-mode

Conversation

@mterczynski

Copy link
Copy Markdown
Owner

Closes #57

What

Adds an experimental ✨ Play vs Gemini game mode where the player plays against the Gemini API.

How it works

  • New GameMode.VS_GEMINI = "vs_gemini" and a new ModeButton on the ModeSelectionScreen with the ✨ sparkles emoji and a black background with a navy-blue radial gradient spreading from the middle of the button.
  • Selecting the mode shows a Gemini settings panel with:
    • a Gemini API Key input (placeholder Insert your own Gemini API Key). The key is kept in memory only and read on every request (committed on blur/Enter so partial keys aren't used mid-typing).
    • Difficulty slider (FIDE rating 1400–2800, default 1600). The system prompt instructs Gemini to play at the strength/style of a player with that FIDE rating.
    • Model selector (gemini-2.5-flash-lite / gemini-2.5-flash / gemini-2.5-pro, default gemini-2.5-flash).
  • On every Gemini turn, the headless GeminiOpponent component sends the current board state, the player's last move and the list of legal moves to models/{model}:generateContent (no conversation history). Input is the game-engine's Board/Move JSON; output uses Gemini structured output (responseMimeType: application/json + a responseSchema matching the game-engine Move interface). The returned move is validated against the legal-move list and applied.
  • If the user makes a move while the API key input is empty, a window.alert explains that the key is required and the move is undone (new GameEngineContext.undoLastMove, which replays the move history minus the last move). The same undo happens if the API call fails (e.g. invalid key), so the player can simply retry.
  • Invalid/illegal moves returned by Gemini are retried up to 3 times before giving up; API errors fail fast.

Separation of concerns / tests

All Gemini code lives in client/src/gemini/:

File Responsibility
buildGeminiRequest.ts system prompt, user payload, response schema, request body (pure)
parseGeminiResponse.ts parse + validate Gemini's JSON into a legal engine Move (pure)
requestGeminiMove.ts fetch wrapper with retry policy and error types
GeminiSettingsContext.tsx / GeminiSettingsPanel.tsx user settings state + UI
GeminiOpponent.tsx React glue: triggers a request on Gemini's turn, applies the move, handles stale responses/undo

17 new unit tests cover prompt building, response parsing (incl. promotions defaulting to queen) and the API client (mocked fetch, retry and error paths). They run as part of the root jest suite (jest.config.js now ignores game-engine/build/ so the workspace package resolves without a haste collision).

No .env needed — the API key is provided by the user at runtime and never persisted.

Verification

  • npx jest --ci — 42 suites / 167 tests pass
  • npm run build -w client passes
  • Manually smoke-tested in the browser: mode button + settings panel render, missing-key alert undoes the player's move, and with fetch stubbed to a canned Gemini response the full move loop works (player plays e4, "Gemini" answers e5 and it's applied to the board).

🤖 Generated with Claude Code

- New GameMode.VS_GEMINI with a sparkles ModeButton (navy radial gradient)
- Gemini settings panel: user-provided API key input, FIDE rating slider
  (1400-2800, default 1600) and model selector (flash-lite/flash/pro)
- GeminiOpponent reads settings on each turn, sends board state, last
  player move and legal moves to the Gemini API as structured JSON and
  applies the returned move (structured output matching game-engine Move)
- Missing API key or a failed request alerts the user and undoes their
  move (new GameEngineContext.undoLastMove)
- Illegal/invalid Gemini moves are retried up to 3 times
- Unit tests for prompt building, response parsing and the API client

Closes #57

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mterczynski

Copy link
Copy Markdown
Owner Author

Verified against the live Gemini API with a real key (gemini-2.5-flash, FIDE 1600): played 1. e4 and 2. Ne2 in the browser - Gemini answered 1... e5 and 2... Nf6, both applied correctly to the board. No errors in the console and no invalid-move retries needed.

Comment thread client/src/App.tsx
<SettingsIcon>⚙️</SettingsIcon>
<GameScreenSelector />
</>
<GeminiSettingsContextProvider>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not need to provide it for every mode, maybe there is a smarter way to declare it

* the current settings, sends the board state and the player's last move to
* the Gemini API and applies the returned move.
*/
export const GeminiOpponent = () => {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a fan of combining UI with behavior. I would highly appreciate having a framework/ui-lib agnostic GeminiOpponent TS class and connecting it to UI via adapter design pattern, React Context or similar approach

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Experiment: Add a play vs Gemini button

1 participant