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
701 lines
23 KiB
TypeScript
701 lines
23 KiB
TypeScript
/**
|
|
* Tests for Board Layout System.
|
|
*
|
|
* Verifies that layout calculations produce valid, non-overlapping zone
|
|
* positions for both landscape and portrait orientations.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest'
|
|
|
|
import {
|
|
calculateLayout,
|
|
isPortrait,
|
|
getScaleFactor,
|
|
zonesOverlap,
|
|
isZoneInBounds,
|
|
getAllZones,
|
|
getCardSize,
|
|
CARD_WIDTH_MEDIUM,
|
|
CARD_HEIGHT_MEDIUM,
|
|
CARD_WIDTH_SMALL,
|
|
BENCH_SIZE,
|
|
PRIZE_SIZE,
|
|
PORTRAIT_THRESHOLD,
|
|
} from './layout'
|
|
import type { BoardLayout, ZonePosition } from '@/types/phaser'
|
|
|
|
// Design resolution constants (duplicated here to avoid Phaser import)
|
|
const DESIGN_WIDTH = 1920
|
|
const DESIGN_HEIGHT = 1080
|
|
|
|
// =============================================================================
|
|
// Test Constants
|
|
// =============================================================================
|
|
|
|
/** Standard landscape resolution (1080p) */
|
|
const LANDSCAPE_WIDTH = 1920
|
|
const LANDSCAPE_HEIGHT = 1080
|
|
|
|
/** Standard portrait resolution (mobile) */
|
|
const PORTRAIT_WIDTH = 390
|
|
const PORTRAIT_HEIGHT = 844
|
|
|
|
/** Square resolution (edge case) */
|
|
const SQUARE_SIZE = 800
|
|
|
|
/** 4K resolution for scaling tests */
|
|
const UHD_WIDTH = 3840
|
|
const UHD_HEIGHT = 2160
|
|
|
|
// =============================================================================
|
|
// Helper Functions
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Check that a zone has valid positive dimensions.
|
|
*/
|
|
function isValidZone(zone: ZonePosition): boolean {
|
|
return (
|
|
typeof zone.x === 'number' &&
|
|
typeof zone.y === 'number' &&
|
|
typeof zone.width === 'number' &&
|
|
typeof zone.height === 'number' &&
|
|
zone.width > 0 &&
|
|
zone.height > 0 &&
|
|
!isNaN(zone.x) &&
|
|
!isNaN(zone.y)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Check that critical gameplay zones don't overlap.
|
|
*
|
|
* We check active zones, bench slots, and deck/discard.
|
|
* Hand zones are allowed to be wide and may overlap with other areas.
|
|
* Prize zones may overlap with each other (stacked display).
|
|
*/
|
|
function getCriticalZones(layout: BoardLayout): ZonePosition[] {
|
|
return [
|
|
layout.myActive,
|
|
...layout.myBench,
|
|
layout.myDeck,
|
|
layout.myDiscard,
|
|
layout.oppActive,
|
|
...layout.oppBench,
|
|
layout.oppDeck,
|
|
layout.oppDiscard,
|
|
]
|
|
}
|
|
|
|
// =============================================================================
|
|
// Orientation Detection Tests
|
|
// =============================================================================
|
|
|
|
describe('isPortrait', () => {
|
|
it('returns false for landscape aspect ratios', () => {
|
|
/**
|
|
* Test that standard desktop/landscape resolutions are detected as landscape.
|
|
*
|
|
* Landscape detection is critical for applying the correct layout algorithm
|
|
* that places opponent zones at top and player zones at bottom.
|
|
*/
|
|
expect(isPortrait(1920, 1080)).toBe(false)
|
|
expect(isPortrait(1280, 720)).toBe(false)
|
|
expect(isPortrait(2560, 1440)).toBe(false)
|
|
})
|
|
|
|
it('returns true for portrait aspect ratios', () => {
|
|
/**
|
|
* Test that mobile/portrait resolutions are detected as portrait.
|
|
*
|
|
* Portrait detection enables the compressed horizontal layout needed
|
|
* for narrow screens where cards must fit side-by-side.
|
|
*/
|
|
expect(isPortrait(390, 844)).toBe(true)
|
|
expect(isPortrait(375, 812)).toBe(true)
|
|
expect(isPortrait(414, 896)).toBe(true)
|
|
})
|
|
|
|
it('handles edge case near threshold', () => {
|
|
/**
|
|
* Test behavior around the portrait/landscape threshold.
|
|
*
|
|
* The threshold should provide a clean switch point without
|
|
* ambiguous behavior for borderline aspect ratios.
|
|
*/
|
|
// Just below threshold = portrait
|
|
expect(isPortrait(89, 100)).toBe(true)
|
|
|
|
// At or above threshold = landscape
|
|
expect(isPortrait(90, 100)).toBe(false)
|
|
expect(isPortrait(100, 100)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('getScaleFactor', () => {
|
|
it('returns 1.0 for design resolution', () => {
|
|
/**
|
|
* Test that the design resolution produces a scale factor of 1.0.
|
|
*
|
|
* This verifies our base calculations are correct and layout positions
|
|
* will match expected values at the reference resolution.
|
|
*/
|
|
const scale = getScaleFactor(DESIGN_WIDTH, DESIGN_HEIGHT)
|
|
expect(scale).toBe(1)
|
|
})
|
|
|
|
it('scales down for smaller screens', () => {
|
|
/**
|
|
* Test that smaller screens get a scale factor less than 1.0.
|
|
*
|
|
* Proper scaling ensures cards and zones fit on smaller displays
|
|
* while maintaining playable proportions.
|
|
*/
|
|
const scale = getScaleFactor(960, 540)
|
|
expect(scale).toBe(0.5)
|
|
})
|
|
|
|
it('scales up for larger screens', () => {
|
|
/**
|
|
* Test that larger screens get a scale factor greater than 1.0.
|
|
*
|
|
* Scaling up for 4K and larger displays ensures the game
|
|
* doesn't appear tiny on high-resolution screens.
|
|
*/
|
|
const scale = getScaleFactor(UHD_WIDTH, UHD_HEIGHT)
|
|
expect(scale).toBe(2)
|
|
})
|
|
|
|
it('uses limiting dimension for non-matching aspect ratios', () => {
|
|
/**
|
|
* Test that scaling is constrained by the smaller dimension ratio.
|
|
*
|
|
* For example, an ultra-wide screen should scale based on height
|
|
* to prevent vertical overflow.
|
|
*/
|
|
// Ultra-wide: limited by height
|
|
const ultraWide = getScaleFactor(3440, 1080)
|
|
expect(ultraWide).toBe(1) // Limited by 1080/1080
|
|
|
|
// Tall and narrow: limited by width
|
|
const tallNarrow = getScaleFactor(960, 2160)
|
|
expect(tallNarrow).toBe(0.5) // Limited by 960/1920
|
|
})
|
|
})
|
|
|
|
// =============================================================================
|
|
// Landscape Layout Tests
|
|
// =============================================================================
|
|
|
|
describe('calculateLayout - landscape', () => {
|
|
it('produces valid positions for all zones', () => {
|
|
/**
|
|
* Test that all zones in a landscape layout have valid positions.
|
|
*
|
|
* Every zone must have numeric coordinates and positive dimensions
|
|
* for Phaser to render them correctly.
|
|
*/
|
|
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
const allZones = getAllZones(layout)
|
|
|
|
for (const { label, zone } of allZones) {
|
|
expect(isValidZone(zone), `Zone ${label} should be valid`).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('creates correct number of bench slots', () => {
|
|
/**
|
|
* Test that bench arrays have exactly 5 slots.
|
|
*
|
|
* The Pokemon TCG bench has 5 positions, and both players
|
|
* need all slots available.
|
|
*/
|
|
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
|
|
expect(layout.myBench).toHaveLength(BENCH_SIZE)
|
|
expect(layout.oppBench).toHaveLength(BENCH_SIZE)
|
|
})
|
|
|
|
it('creates correct number of prize slots', () => {
|
|
/**
|
|
* Test that prize arrays have exactly 6 slots.
|
|
*
|
|
* Each player starts with 6 prize cards that must all have
|
|
* defined positions.
|
|
*/
|
|
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
|
|
expect(layout.myPrizes).toHaveLength(PRIZE_SIZE)
|
|
expect(layout.oppPrizes).toHaveLength(PRIZE_SIZE)
|
|
})
|
|
|
|
it('places player zones in bottom half of screen', () => {
|
|
/**
|
|
* Test that player's active zone is below the center line.
|
|
*
|
|
* The player's perspective should have their cards at the bottom
|
|
* for easy access and viewing.
|
|
*/
|
|
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
const centerY = LANDSCAPE_HEIGHT / 2
|
|
|
|
expect(layout.myActive.y).toBeGreaterThan(centerY)
|
|
expect(layout.myDeck.y).toBeGreaterThan(centerY)
|
|
})
|
|
|
|
it('places opponent zones in top half of screen', () => {
|
|
/**
|
|
* Test that opponent's active zone is above the center line.
|
|
*
|
|
* The opponent's cards should be at the top, creating the
|
|
* traditional face-to-face card game layout.
|
|
*/
|
|
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
const centerY = LANDSCAPE_HEIGHT / 2
|
|
|
|
expect(layout.oppActive.y).toBeLessThan(centerY)
|
|
expect(layout.oppDeck.y).toBeLessThan(centerY)
|
|
})
|
|
|
|
it('rotates opponent zones by 180 degrees', () => {
|
|
/**
|
|
* Test that opponent cards are displayed upside down.
|
|
*
|
|
* This creates the impression of facing an opponent across
|
|
* a table, making their cards readable from their perspective.
|
|
*/
|
|
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
|
|
expect(layout.oppActive.rotation).toBe(Math.PI)
|
|
expect(layout.oppBench[0].rotation).toBe(Math.PI)
|
|
})
|
|
|
|
it('does not rotate player zones', () => {
|
|
/**
|
|
* Test that player's own cards are right-side up.
|
|
*
|
|
* The player's cards should face them for easy reading.
|
|
*/
|
|
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
|
|
expect(layout.myActive.rotation).toBe(0)
|
|
expect(layout.myBench[0].rotation).toBe(0)
|
|
})
|
|
|
|
it('places hand zone at bottom edge', () => {
|
|
/**
|
|
* Test that the hand zone is near the bottom of the screen.
|
|
*
|
|
* The hand should be easily accessible at the bottom where
|
|
* the player can drag cards from it.
|
|
*/
|
|
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
|
|
// Hand should be in the bottom 15% of the screen
|
|
expect(layout.myHand.y).toBeGreaterThan(LANDSCAPE_HEIGHT * 0.85)
|
|
})
|
|
|
|
it('critical zones do not overlap', () => {
|
|
/**
|
|
* Test that active zones, bench slots, and deck/discard don't overlap.
|
|
*
|
|
* Overlapping zones would make it impossible to distinguish
|
|
* between different game areas during play.
|
|
*/
|
|
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
const criticalZones = getCriticalZones(layout)
|
|
|
|
for (let i = 0; i < criticalZones.length; i++) {
|
|
for (let j = i + 1; j < criticalZones.length; j++) {
|
|
const overlaps = zonesOverlap(criticalZones[i], criticalZones[j])
|
|
expect(overlaps, `Zones ${i} and ${j} should not overlap`).toBe(false)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('active zones are horizontally centered', () => {
|
|
/**
|
|
* Test that active zones are centered on the screen.
|
|
*
|
|
* The active Pokemon should be prominently displayed in
|
|
* the center of each player's area.
|
|
*/
|
|
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
const centerX = LANDSCAPE_WIDTH / 2
|
|
|
|
expect(layout.myActive.x).toBe(centerX)
|
|
expect(layout.oppActive.x).toBe(centerX)
|
|
})
|
|
})
|
|
|
|
// =============================================================================
|
|
// Portrait Layout Tests
|
|
// =============================================================================
|
|
|
|
describe('calculateLayout - portrait', () => {
|
|
it('produces valid positions for all zones', () => {
|
|
/**
|
|
* Test that portrait layout produces valid zone positions.
|
|
*
|
|
* Portrait mode must work on mobile devices with limited
|
|
* horizontal space.
|
|
*/
|
|
const layout = calculateLayout(PORTRAIT_WIDTH, PORTRAIT_HEIGHT)
|
|
const allZones = getAllZones(layout)
|
|
|
|
for (const { label, zone } of allZones) {
|
|
expect(isValidZone(zone), `Zone ${label} should be valid`).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('uses smaller card sizes than landscape', () => {
|
|
/**
|
|
* Test that portrait mode uses smaller card sizes.
|
|
*
|
|
* Mobile screens need smaller cards to fit all zones
|
|
* without excessive overlap.
|
|
*/
|
|
const portrait = calculateLayout(PORTRAIT_WIDTH, PORTRAIT_HEIGHT)
|
|
const landscape = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
|
|
expect(portrait.myActive.width).toBeLessThan(landscape.myActive.width)
|
|
expect(portrait.myActive.height).toBeLessThan(landscape.myActive.height)
|
|
})
|
|
|
|
it('maintains player/opponent vertical separation', () => {
|
|
/**
|
|
* Test that player and opponent zones are still vertically separated.
|
|
*
|
|
* Even in portrait mode, the fundamental layout of opponent-top
|
|
* and player-bottom must be maintained.
|
|
*/
|
|
const layout = calculateLayout(PORTRAIT_WIDTH, PORTRAIT_HEIGHT)
|
|
|
|
expect(layout.myActive.y).toBeGreaterThan(layout.oppActive.y)
|
|
})
|
|
|
|
it('fits all bench slots within screen width', () => {
|
|
/**
|
|
* Test that bench slots don't extend beyond screen edges.
|
|
*
|
|
* Portrait mode must compress bench horizontally while keeping
|
|
* all 5 slots visible and accessible.
|
|
*/
|
|
const layout = calculateLayout(PORTRAIT_WIDTH, PORTRAIT_HEIGHT)
|
|
|
|
for (const benchSlot of layout.myBench) {
|
|
const left = benchSlot.x - benchSlot.width / 2
|
|
const right = benchSlot.x + benchSlot.width / 2
|
|
|
|
expect(left).toBeGreaterThanOrEqual(0)
|
|
expect(right).toBeLessThanOrEqual(PORTRAIT_WIDTH)
|
|
}
|
|
})
|
|
|
|
it('critical zones do not overlap', () => {
|
|
/**
|
|
* Test that critical zones don't overlap in portrait mode.
|
|
*
|
|
* The compressed layout must still keep gameplay zones distinct.
|
|
*/
|
|
const layout = calculateLayout(PORTRAIT_WIDTH, PORTRAIT_HEIGHT)
|
|
const criticalZones = getCriticalZones(layout)
|
|
|
|
for (let i = 0; i < criticalZones.length; i++) {
|
|
for (let j = i + 1; j < criticalZones.length; j++) {
|
|
const overlaps = zonesOverlap(criticalZones[i], criticalZones[j])
|
|
expect(overlaps, `Zones ${i} and ${j} should not overlap`).toBe(false)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
// =============================================================================
|
|
// Scaling Tests
|
|
// =============================================================================
|
|
|
|
describe('layout scaling', () => {
|
|
it('scales proportionally for larger screens', () => {
|
|
/**
|
|
* Test that 4K resolution produces proportionally larger zones.
|
|
*
|
|
* The layout should scale up while maintaining the same
|
|
* relative proportions.
|
|
*/
|
|
const hd = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
const uhd = calculateLayout(UHD_WIDTH, UHD_HEIGHT)
|
|
|
|
// 4K is exactly 2x 1080p, so zones should be 2x larger
|
|
expect(uhd.myActive.width).toBeCloseTo(hd.myActive.width * 2, 0)
|
|
expect(uhd.myActive.height).toBeCloseTo(hd.myActive.height * 2, 0)
|
|
})
|
|
|
|
it('scales proportionally for smaller screens', () => {
|
|
/**
|
|
* Test that half-size resolution produces proportionally smaller zones.
|
|
*
|
|
* The layout should scale down while maintaining relative proportions.
|
|
*/
|
|
const hd = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
const half = calculateLayout(LANDSCAPE_WIDTH / 2, LANDSCAPE_HEIGHT / 2)
|
|
|
|
expect(half.myActive.width).toBeCloseTo(hd.myActive.width / 2, 0)
|
|
expect(half.myActive.height).toBeCloseTo(hd.myActive.height / 2, 0)
|
|
})
|
|
|
|
it('maintains relative zone positions across scales', () => {
|
|
/**
|
|
* Test that zone positions scale proportionally.
|
|
*
|
|
* If active zone is at 50% width on 1080p, it should still
|
|
* be at 50% width on 4K.
|
|
*/
|
|
const hd = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
const uhd = calculateLayout(UHD_WIDTH, UHD_HEIGHT)
|
|
|
|
// Active zone should be centered at same relative position
|
|
const hdRelativeX = hd.myActive.x / LANDSCAPE_WIDTH
|
|
const uhdRelativeX = uhd.myActive.x / UHD_WIDTH
|
|
|
|
expect(uhdRelativeX).toBeCloseTo(hdRelativeX, 1)
|
|
})
|
|
})
|
|
|
|
// =============================================================================
|
|
// Utility Function Tests
|
|
// =============================================================================
|
|
|
|
describe('zonesOverlap', () => {
|
|
it('detects overlapping zones', () => {
|
|
/**
|
|
* Test that overlapping rectangles are correctly identified.
|
|
*
|
|
* This utility is critical for validating layout correctness.
|
|
*/
|
|
const zone1: ZonePosition = { x: 100, y: 100, width: 50, height: 50 }
|
|
const zone2: ZonePosition = { x: 120, y: 120, width: 50, height: 50 }
|
|
|
|
expect(zonesOverlap(zone1, zone2)).toBe(true)
|
|
})
|
|
|
|
it('returns false for non-overlapping zones', () => {
|
|
/**
|
|
* Test that separated zones are correctly identified as non-overlapping.
|
|
*/
|
|
const zone1: ZonePosition = { x: 100, y: 100, width: 50, height: 50 }
|
|
const zone2: ZonePosition = { x: 200, y: 200, width: 50, height: 50 }
|
|
|
|
expect(zonesOverlap(zone1, zone2)).toBe(false)
|
|
})
|
|
|
|
it('handles adjacent zones (edge touching)', () => {
|
|
/**
|
|
* Test that zones with touching edges are not considered overlapping.
|
|
*
|
|
* Two zones can be directly adjacent without overlapping.
|
|
*/
|
|
const zone1: ZonePosition = { x: 100, y: 100, width: 50, height: 50 }
|
|
const zone2: ZonePosition = { x: 150, y: 100, width: 50, height: 50 } // Adjacent on right
|
|
|
|
expect(zonesOverlap(zone1, zone2)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('isZoneInBounds', () => {
|
|
it('returns true for zones within canvas', () => {
|
|
/**
|
|
* Test that zones fully within canvas bounds are detected.
|
|
*/
|
|
const zone: ZonePosition = { x: 500, y: 500, width: 100, height: 100 }
|
|
|
|
expect(isZoneInBounds(zone, 1000, 1000)).toBe(true)
|
|
})
|
|
|
|
it('returns false for zones extending past canvas', () => {
|
|
/**
|
|
* Test that zones extending beyond canvas edges are detected.
|
|
*/
|
|
const zone: ZonePosition = { x: 980, y: 500, width: 100, height: 100 }
|
|
|
|
expect(isZoneInBounds(zone, 1000, 1000)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('getCardSize', () => {
|
|
it('returns medium card size for landscape', () => {
|
|
/**
|
|
* Test that landscape mode uses medium-sized cards.
|
|
*
|
|
* Desktop screens have enough space for comfortably-sized cards.
|
|
*/
|
|
const size = getCardSize(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
|
|
// Should be close to medium card size at scale 1.0
|
|
expect(size.width).toBeCloseTo(CARD_WIDTH_MEDIUM, 0)
|
|
})
|
|
|
|
it('returns smaller card size for portrait', () => {
|
|
/**
|
|
* Test that portrait mode uses smaller cards.
|
|
*
|
|
* Mobile screens need smaller cards to fit the compressed layout.
|
|
*/
|
|
const portrait = getCardSize(PORTRAIT_WIDTH, PORTRAIT_HEIGHT)
|
|
const landscape = getCardSize(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
|
|
expect(portrait.width).toBeLessThan(landscape.width)
|
|
})
|
|
})
|
|
|
|
describe('getAllZones', () => {
|
|
it('returns all zones with labels', () => {
|
|
/**
|
|
* Test that getAllZones returns all defined zones.
|
|
*
|
|
* This utility should provide complete access to all layout zones
|
|
* for iteration and validation.
|
|
*/
|
|
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
const allZones = getAllZones(layout)
|
|
|
|
// Count expected zones:
|
|
// 2 active + 10 bench + 2 hand + 2 deck + 2 discard + 12 prizes + 2 energy = 32
|
|
const expectedCount = 2 + (BENCH_SIZE * 2) + 2 + 2 + 2 + (PRIZE_SIZE * 2) + 2
|
|
expect(allZones).toHaveLength(expectedCount)
|
|
})
|
|
|
|
it('includes labeled bench slots', () => {
|
|
/**
|
|
* Test that bench slots have indexed labels.
|
|
*
|
|
* Labels should clearly identify which slot each zone represents.
|
|
*/
|
|
const layout = calculateLayout(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT)
|
|
const allZones = getAllZones(layout)
|
|
|
|
const benchLabels = allZones.filter(z => z.label.includes('Bench'))
|
|
expect(benchLabels).toHaveLength(BENCH_SIZE * 2)
|
|
|
|
// Check specific labels exist
|
|
expect(allZones.find(z => z.label === 'myBench[0]')).toBeDefined()
|
|
expect(allZones.find(z => z.label === 'myBench[4]')).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)
|
|
}
|
|
})
|
|
})
|