Add configurable bench size based on RulesConfig

Extends the RulesConfig support in the frontend game board to also
honor the bench.max_size setting:

- Add benchSize option to LayoutOptions interface
- Update calculateLandscapeLayout to use configurable bench count
- Update calculatePortraitLayout to use configurable bench count
- Update StateRenderer to pass benchSize from rules_config.bench.max_size
- Add benchSize computed property to game store
- Add 7 tests for configurable bench size behavior

The layout now generates the correct number of bench slots based on
the rules config (defaults to 5 for backwards compatibility). Bench
slots remain horizontally centered regardless of count.

https://claude.ai/code/session_01AAxKmpq2AGde327eX1nzUC
This commit is contained in:
Claude 2026-02-02 09:28:17 +00:00
parent 7885b272a4
commit c430a43e19
No known key found for this signature in database
4 changed files with 148 additions and 13 deletions

View File

@ -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)
})
})

View File

@ -71,6 +71,8 @@ export interface LayoutOptions {
usePrizeCards?: boolean usePrizeCards?: boolean
/** Number of prize cards when using classic mode. Defaults to 6. */ /** Number of prize cards when using classic mode. Defaults to 6. */
prizeCount?: number prizeCount?: number
/** Number of bench slots. Defaults to 5. */
benchSize?: number
} }
// ============================================================================= // =============================================================================
@ -162,8 +164,9 @@ function calculateLandscapeLayout(
): BoardLayout { ): BoardLayout {
const usePrizeCards = options?.usePrizeCards ?? true const usePrizeCards = options?.usePrizeCards ?? true
const prizeCount = options?.prizeCount ?? PRIZE_SIZE const prizeCount = options?.prizeCount ?? PRIZE_SIZE
const benchSize = options?.benchSize ?? BENCH_SIZE
const scale = getScaleFactor(width, height) const scale = getScaleFactor(width, height)
// Scale card dimensions // Scale card dimensions
const cardW = Math.round(CARD_WIDTH_MEDIUM * scale) const cardW = Math.round(CARD_WIDTH_MEDIUM * scale)
const cardH = Math.round(CARD_HEIGHT_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 smallCardH = Math.round(CARD_HEIGHT_SMALL * scale)
const padding = Math.round(ZONE_PADDING * scale) const padding = Math.round(ZONE_PADDING * scale)
const edgePad = Math.round(EDGE_PADDING * scale) const edgePad = Math.round(EDGE_PADDING * scale)
// Vertical layout divisions // Vertical layout divisions
const centerY = height / 2 const centerY = height / 2
const playerY = centerY + (height * 0.18) // Player active zone 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 benchOffsetY = cardH + padding // Bench below/above active (full card height + padding)
const handY = height - edgePad - cardH / 2 // Hand at bottom const handY = height - edgePad - cardH / 2 // Hand at bottom
const oppHandY = edgePad + cardH / 2 // Opponent hand at top const oppHandY = edgePad + cardH / 2 // Opponent hand at top
// Horizontal layout - center of screen // Horizontal layout - center of screen
const centerX = width / 2 const centerX = width / 2
const benchSpacing = cardW + padding const benchSpacing = cardW + padding
const totalBenchWidth = (BENCH_SIZE - 1) * benchSpacing const totalBenchWidth = (benchSize - 1) * benchSpacing
// Deck/discard positions (right side) // Deck/discard positions (right side)
const deckX = width - edgePad - cardW / 2 - padding const deckX = width - edgePad - cardW / 2 - padding
@ -208,7 +211,7 @@ function calculateLandscapeLayout(
rotation: 0, rotation: 0,
}, },
myBench: Array.from({ length: BENCH_SIZE }, (_, i) => ({ myBench: Array.from({ length: benchSize }, (_, i) => ({
x: centerX - totalBenchWidth / 2 + i * benchSpacing, x: centerX - totalBenchWidth / 2 + i * benchSpacing,
y: playerY + benchOffsetY, y: playerY + benchOffsetY,
width: cardW, width: cardW,
@ -270,7 +273,7 @@ function calculateLandscapeLayout(
rotation: Math.PI, // Upside down 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 x: centerX + totalBenchWidth / 2 - i * benchSpacing, // Reversed order
y: oppY - benchOffsetY, // Above active for opponent y: oppY - benchOffsetY, // Above active for opponent
width: cardW, width: cardW,
@ -348,8 +351,9 @@ function calculatePortraitLayout(
): BoardLayout { ): BoardLayout {
const usePrizeCards = options?.usePrizeCards ?? true const usePrizeCards = options?.usePrizeCards ?? true
const prizeCount = options?.prizeCount ?? PRIZE_SIZE const prizeCount = options?.prizeCount ?? PRIZE_SIZE
const benchSize = options?.benchSize ?? BENCH_SIZE
const scale = getScaleFactor(width, height) * 0.85 // Slightly smaller for portrait const scale = getScaleFactor(width, height) * 0.85 // Slightly smaller for portrait
// Scale card dimensions - use smaller cards in portrait // Scale card dimensions - use smaller cards in portrait
const cardW = Math.round(CARD_WIDTH_SMALL * scale * 1.2) const cardW = Math.round(CARD_WIDTH_SMALL * scale * 1.2)
const cardH = Math.round(CARD_HEIGHT_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 smallCardH = Math.round(CARD_HEIGHT_SMALL * scale * 0.9)
const padding = Math.round(ZONE_PADDING * scale * 0.8) const padding = Math.round(ZONE_PADDING * scale * 0.8)
const edgePad = Math.round(EDGE_PADDING * scale * 0.8) const edgePad = Math.round(EDGE_PADDING * scale * 0.8)
// Vertical layout divisions // Vertical layout divisions
const centerY = height / 2 const centerY = height / 2
const playerY = centerY + (height * 0.15) const playerY = centerY + (height * 0.15)
@ -365,11 +369,11 @@ function calculatePortraitLayout(
const benchOffsetY = cardH + padding // Full card height + padding const benchOffsetY = cardH + padding // Full card height + padding
const handY = height - edgePad - cardH / 2 const handY = height - edgePad - cardH / 2
const oppHandY = edgePad + cardH / 2 const oppHandY = edgePad + cardH / 2
// Horizontal layout - tighter spacing for portrait // Horizontal layout - tighter spacing for portrait
const centerX = width / 2 const centerX = width / 2
const benchSpacing = Math.min(cardW + padding, (width - edgePad * 2) / BENCH_SIZE) const benchSpacing = Math.min(cardW + padding, (width - edgePad * 2) / benchSize)
const totalBenchWidth = (BENCH_SIZE - 1) * benchSpacing const totalBenchWidth = (benchSize - 1) * benchSpacing
// Deck/discard positions (right side, more compact) // Deck/discard positions (right side, more compact)
// Use reduced size (0.8) for these zones // Use reduced size (0.8) for these zones
@ -395,7 +399,7 @@ function calculatePortraitLayout(
rotation: 0, rotation: 0,
}, },
myBench: Array.from({ length: BENCH_SIZE }, (_, i) => ({ myBench: Array.from({ length: benchSize }, (_, i) => ({
x: centerX - totalBenchWidth / 2 + i * benchSpacing, x: centerX - totalBenchWidth / 2 + i * benchSpacing,
y: playerY + benchOffsetY, y: playerY + benchOffsetY,
width: cardW * 0.9, // Slightly smaller bench cards width: cardW * 0.9, // Slightly smaller bench cards
@ -456,7 +460,7 @@ function calculatePortraitLayout(
rotation: Math.PI, rotation: Math.PI,
}, },
oppBench: Array.from({ length: BENCH_SIZE }, (_, i) => ({ oppBench: Array.from({ length: benchSize }, (_, i) => ({
x: centerX + totalBenchWidth / 2 - i * benchSpacing, x: centerX + totalBenchWidth / 2 - i * benchSpacing,
y: oppY - benchOffsetY, y: oppY - benchOffsetY,
width: cardW * 0.9, width: cardW * 0.9,

View File

@ -132,6 +132,7 @@ export class StateRenderer {
const layoutOptions: LayoutOptions = { const layoutOptions: LayoutOptions = {
usePrizeCards: state.rules_config?.prizes.use_prize_cards ?? false, usePrizeCards: state.rules_config?.prizes.use_prize_cards ?? false,
prizeCount: state.rules_config?.prizes.count ?? 4, prizeCount: state.rules_config?.prizes.count ?? 4,
benchSize: state.rules_config?.bench.max_size ?? 5,
} }
// Calculate layout // Calculate layout

View File

@ -236,6 +236,11 @@ export const useGameStore = defineStore('game', () => {
rulesConfig.value?.prizes.count ?? 4 rulesConfig.value?.prizes.count ?? 4
) )
/** Maximum number of Pokemon on the bench */
const benchSize = computed<number>(() =>
rulesConfig.value?.bench.max_size ?? 5
)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Computed - Card Lookup // Computed - Card Lookup
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -361,6 +366,7 @@ export const useGameStore = defineStore('game', () => {
rulesConfig, rulesConfig,
usePrizeCards, usePrizeCards,
prizeCount, prizeCount,
benchSize,
// Card lookup // Card lookup
lookupCard, lookupCard,