diff --git a/frontend/src/game/layout.spec.ts b/frontend/src/game/layout.spec.ts index d8ab428..1338c34 100644 --- a/frontend/src/game/layout.spec.ts +++ b/frontend/src/game/layout.spec.ts @@ -698,3 +698,127 @@ describe('calculateLayout - prize zone options', () => { } }) }) + +// ============================================================================= +// Conditional Bench Size Tests (RulesConfig support) +// ============================================================================= + +describe('calculateLayout - bench size options', () => { + it('returns default 5 bench slots when no options provided', () => { + /** + * Test backwards compatibility with existing code. + * + * When called without options, the layout should generate 5 bench slots + * for each player (standard Pokemon TCG bench size). + */ + const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT) + + expect(layout.myBench).toHaveLength(BENCH_SIZE) + expect(layout.oppBench).toHaveLength(BENCH_SIZE) + }) + + it('returns correct bench count with custom benchSize option', () => { + /** + * Test that bench zones can have a custom count. + * + * Some rule variants may use fewer bench slots (e.g., 3 for a faster game). + * The layout should generate exactly the requested number of slots. + */ + const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, { + benchSize: 3, + }) + + expect(layout.myBench).toHaveLength(3) + expect(layout.oppBench).toHaveLength(3) + }) + + it('works with larger bench sizes', () => { + /** + * Test that larger bench sizes are supported. + * + * Some custom variants might use more than 5 bench slots. + */ + const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, { + benchSize: 8, + }) + + expect(layout.myBench).toHaveLength(8) + expect(layout.oppBench).toHaveLength(8) + }) + + it('works correctly in portrait mode with custom bench size', () => { + /** + * Test that portrait mode also respects bench size settings. + * + * The portrait layout should also generate the correct number + * of bench positions based on the benchSize option. + */ + const layout = calculateLayout(PORTRAIT_WIDTH, PORTRAIT_HEIGHT, { + benchSize: 3, + }) + + expect(layout.myBench).toHaveLength(3) + expect(layout.oppBench).toHaveLength(3) + }) + + it('bench positions are valid when using custom size', () => { + /** + * Test that generated bench positions are all valid. + * + * Each bench slot should have positive dimensions and valid coordinates. + */ + const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, { + benchSize: 4, + }) + + for (const bench of layout.myBench) { + expect(isValidZone(bench), 'Player bench zone should be valid').toBe(true) + } + for (const bench of layout.oppBench) { + expect(isValidZone(bench), 'Opponent bench zone should be valid').toBe(true) + } + }) + + it('bench slots are centered properly with different sizes', () => { + /** + * Test that bench zones remain centered regardless of count. + * + * Whether using 3, 5, or 8 bench slots, they should all be + * horizontally centered on the screen. + */ + const layout3 = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, { benchSize: 3 }) + const layout5 = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, { benchSize: 5 }) + const layout7 = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, { benchSize: 7 }) + + const centerX = LANDSCAPE_WIDTH / 2 + + // Calculate average X position of each player's bench + const avg3 = layout3.myBench.reduce((sum, b) => sum + b.x, 0) / 3 + const avg5 = layout5.myBench.reduce((sum, b) => sum + b.x, 0) / 5 + const avg7 = layout7.myBench.reduce((sum, b) => sum + b.x, 0) / 7 + + // All should be centered + expect(avg3).toBeCloseTo(centerX, 0) + expect(avg5).toBeCloseTo(centerX, 0) + expect(avg7).toBeCloseTo(centerX, 0) + }) + + it('can combine benchSize with prize options', () => { + /** + * Test that bench and prize options work together. + * + * Custom rules might have both custom bench size and prize count, + * and both should be applied correctly. + */ + const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, { + benchSize: 3, + usePrizeCards: true, + prizeCount: 4, + }) + + expect(layout.myBench).toHaveLength(3) + expect(layout.oppBench).toHaveLength(3) + expect(layout.myPrizes).toHaveLength(4) + expect(layout.oppPrizes).toHaveLength(4) + }) +}) diff --git a/frontend/src/game/layout.ts b/frontend/src/game/layout.ts index 918857a..82905c3 100644 --- a/frontend/src/game/layout.ts +++ b/frontend/src/game/layout.ts @@ -71,6 +71,8 @@ export interface LayoutOptions { usePrizeCards?: boolean /** Number of prize cards when using classic mode. Defaults to 6. */ prizeCount?: number + /** Number of bench slots. Defaults to 5. */ + benchSize?: number } // ============================================================================= @@ -162,8 +164,9 @@ function calculateLandscapeLayout( ): BoardLayout { const usePrizeCards = options?.usePrizeCards ?? true const prizeCount = options?.prizeCount ?? PRIZE_SIZE + const benchSize = options?.benchSize ?? BENCH_SIZE const scale = getScaleFactor(width, height) - + // Scale card dimensions const cardW = Math.round(CARD_WIDTH_MEDIUM * scale) const cardH = Math.round(CARD_HEIGHT_MEDIUM * scale) @@ -171,7 +174,7 @@ function calculateLandscapeLayout( const smallCardH = Math.round(CARD_HEIGHT_SMALL * scale) const padding = Math.round(ZONE_PADDING * scale) const edgePad = Math.round(EDGE_PADDING * scale) - + // Vertical layout divisions const centerY = height / 2 const playerY = centerY + (height * 0.18) // Player active zone @@ -179,11 +182,11 @@ function calculateLandscapeLayout( const benchOffsetY = cardH + padding // Bench below/above active (full card height + padding) const handY = height - edgePad - cardH / 2 // Hand at bottom const oppHandY = edgePad + cardH / 2 // Opponent hand at top - + // Horizontal layout - center of screen const centerX = width / 2 const benchSpacing = cardW + padding - const totalBenchWidth = (BENCH_SIZE - 1) * benchSpacing + const totalBenchWidth = (benchSize - 1) * benchSpacing // Deck/discard positions (right side) const deckX = width - edgePad - cardW / 2 - padding @@ -208,7 +211,7 @@ function calculateLandscapeLayout( rotation: 0, }, - myBench: Array.from({ length: BENCH_SIZE }, (_, i) => ({ + myBench: Array.from({ length: benchSize }, (_, i) => ({ x: centerX - totalBenchWidth / 2 + i * benchSpacing, y: playerY + benchOffsetY, width: cardW, @@ -270,7 +273,7 @@ function calculateLandscapeLayout( rotation: Math.PI, // Upside down }, - oppBench: Array.from({ length: BENCH_SIZE }, (_, i) => ({ + oppBench: Array.from({ length: benchSize }, (_, i) => ({ x: centerX + totalBenchWidth / 2 - i * benchSpacing, // Reversed order y: oppY - benchOffsetY, // Above active for opponent width: cardW, @@ -348,8 +351,9 @@ function calculatePortraitLayout( ): BoardLayout { const usePrizeCards = options?.usePrizeCards ?? true const prizeCount = options?.prizeCount ?? PRIZE_SIZE + const benchSize = options?.benchSize ?? BENCH_SIZE const scale = getScaleFactor(width, height) * 0.85 // Slightly smaller for portrait - + // Scale card dimensions - use smaller cards in portrait const cardW = Math.round(CARD_WIDTH_SMALL * scale * 1.2) const cardH = Math.round(CARD_HEIGHT_SMALL * scale * 1.2) @@ -357,7 +361,7 @@ function calculatePortraitLayout( const smallCardH = Math.round(CARD_HEIGHT_SMALL * scale * 0.9) const padding = Math.round(ZONE_PADDING * scale * 0.8) const edgePad = Math.round(EDGE_PADDING * scale * 0.8) - + // Vertical layout divisions const centerY = height / 2 const playerY = centerY + (height * 0.15) @@ -365,11 +369,11 @@ function calculatePortraitLayout( const benchOffsetY = cardH + padding // Full card height + padding const handY = height - edgePad - cardH / 2 const oppHandY = edgePad + cardH / 2 - + // Horizontal layout - tighter spacing for portrait const centerX = width / 2 - const benchSpacing = Math.min(cardW + padding, (width - edgePad * 2) / BENCH_SIZE) - const totalBenchWidth = (BENCH_SIZE - 1) * benchSpacing + const benchSpacing = Math.min(cardW + padding, (width - edgePad * 2) / benchSize) + const totalBenchWidth = (benchSize - 1) * benchSpacing // Deck/discard positions (right side, more compact) // Use reduced size (0.8) for these zones @@ -395,7 +399,7 @@ function calculatePortraitLayout( rotation: 0, }, - myBench: Array.from({ length: BENCH_SIZE }, (_, i) => ({ + myBench: Array.from({ length: benchSize }, (_, i) => ({ x: centerX - totalBenchWidth / 2 + i * benchSpacing, y: playerY + benchOffsetY, width: cardW * 0.9, // Slightly smaller bench cards @@ -456,7 +460,7 @@ function calculatePortraitLayout( rotation: Math.PI, }, - oppBench: Array.from({ length: BENCH_SIZE }, (_, i) => ({ + oppBench: Array.from({ length: benchSize }, (_, i) => ({ x: centerX + totalBenchWidth / 2 - i * benchSpacing, y: oppY - benchOffsetY, width: cardW * 0.9, diff --git a/frontend/src/game/sync/StateRenderer.ts b/frontend/src/game/sync/StateRenderer.ts index 302cb53..61d1221 100644 --- a/frontend/src/game/sync/StateRenderer.ts +++ b/frontend/src/game/sync/StateRenderer.ts @@ -132,6 +132,7 @@ export class StateRenderer { const layoutOptions: LayoutOptions = { usePrizeCards: state.rules_config?.prizes.use_prize_cards ?? false, prizeCount: state.rules_config?.prizes.count ?? 4, + benchSize: state.rules_config?.bench.max_size ?? 5, } // Calculate layout diff --git a/frontend/src/stores/game.ts b/frontend/src/stores/game.ts index d7877fd..6c4afdc 100644 --- a/frontend/src/stores/game.ts +++ b/frontend/src/stores/game.ts @@ -236,6 +236,11 @@ export const useGameStore = defineStore('game', () => { rulesConfig.value?.prizes.count ?? 4 ) + /** Maximum number of Pokemon on the bench */ + const benchSize = computed(() => + rulesConfig.value?.bench.max_size ?? 5 + ) + // --------------------------------------------------------------------------- // Computed - Card Lookup // --------------------------------------------------------------------------- @@ -361,6 +366,7 @@ export const useGameStore = defineStore('game', () => { rulesConfig, usePrizeCards, prizeCount, + benchSize, // Card lookup lookupCard,