Honor RulesConfig for prize cards vs points in frontend game board

The game board now conditionally renders prize card zones based on
the RulesConfig sent from the backend:

- Add rules_config field to VisibleGameState in backend (visibility.py)
- Add rules_config to frontend game types and game store
- Update layout.ts to accept LayoutOptions with usePrizeCards and prizeCount
- Update StateRenderer to conditionally create PrizeZone objects
- Update Board to handle empty prize position arrays gracefully
- Add game store computed properties: rulesConfig, usePrizeCards, prizeCount
- Add tests for conditional prize zone rendering

When use_prize_cards is false (Mantimon TCG points system), the prize
zones are not rendered, saving screen space. When true (classic Pokemon
TCG mode), the correct number of prize slots is rendered based on
the rules config's prize count.

https://claude.ai/code/session_01AAxKmpq2AGde327eX1nzUC
This commit is contained in:
Claude 2026-02-02 09:22:44 +00:00
parent 1a21d3d2d4
commit 7885b272a4
No known key found for this signature in database
6 changed files with 294 additions and 70 deletions

View File

@ -37,6 +37,7 @@ from typing import TYPE_CHECKING
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.core.config import RulesConfig
from app.core.enums import GameEndReason, TurnPhase from app.core.enums import GameEndReason, TurnPhase
from app.core.models.card import CardDefinition, CardInstance from app.core.models.card import CardDefinition, CardInstance
@ -124,6 +125,7 @@ class VisibleGameState(BaseModel):
stadium_owner_id: Player who played the current stadium (public). stadium_owner_id: Player who played the current stadium (public).
forced_action: Current forced action, if any. forced_action: Current forced action, if any.
card_registry: Card definitions (needed to display cards). card_registry: Card definitions (needed to display cards).
rules_config: Rules configuration for UI rendering decisions (e.g., prize cards vs points).
""" """
game_id: str game_id: str
@ -154,6 +156,9 @@ class VisibleGameState(BaseModel):
# Card definitions for display # Card definitions for display
card_registry: dict[str, CardDefinition] = Field(default_factory=dict) card_registry: dict[str, CardDefinition] = Field(default_factory=dict)
# Rules configuration for UI rendering decisions
rules_config: RulesConfig = Field(default_factory=RulesConfig)
def _create_visible_zone( def _create_visible_zone(
cards: list[CardInstance], cards: list[CardInstance],
@ -298,6 +303,7 @@ def get_visible_state(game: GameState, viewer_id: str) -> VisibleGameState:
forced_action_type=forced_action_type, forced_action_type=forced_action_type,
forced_action_reason=forced_action_reason, forced_action_reason=forced_action_reason,
card_registry=game.card_registry, card_registry=game.card_registry,
rules_config=game.rules,
) )
@ -393,4 +399,5 @@ def get_spectator_state(game: GameState) -> VisibleGameState:
forced_action_type=forced_action_type, forced_action_type=forced_action_type,
forced_action_reason=forced_action_reason, forced_action_reason=forced_action_reason,
card_registry=game.card_registry, card_registry=game.card_registry,
rules_config=game.rules,
) )

View File

@ -582,3 +582,119 @@ describe('getAllZones', () => {
expect(allZones.find(z => z.label === 'oppBench[0]')).toBeDefined() expect(allZones.find(z => z.label === 'oppBench[0]')).toBeDefined()
}) })
}) })
// =============================================================================
// Conditional Prize Zone Tests (RulesConfig support)
// =============================================================================
describe('calculateLayout - prize zone options', () => {
it('returns empty prize arrays when usePrizeCards is false', () => {
/**
* Test that prize zones are not generated in points-based mode.
*
* When using the Mantimon TCG points system instead of classic prize cards,
* the prize zones should be omitted from the layout to save screen space.
*/
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
usePrizeCards: false,
})
expect(layout.myPrizes).toHaveLength(0)
expect(layout.oppPrizes).toHaveLength(0)
})
it('returns correct prize count when usePrizeCards is true with custom count', () => {
/**
* Test that prize zones can have a custom count.
*
* Different rule variants may use 4, 6, or other prize counts.
* The layout should generate exactly the requested number of slots.
*/
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
usePrizeCards: true,
prizeCount: 4,
})
expect(layout.myPrizes).toHaveLength(4)
expect(layout.oppPrizes).toHaveLength(4)
})
it('defaults to 6 prizes when usePrizeCards is true without count', () => {
/**
* Test that classic mode defaults to 6 prize cards.
*
* When using prize cards without specifying a count, the standard
* Pokemon TCG count of 6 should be used.
*/
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
usePrizeCards: true,
})
expect(layout.myPrizes).toHaveLength(PRIZE_SIZE)
expect(layout.oppPrizes).toHaveLength(PRIZE_SIZE)
})
it('returns 6 prizes when no options provided (backwards compatibility)', () => {
/**
* Test backwards compatibility with existing code.
*
* When called without options, the layout should behave as before,
* generating 6 prize slots for each player.
*/
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
// Default behavior assumes classic prize card mode with 6 prizes
expect(layout.myPrizes).toHaveLength(PRIZE_SIZE)
expect(layout.oppPrizes).toHaveLength(PRIZE_SIZE)
})
it('works correctly in portrait mode with usePrizeCards false', () => {
/**
* Test that portrait mode also respects prize card settings.
*
* The portrait layout should also omit prize zones when using
* the points system.
*/
const layout = calculateLayout(PORTRAIT_WIDTH, PORTRAIT_HEIGHT, {
usePrizeCards: false,
})
expect(layout.myPrizes).toHaveLength(0)
expect(layout.oppPrizes).toHaveLength(0)
})
it('works correctly in portrait mode with custom prize count', () => {
/**
* Test that portrait mode generates correct prize count.
*
* The portrait layout should create the correct number of
* prize positions when using prize cards with a custom count.
*/
const layout = calculateLayout(PORTRAIT_WIDTH, PORTRAIT_HEIGHT, {
usePrizeCards: true,
prizeCount: 4,
})
expect(layout.myPrizes).toHaveLength(4)
expect(layout.oppPrizes).toHaveLength(4)
})
it('prize positions are valid when generated', () => {
/**
* Test that generated prize positions are all valid.
*
* Each prize slot should have positive dimensions and valid coordinates.
*/
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT, {
usePrizeCards: true,
prizeCount: 4,
})
for (const prize of layout.myPrizes) {
expect(isValidZone(prize), 'Player prize zone should be valid').toBe(true)
}
for (const prize of layout.oppPrizes) {
expect(isValidZone(prize), 'Opponent prize zone should be valid').toBe(true)
}
})
})

View File

@ -53,12 +53,26 @@ export const EDGE_PADDING = 16
/** Number of bench slots */ /** Number of bench slots */
export const BENCH_SIZE = 5 export const BENCH_SIZE = 5
/** Number of prize slots */ /** Default number of prize slots (for classic Pokemon TCG mode) */
export const PRIZE_SIZE = 6 export const PRIZE_SIZE = 6
/** Portrait mode threshold - below this aspect ratio we use portrait layout */ /** Portrait mode threshold - below this aspect ratio we use portrait layout */
export const PORTRAIT_THRESHOLD = 0.9 export const PORTRAIT_THRESHOLD = 0.9
// =============================================================================
// Layout Options
// =============================================================================
/**
* Options for customizing board layout based on rules config.
*/
export interface LayoutOptions {
/** Whether to use prize cards (classic mode). If false, no prize zones are rendered. */
usePrizeCards?: boolean
/** Number of prize cards when using classic mode. Defaults to 6. */
prizeCount?: number
}
// ============================================================================= // =============================================================================
// Orientation Detection // Orientation Detection
// ============================================================================= // =============================================================================
@ -103,19 +117,28 @@ export function getScaleFactor(width: number, height: number): number {
* *
* @param width - Canvas width in pixels * @param width - Canvas width in pixels
* @param height - Canvas height in pixels * @param height - Canvas height in pixels
* @param options - Optional layout customization based on rules config
* @returns Complete board layout with all zone positions * @returns Complete board layout with all zone positions
* *
* @example * @example
* ```ts * ```ts
* const layout = calculateLayout(1920, 1080) * const layout = calculateLayout(1920, 1080)
* console.log(layout.myActive) // { x: 960, y: 700, width: 120, height: 168 } * console.log(layout.myActive) // { x: 960, y: 700, width: 120, height: 168 }
*
* // For points-based mode (no prize cards)
* const layoutNoCards = calculateLayout(1920, 1080, { usePrizeCards: false })
* console.log(layoutNoCards.myPrizes) // [] (empty array)
* ``` * ```
*/ */
export function calculateLayout(width: number, height: number): BoardLayout { export function calculateLayout(
width: number,
height: number,
options?: LayoutOptions
): BoardLayout {
if (isPortrait(width, height)) { if (isPortrait(width, height)) {
return calculatePortraitLayout(width, height) return calculatePortraitLayout(width, height, options)
} }
return calculateLandscapeLayout(width, height) return calculateLandscapeLayout(width, height, options)
} }
/** /**
@ -129,9 +152,16 @@ export function calculateLayout(width: number, height: number): BoardLayout {
* *
* @param width - Canvas width * @param width - Canvas width
* @param height - Canvas height * @param height - Canvas height
* @param options - Optional layout customization
* @returns Board layout for landscape orientation * @returns Board layout for landscape orientation
*/ */
function calculateLandscapeLayout(width: number, height: number): BoardLayout { function calculateLandscapeLayout(
width: number,
height: number,
options?: LayoutOptions
): BoardLayout {
const usePrizeCards = options?.usePrizeCards ?? true
const prizeCount = options?.prizeCount ?? PRIZE_SIZE
const scale = getScaleFactor(width, height) const scale = getScaleFactor(width, height)
// Scale card dimensions // Scale card dimensions
@ -210,15 +240,18 @@ function calculateLandscapeLayout(width: number, height: number): BoardLayout {
rotation: 0, rotation: 0,
}, },
myPrizes: calculatePrizePositions( myPrizes: usePrizeCards
prizesStartX, ? calculatePrizePositions(
playerY - cardH / 2, prizesStartX,
smallCardW, playerY - cardH / 2,
smallCardH, smallCardW,
prizesColSpacing, smallCardH,
prizesRowSpacing, prizesColSpacing,
false // Not flipped for player prizesRowSpacing,
), false, // Not flipped for player
prizeCount
)
: [],
myEnergyZone: { myEnergyZone: {
x: centerX + energyOffsetX, x: centerX + energyOffsetX,
@ -269,15 +302,18 @@ function calculateLandscapeLayout(width: number, height: number): BoardLayout {
rotation: Math.PI, rotation: Math.PI,
}, },
oppPrizes: calculatePrizePositions( oppPrizes: usePrizeCards
width - prizesStartX - prizesColSpacing, // Right side for opponent ? calculatePrizePositions(
oppY + cardH / 2, width - prizesStartX - prizesColSpacing, // Right side for opponent
smallCardW, oppY + cardH / 2,
smallCardH, smallCardW,
-prizesColSpacing, // Reversed column spacing smallCardH,
-prizesRowSpacing, // Reversed row spacing -prizesColSpacing, // Reversed column spacing
true // Flipped for opponent -prizesRowSpacing, // Reversed row spacing
), true, // Flipped for opponent
prizeCount
)
: [],
oppEnergyZone: { oppEnergyZone: {
x: centerX - energyOffsetX, x: centerX - energyOffsetX,
@ -302,9 +338,16 @@ function calculateLandscapeLayout(width: number, height: number): BoardLayout {
* *
* @param width - Canvas width * @param width - Canvas width
* @param height - Canvas height * @param height - Canvas height
* @param options - Optional layout customization
* @returns Board layout for portrait orientation * @returns Board layout for portrait orientation
*/ */
function calculatePortraitLayout(width: number, height: number): BoardLayout { function calculatePortraitLayout(
width: number,
height: number,
options?: LayoutOptions
): BoardLayout {
const usePrizeCards = options?.usePrizeCards ?? true
const prizeCount = options?.prizeCount ?? PRIZE_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
@ -384,14 +427,17 @@ function calculatePortraitLayout(width: number, height: number): BoardLayout {
rotation: 0, rotation: 0,
}, },
myPrizes: calculatePortraitPrizePositions( myPrizes: usePrizeCards
prizesX, ? calculatePortraitPrizePositions(
playerY, prizesX,
smallCardW, playerY,
smallCardH, smallCardW,
prizesRowSpacing, smallCardH,
false prizesRowSpacing,
), false,
prizeCount
)
: [],
myEnergyZone: { myEnergyZone: {
x: centerX + energyOffsetX, x: centerX + energyOffsetX,
@ -442,14 +488,17 @@ function calculatePortraitLayout(width: number, height: number): BoardLayout {
rotation: Math.PI, rotation: Math.PI,
}, },
oppPrizes: calculatePortraitPrizePositions( oppPrizes: usePrizeCards
width - prizesX, ? calculatePortraitPrizePositions(
oppY, width - prizesX,
smallCardW, oppY,
smallCardH, smallCardW,
-prizesRowSpacing, smallCardH,
true -prizesRowSpacing,
), true,
prizeCount
)
: [],
oppEnergyZone: { oppEnergyZone: {
x: centerX - energyOffsetX, x: centerX - energyOffsetX,
@ -464,7 +513,7 @@ function calculatePortraitLayout(width: number, height: number): BoardLayout {
} }
/** /**
* Calculate prize card positions in a 2x3 grid. * Calculate prize card positions in a 2xN grid.
* *
* @param startX - X position of first column * @param startX - X position of first column
* @param startY - Y position of first row center * @param startY - Y position of first row center
@ -473,7 +522,8 @@ function calculatePortraitLayout(width: number, height: number): BoardLayout {
* @param colSpacing - Horizontal spacing between columns * @param colSpacing - Horizontal spacing between columns
* @param rowSpacing - Vertical spacing between rows * @param rowSpacing - Vertical spacing between rows
* @param flipped - Whether cards are upside down (opponent) * @param flipped - Whether cards are upside down (opponent)
* @returns Array of 6 prize zone positions * @param count - Number of prize cards (defaults to PRIZE_SIZE)
* @returns Array of prize zone positions
*/ */
function calculatePrizePositions( function calculatePrizePositions(
startX: number, startX: number,
@ -482,13 +532,17 @@ function calculatePrizePositions(
cardH: number, cardH: number,
colSpacing: number, colSpacing: number,
rowSpacing: number, rowSpacing: number,
flipped: boolean flipped: boolean,
count: number = PRIZE_SIZE
): ZonePosition[] { ): ZonePosition[] {
const prizes: ZonePosition[] = [] const prizes: ZonePosition[] = []
const cols = 2
const rows = Math.ceil(count / cols)
for (let row = 0; row < 3; row++) { for (let row = 0; row < rows; row++) {
for (let col = 0; col < 2; col++) { for (let col = 0; col < cols; col++) {
const index = row * 2 + col const index = row * cols + col
if (index >= count) break
prizes.push({ prizes.push({
x: startX + col * colSpacing, x: startX + col * colSpacing,
y: startY + row * rowSpacing, y: startY + row * rowSpacing,
@ -514,7 +568,8 @@ function calculatePrizePositions(
* @param cardH - Card height * @param cardH - Card height
* @param rowSpacing - Vertical spacing (can be negative for overlap) * @param rowSpacing - Vertical spacing (can be negative for overlap)
* @param flipped - Whether cards are upside down * @param flipped - Whether cards are upside down
* @returns Array of 6 prize zone positions * @param count - Number of prize cards (defaults to PRIZE_SIZE)
* @returns Array of prize zone positions
*/ */
function calculatePortraitPrizePositions( function calculatePortraitPrizePositions(
x: number, x: number,
@ -522,12 +577,14 @@ function calculatePortraitPrizePositions(
cardW: number, cardW: number,
cardH: number, cardH: number,
rowSpacing: number, rowSpacing: number,
flipped: boolean flipped: boolean,
count: number = PRIZE_SIZE
): ZonePosition[] { ): ZonePosition[] {
const prizes: ZonePosition[] = [] const prizes: ZonePosition[] = []
const startY = centerY - (2.5 * Math.abs(rowSpacing)) // Center the stack // Center the stack based on count
const startY = centerY - ((count - 1) / 2 * Math.abs(rowSpacing))
for (let i = 0; i < PRIZE_SIZE; i++) { for (let i = 0; i < count; i++) {
prizes.push({ prizes.push({
x, x,
y: startY + i * Math.abs(rowSpacing), y: startY + i * Math.abs(rowSpacing),

View File

@ -29,7 +29,7 @@ import type {
} from '@/types/game' } from '@/types/game'
import type { BoardLayout } from '@/types/phaser' import type { BoardLayout } from '@/types/phaser'
import { getMyPlayerState, getOpponentState } from '@/types/game' import { getMyPlayerState, getOpponentState } from '@/types/game'
import { calculateLayout } from '../layout' import { calculateLayout, type LayoutOptions } from '../layout'
import { Card } from '../objects/Card' import { Card } from '../objects/Card'
import { ActiveZone } from '../objects/ActiveZone' import { ActiveZone } from '../objects/ActiveZone'
import { BenchZone } from '../objects/BenchZone' import { BenchZone } from '../objects/BenchZone'
@ -51,7 +51,7 @@ interface PlayerZones {
hand: HandZone hand: HandZone
deck: PileZone deck: PileZone
discard: PileZone discard: PileZone
prizes: PrizeZone prizes: PrizeZone | null // null when using points system instead of prize cards
energyZone: PileZone energyZone: PileZone
} }
@ -128,8 +128,14 @@ export class StateRenderer {
// Get canvas dimensions // Get canvas dimensions
const { width, height } = this.scene.cameras.main const { width, height } = this.scene.cameras.main
// Extract layout options from rules config
const layoutOptions: LayoutOptions = {
usePrizeCards: state.rules_config?.prizes.use_prize_cards ?? false,
prizeCount: state.rules_config?.prizes.count ?? 4,
}
// Calculate layout // Calculate layout
this.layout = calculateLayout(width, height) this.layout = calculateLayout(width, height, layoutOptions)
// Create zones if needed // Create zones if needed
if (!this.zones) { if (!this.zones) {
@ -258,13 +264,16 @@ export class StateRenderer {
return return
} }
// Check if we should create prize zones
const usePrizeCards = state.rules_config?.prizes.use_prize_cards ?? false
// Create main container // Create main container
this.container = this.scene.add.container(0, 0) this.container = this.scene.add.container(0, 0)
// Create player zones // Create player zones
this.zones = { this.zones = {
my: this.createPlayerZones(myState.player_id, false), my: this.createPlayerZones(myState.player_id, false, usePrizeCards),
opp: this.createPlayerZones(oppState.player_id, true), opp: this.createPlayerZones(oppState.player_id, true, usePrizeCards),
} }
} }
@ -273,9 +282,14 @@ export class StateRenderer {
* *
* @param playerId - The player's ID * @param playerId - The player's ID
* @param isOpponent - Whether this is the opponent (affects layout orientation) * @param isOpponent - Whether this is the opponent (affects layout orientation)
* @param usePrizeCards - Whether to create prize zones (false for points system)
* @returns PlayerZones object * @returns PlayerZones object
*/ */
private createPlayerZones(playerId: string, isOpponent: boolean): PlayerZones { private createPlayerZones(
playerId: string,
isOpponent: boolean,
usePrizeCards: boolean
): PlayerZones {
// Create zones at origin - positions updated in updateZonePositions() // Create zones at origin - positions updated in updateZonePositions()
const zones: PlayerZones = { const zones: PlayerZones = {
active: new ActiveZone(this.scene, 0, 0, playerId), active: new ActiveZone(this.scene, 0, 0, playerId),
@ -283,7 +297,7 @@ export class StateRenderer {
hand: new HandZone(this.scene, 0, 0, playerId), hand: new HandZone(this.scene, 0, 0, playerId),
deck: new PileZone(this.scene, 0, 0, playerId, 'deck'), deck: new PileZone(this.scene, 0, 0, playerId, 'deck'),
discard: new PileZone(this.scene, 0, 0, playerId, 'discard'), discard: new PileZone(this.scene, 0, 0, playerId, 'discard'),
prizes: new PrizeZone(this.scene, 0, 0, playerId), prizes: usePrizeCards ? new PrizeZone(this.scene, 0, 0, playerId) : null,
energyZone: new PileZone(this.scene, 0, 0, playerId, 'deck'), // Energy deck uses pile zone energyZone: new PileZone(this.scene, 0, 0, playerId, 'deck'), // Energy deck uses pile zone
} }
@ -294,7 +308,9 @@ export class StateRenderer {
this.container.add(zones.hand) this.container.add(zones.hand)
this.container.add(zones.deck) this.container.add(zones.deck)
this.container.add(zones.discard) this.container.add(zones.discard)
this.container.add(zones.prizes) if (zones.prizes) {
this.container.add(zones.prizes)
}
this.container.add(zones.energyZone) this.container.add(zones.energyZone)
} }
@ -312,7 +328,7 @@ export class StateRenderer {
zones.hand.destroy() zones.hand.destroy()
zones.deck.destroy() zones.deck.destroy()
zones.discard.destroy() zones.discard.destroy()
zones.prizes.destroy() zones.prizes?.destroy()
zones.energyZone.destroy() zones.energyZone.destroy()
} }
@ -344,8 +360,8 @@ export class StateRenderer {
this.zones.my.bench.setZoneDimensions(totalWidth, benchLayout[0].height) this.zones.my.bench.setZoneDimensions(totalWidth, benchLayout[0].height)
} }
// Prizes - use the first prize position for the zone // Prizes - use the first prize position for the zone (only if using prize cards)
if (this.layout.myPrizes.length > 0) { if (this.zones.my.prizes && this.layout.myPrizes.length > 0) {
const prizePositions = this.layout.myPrizes const prizePositions = this.layout.myPrizes
const minX = Math.min(...prizePositions.map(p => p.x - p.width / 2)) const minX = Math.min(...prizePositions.map(p => p.x - p.width / 2))
const maxX = Math.max(...prizePositions.map(p => p.x + p.width / 2)) const maxX = Math.max(...prizePositions.map(p => p.x + p.width / 2))
@ -375,8 +391,8 @@ export class StateRenderer {
this.zones.opp.bench.setZoneDimensions(totalWidth, oppBenchLayout[0].height) this.zones.opp.bench.setZoneDimensions(totalWidth, oppBenchLayout[0].height)
} }
// Opponent prizes // Opponent prizes (only if using prize cards)
if (this.layout.oppPrizes.length > 0) { if (this.zones.opp.prizes && this.layout.oppPrizes.length > 0) {
const prizePositions = this.layout.oppPrizes const prizePositions = this.layout.oppPrizes
const minX = Math.min(...prizePositions.map(p => p.x - p.width / 2)) const minX = Math.min(...prizePositions.map(p => p.x - p.width / 2))
const maxX = Math.max(...prizePositions.map(p => p.x + p.width / 2)) const maxX = Math.max(...prizePositions.map(p => p.x + p.width / 2))
@ -449,10 +465,10 @@ export class StateRenderer {
true // always visible true // always visible
) )
// Update prizes // Update prizes (only if using prize cards)
// Viewer sees their own prize count (cards still hidden) // Viewer sees their own prize count (cards still hidden)
// Opponent just sees count // Opponent just sees count
zones.prizes.setRemainingCount(playerState.prizes_count) zones.prizes?.setRemainingCount(playerState.prizes_count)
// Update energy zone // Update energy zone
this.updateZone( this.updateZone(

View File

@ -14,6 +14,7 @@ import type {
CardDefinition, CardDefinition,
TurnPhase, TurnPhase,
Action, Action,
RulesConfig,
} from '@/types' } from '@/types'
import { getMyPlayerState, getOpponentState, getCardDefinition, ConnectionStatus } from '@/types' import { getMyPlayerState, getOpponentState, getCardDefinition, ConnectionStatus } from '@/types'
@ -216,6 +217,25 @@ export const useGameStore = defineStore('game', () => {
forcedAction.value?.player === gameState.value?.viewer_id forcedAction.value?.player === gameState.value?.viewer_id
) )
// ---------------------------------------------------------------------------
// Computed - Rules Config
// ---------------------------------------------------------------------------
/** Current game rules configuration */
const rulesConfig = computed<RulesConfig | null>(() =>
gameState.value?.rules_config ?? null
)
/** Whether this game uses classic prize cards (vs points system) */
const usePrizeCards = computed<boolean>(() =>
rulesConfig.value?.prizes.use_prize_cards ?? false
)
/** Number of prizes/points needed to win */
const prizeCount = computed<number>(() =>
rulesConfig.value?.prizes.count ?? 4
)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Computed - Card Lookup // Computed - Card Lookup
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -337,6 +357,11 @@ export const useGameStore = defineStore('game', () => {
hasForcedAction, hasForcedAction,
isConnected, isConnected,
// Rules config
rulesConfig,
usePrizeCards,
prizeCount,
// Card lookup // Card lookup
lookupCard, lookupCard,

View File

@ -6,7 +6,7 @@
* and sends this structure via WebSocket. * and sends this structure via WebSocket.
*/ */
import type { ModifierMode } from './rules' import type { ModifierMode, RulesConfig } from './rules'
// ============================================================================= // =============================================================================
// Enums and Constants // Enums and Constants
@ -407,6 +407,9 @@ export interface VisibleGameState {
/** Card definitions for display (definition_id -> CardDefinition) */ /** Card definitions for display (definition_id -> CardDefinition) */
card_registry: Record<string, CardDefinition> card_registry: Record<string, CardDefinition>
/** Rules configuration for UI rendering decisions (e.g., prize cards vs points) */
rules_config?: RulesConfig
} }
// ============================================================================= // =============================================================================