From 4b7e5ef7652c6b10df2616cf4db4811555a7d6cd Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Feb 2026 14:38:54 +0000 Subject: [PATCH 1/2] feat(mecha-duel): add pawn colors, pawn destruction win condition, and multi-piece display - Pawns now have colors based on player ownership (pawn0/pawn1) - Secondary win condition: destroying all opponent's pawns wins the game - Board display now shows all pieces in a cell (targeting piece + pawn/king) - Updated scenarios to assign pawns to players (row 2 for player 0, row 5 for player 1) https://claude.ai/code/session_01LE2nLqYkeBJNpnVq65znzC --- src/games/mecha-duel/Board.tsx | 87 ++++++++++++++++++++++--------- src/games/mecha-duel/Game.test.ts | 70 +++++++++++++++++++------ src/games/mecha-duel/Game.ts | 26 +++++++-- src/games/mecha-duel/scenarios.ts | 19 ++++--- 4 files changed, 150 insertions(+), 52 deletions(-) diff --git a/src/games/mecha-duel/Board.tsx b/src/games/mecha-duel/Board.tsx index 8f309a7..9e183b0 100644 --- a/src/games/mecha-duel/Board.tsx +++ b/src/games/mecha-duel/Board.tsx @@ -127,58 +127,93 @@ export function Board({ G, ctx, moves, playerID }: BoardProps) { (cp) => cp.target.x === pos.x && cp.target.y === pos.y ); - let cellContent = ''; - let cellColor = '#f5f5dc'; // default tan - let textColor = '#333'; + // Determine underlying content (pawn or king) + let underlyingSymbol = ''; + let underlyingColor = ''; if (content === 'king0') { - cellContent = 'K'; - cellColor = '#4a90d9'; - textColor = '#fff'; + underlyingSymbol = 'K'; + underlyingColor = '#4a90d9'; // blue } else if (content === 'king1') { - cellContent = 'K'; - cellColor = '#d94a4a'; - textColor = '#fff'; - } else if (content === 'pawn') { - cellContent = 'P'; - cellColor = '#888'; - textColor = '#fff'; + underlyingSymbol = 'K'; + underlyingColor = '#d94a4a'; // red + } else if (content === 'pawn0') { + underlyingSymbol = 'P'; + underlyingColor = '#4a90d9'; // blue (player 0) + } else if (content === 'pawn1') { + underlyingSymbol = 'P'; + underlyingColor = '#d94a4a'; // red (player 1) } - // Show committed pieces - if (committedHere.length > 0) { - const cp = committedHere[0]; - cellContent = PIECE_SYMBOLS[cp.type]; - cellColor = cp.playerId === '0' ? '#7ab8e6' : '#e67a7a'; - textColor = '#000'; - } + // Determine committed piece info + const hasCommitted = committedHere.length > 0; + const committedSymbols = committedHere.map((cp) => ({ + symbol: PIECE_SYMBOLS[cp.type], + color: cp.playerId === '0' ? '#7ab8e6' : '#e67a7a', + })); - // Highlight valid cells + // Determine cell background + let cellColor = '#f5f5dc'; // default tan if (isValid) { - cellColor = '#90EE90'; + cellColor = '#90EE90'; // green highlight for valid moves + } else if (underlyingColor && !hasCommitted) { + cellColor = underlyingColor; } const cellStyle: React.CSSProperties = { width: `${CELL_SIZE}px`, height: `${CELL_SIZE}px`, - fontSize: '24px', - fontWeight: 'bold', border: '1px solid #333', backgroundColor: cellColor, - color: textColor, cursor: isValid ? 'pointer' : 'default', display: 'flex', + flexDirection: 'column', alignItems: 'center', justifyContent: 'center', + position: 'relative', }; + // Render cell content showing all pieces + // If there are committed pieces, show them prominently + // If there's underlying content (pawn/king), show it too return (
handleCellClick(index)} > - {cellContent} + {/* Underlying content (pawn/king) */} + {underlyingSymbol && ( +
+ {underlyingSymbol} +
+ )} + {/* Committed pieces (targeting) */} + {committedSymbols.map((cp, i) => ( +
1 ? '1px' : '0', + }} + > + {cp.symbol} +
+ ))}
); }; diff --git a/src/games/mecha-duel/Game.test.ts b/src/games/mecha-duel/Game.test.ts index ed0b89f..3eab257 100644 --- a/src/games/mecha-duel/Game.test.ts +++ b/src/games/mecha-duel/Game.test.ts @@ -128,13 +128,13 @@ describe('createInitialBoard', () => { expect(cells[posToIndex({ x: 4, y: 7 })]).toBe('king1'); }); - it('places pawns at scenario positions', () => { + it('places pawns at scenario positions with correct colors', () => { const { cells } = createInitialBoard('simple'); - // Simple scenario has pawns at (2,2), (5,2), (2,5), (5,5) - expect(cells[posToIndex({ x: 2, y: 2 })]).toBe('pawn'); - expect(cells[posToIndex({ x: 5, y: 2 })]).toBe('pawn'); - expect(cells[posToIndex({ x: 2, y: 5 })]).toBe('pawn'); - expect(cells[posToIndex({ x: 5, y: 5 })]).toBe('pawn'); + // Simple scenario: player 0 pawns at row 2, player 1 pawns at row 5 + expect(cells[posToIndex({ x: 2, y: 2 })]).toBe('pawn0'); + expect(cells[posToIndex({ x: 5, y: 2 })]).toBe('pawn0'); + expect(cells[posToIndex({ x: 2, y: 5 })]).toBe('pawn1'); + expect(cells[posToIndex({ x: 5, y: 5 })]).toBe('pawn1'); }); }); @@ -298,8 +298,8 @@ describe('raycast', () => { it('finds first pawn in path', () => { const cells = createEmptyBoard(); - cells[posToIndex({ x: 4, y: 3 })] = 'pawn'; - cells[posToIndex({ x: 4, y: 5 })] = 'pawn'; + cells[posToIndex({ x: 4, y: 3 })] = 'pawn0'; + cells[posToIndex({ x: 4, y: 5 })] = 'pawn1'; const from = { x: 4, y: 0 }; const toward = { x: 4, y: 7 }; const result = Game.raycast(from, toward, cells); @@ -317,7 +317,7 @@ describe('raycast', () => { it('works for diagonal directions', () => { const cells = createEmptyBoard(); - cells[posToIndex({ x: 6, y: 6 })] = 'pawn'; + cells[posToIndex({ x: 6, y: 6 })] = 'pawn1'; const from = { x: 4, y: 4 }; const toward = { x: 7, y: 7 }; const result = Game.raycast(from, toward, cells); @@ -351,14 +351,14 @@ describe('resolveStrike', () => { it('destroys first pawn in strike path', () => { const state = createTestState(); const pawnPos = { x: 4, y: 3 }; - state.cells[posToIndex(pawnPos)] = 'pawn'; + state.cells[posToIndex(pawnPos)] = 'pawn1'; // enemy pawn const kingPos = state.kings['0']!; const targetPos = { x: 4, y: 7 }; const result = Game.resolveStrike(state, kingPos, targetPos); expect(result.hit).toEqual(pawnPos); - expect(result.destroyed).toBe('pawn'); + expect(result.destroyed).toBe('pawn1'); }); it('destroys opponent king in strike path', () => { @@ -440,12 +440,12 @@ describe('resolveKnight', () => { it('destroys pawn at landing square', () => { const state = createTestState(); const targetPos = { x: 5, y: 2 }; - state.cells[posToIndex(targetPos)] = 'pawn'; + state.cells[posToIndex(targetPos)] = 'pawn1'; // enemy pawn const result = Game.resolveKnight(state, '0', targetPos); expect(result.success).toBe(true); - expect(result.destroyed).toBe('pawn'); + expect(result.destroyed).toBe('pawn1'); }); it('destroys opponent king at landing square', () => { @@ -522,12 +522,12 @@ describe('resolveKingStep', () => { it('destroys pawn when entering pawn square', () => { const state = createTestState(); const targetPos = { x: 4, y: 1 }; - state.cells[posToIndex(targetPos)] = 'pawn'; + state.cells[posToIndex(targetPos)] = 'pawn1'; // enemy pawn const result = Game.resolveKingStep(state, '0', targetPos); expect(result.success).toBe(true); - expect(result.destroyed).toBe('pawn'); + expect(result.destroyed).toBe('pawn1'); }); it('destroys opponent king when stepping on', () => { @@ -630,4 +630,44 @@ describe('checkWinCondition', () => { state.kings['1'] = null; expect(Game.checkWinCondition(state)).toEqual({ winner: '0' }); }); + + it('returns player 1 as winner when all player 0 pawns destroyed', () => { + const state = createTestState(); + // Remove all player 0 pawns + for (let i = 0; i < state.cells.length; i++) { + if (state.cells[i] === 'pawn0') { + state.cells[i] = null; + } + } + expect(Game.checkWinCondition(state)).toEqual({ winner: '1' }); + }); + + it('returns player 0 as winner when all player 1 pawns destroyed', () => { + const state = createTestState(); + // Remove all player 1 pawns + for (let i = 0; i < state.cells.length; i++) { + if (state.cells[i] === 'pawn1') { + state.cells[i] = null; + } + } + expect(Game.checkWinCondition(state)).toEqual({ winner: '0' }); + }); +}); + +describe('countPawns', () => { + it('counts player 0 pawns correctly', () => { + const { cells } = createInitialBoard('simple'); + expect(Game.countPawns(cells, '0')).toBe(2); + }); + + it('counts player 1 pawns correctly', () => { + const { cells } = createInitialBoard('simple'); + expect(Game.countPawns(cells, '1')).toBe(2); + }); + + it('returns 0 when no pawns exist for player', () => { + const cells: CellContent[] = Array(TOTAL_CELLS).fill(null); + expect(Game.countPawns(cells, '0')).toBe(0); + expect(Game.countPawns(cells, '1')).toBe(0); + }); }); diff --git a/src/games/mecha-duel/Game.ts b/src/games/mecha-duel/Game.ts index d0bf343..3384f4c 100644 --- a/src/games/mecha-duel/Game.ts +++ b/src/games/mecha-duel/Game.ts @@ -27,7 +27,7 @@ export interface CommittedPiece { target: Position; } -export type CellContent = 'pawn' | 'king0' | 'king1' | null; +export type CellContent = 'pawn0' | 'pawn1' | 'king0' | 'king1' | null; export interface MechaDuelState { cells: CellContent[]; @@ -135,9 +135,9 @@ export function createInitialBoard(scenarioId: string = defaultScenarioId): { const scenario = getScenario(scenarioId); const cells: CellContent[] = Array(TOTAL_CELLS).fill(null); - // Place pawns - for (const pawnPos of scenario.pawns) { - cells[posToIndex(pawnPos)] = 'pawn'; + // Place pawns with player ownership + for (const pawn of scenario.pawns) { + cells[posToIndex(pawn.position)] = `pawn${pawn.player}` as CellContent; } // Place kings on the cells @@ -379,15 +379,33 @@ function exhaustCommittedPieces(pieces: Piece[]): Piece[] { // Win Conditions // ============================================================================= +export function countPawns(cells: CellContent[], playerId: '0' | '1'): number { + const pawnType = `pawn${playerId}` as CellContent; + return cells.filter((cell) => cell === pawnType).length; +} + export function checkWinCondition( state: MechaDuelState ): { winner: string } | null { + // Primary win condition: opponent's king destroyed if (state.kings['0'] === null) { return { winner: '1' }; } if (state.kings['1'] === null) { return { winner: '0' }; } + + // Secondary win condition: all opponent's pawns destroyed + const player0Pawns = countPawns(state.cells, '0'); + const player1Pawns = countPawns(state.cells, '1'); + + if (player0Pawns === 0) { + return { winner: '1' }; + } + if (player1Pawns === 0) { + return { winner: '0' }; + } + return null; } diff --git a/src/games/mecha-duel/scenarios.ts b/src/games/mecha-duel/scenarios.ts index bcdbe15..9996313 100644 --- a/src/games/mecha-duel/scenarios.ts +++ b/src/games/mecha-duel/scenarios.ts @@ -1,9 +1,14 @@ import type { Position } from './Game'; +export interface PawnPlacement { + position: Position; + player: '0' | '1'; +} + export interface Scenario { name: string; description: string; - pawns: Position[]; + pawns: PawnPlacement[]; kingPositions: { '0': Position; '1': Position; @@ -17,10 +22,10 @@ export interface Scenario { * Layout (8x8): * Row 7: [ ][ ][ ][ ][K1][ ][ ][ ] * Row 6: [ ][ ][ ][ ][ ][ ][ ][ ] - * Row 5: [ ][ ][P][ ][ ][P][ ][ ] + * Row 5: [ ][ ][p1][ ][ ][p1][ ][ ] (Player 1's pawns) * Row 4: [ ][ ][ ][ ][ ][ ][ ][ ] * Row 3: [ ][ ][ ][ ][ ][ ][ ][ ] - * Row 2: [ ][ ][P][ ][ ][P][ ][ ] + * Row 2: [ ][ ][p0][ ][ ][p0][ ][ ] (Player 0's pawns) * Row 1: [ ][ ][ ][ ][ ][ ][ ][ ] * Row 0: [ ][ ][ ][ ][K0][ ][ ][ ] * 0 1 2 3 4 5 6 7 @@ -29,10 +34,10 @@ export const simpleScenario: Scenario = { name: 'Simple', description: 'A basic symmetric layout with minimal pawns', pawns: [ - { x: 2, y: 2 }, - { x: 5, y: 2 }, - { x: 2, y: 5 }, - { x: 5, y: 5 }, + { position: { x: 2, y: 2 }, player: '0' }, + { position: { x: 5, y: 2 }, player: '0' }, + { position: { x: 2, y: 5 }, player: '1' }, + { position: { x: 5, y: 5 }, player: '1' }, ], kingPositions: { '0': { x: 4, y: 0 }, From 36a042936d3d7a1a4b4a1fe81de04d79856a9001 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Feb 2026 17:24:54 +0000 Subject: [PATCH 2/2] docs(mecha-duel): update rules for pawn ownership and secondary win condition - Pawns now belong to players, not neutral obstacles - Added secondary win condition: destroying all opponent's pawns https://claude.ai/code/session_01LE2nLqYkeBJNpnVq65znzC --- src/games/mecha-duel/rules.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/games/mecha-duel/rules.md b/src/games/mecha-duel/rules.md index 9e3143d..2131138 100644 --- a/src/games/mecha-duel/rules.md +++ b/src/games/mecha-duel/rules.md @@ -3,7 +3,7 @@ ## Setup - 8×8 board - Each player starts with King on opposite ends (row 0 and row 7) -- Scenario determines pawn placement (static obstacles) +- Each player has pawns on their side of the board (scenario determines placement) - Each player has piece supply: 2 Bishops, 2 Rooks, 2 Knights, 1 Queen (all start Ready) ## Piece States @@ -11,8 +11,9 @@ - **Committed:** Placed on board as threat - **Exhausted:** Recently used, unavailable -## Win Condition -- Opponent's King is destroyed when struck by executed attack or stepped on +## Win Conditions +- **Primary:** Opponent's King is destroyed (struck by executed attack or stepped on) +- **Secondary:** All of opponent's pawns are destroyed - Opponent resigns ## Turn Structure @@ -64,6 +65,7 @@ On your turn, choose ONE action: - Strike attacks (first piece in the ray path) - Knight landing on a pawn square - KingStep entering a pawn square + - Destroying all of an opponent's pawns wins the game 5. **Execution Order:** When executing, all Strikes resolve first (simultaneously), then Knights resolve in the order chosen by the player.