fix(know-your-world): fix server/client filter mismatch for USA map

PlayingPhase was using deprecated getFilteredMapDataSync with state.difficulty
(which was undefined) instead of the new getFilteredMapDataBySizesSync with
state.includeSizes. This caused a mismatch where the server generated 50 regions
but the client filtered to 35, resulting in prompts showing "..." when the
current region wasn't in the client's filtered list.

Changes:
- Update PlayingPhase to use getFilteredMapDataBySizesSync with includeSizes
- Add error throwing (not just warning) when prompt not found in filtered regions
- Update test mocks to use new function and state structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-26 21:59:00 -06:00
parent a05c4ca5bf
commit 98e74bae3a
6 changed files with 48 additions and 30 deletions

View File

@ -77,7 +77,9 @@
"Bash(docker logs:*)", "Bash(docker logs:*)",
"Bash(docker exec:*)", "Bash(docker exec:*)",
"Bash(node --input-type=module -e:*)", "Bash(node --input-type=module -e:*)",
"Bash(npm test:*)" "Bash(npm test:*)",
"Bash(npx tsx:*)",
"Bash(tsc:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@ -553,7 +553,8 @@ export class KnowYourWorldValidator
// Determine re-ask position based on assistance level // Determine re-ask position based on assistance level
// Guided/Helpful: re-ask soon (after 2-3 regions) to reinforce learning // Guided/Helpful: re-ask soon (after 2-3 regions) to reinforce learning
// Standard/None: re-ask at the end // Standard/None: re-ask at the end
const isHighAssistance = state.assistanceLevel === 'guided' || state.assistanceLevel === 'helpful' const isHighAssistance =
state.assistanceLevel === 'guided' || state.assistanceLevel === 'helpful'
const reaskDelay = isHighAssistance ? 3 : state.regionsToFind.length const reaskDelay = isHighAssistance ? 3 : state.regionsToFind.length
// Build new regions queue: take next regions, then insert given-up region at appropriate position // Build new regions queue: take next regions, then insert given-up region at appropriate position

View File

@ -853,11 +853,7 @@ export function DrillDownMapSelector({
<span <span
className={css({ className={css({
fontWeight: '500', fontWeight: '500',
color: isChecked color: isChecked ? 'white' : isDark ? 'gray.200' : 'gray.700',
? 'white'
: isDark
? 'gray.200'
: 'gray.700',
})} })}
> >
{config.label} {config.label}
@ -908,8 +904,7 @@ export function DrillDownMapSelector({
{peers.map((peer) => { {peers.map((peer) => {
// Check if this is a planet (joke at world level) // Check if this is a planet (joke at world level)
const isPlanet = 'isPlanet' in peer && peer.isPlanet const isPlanet = 'isPlanet' in peer && peer.isPlanet
const planetData = const planetData = 'planetData' in peer ? (peer.planetData as PlanetData | null) : null
'planetData' in peer ? (peer.planetData as PlanetData | null) : null
// Calculate viewBox for this peer's continent (only for non-planets) // Calculate viewBox for this peer's continent (only for non-planets)
const peerContinentId = peer.path[0] const peerContinentId = peer.path[0]
@ -1105,7 +1100,6 @@ export function DrillDownMapSelector({
})} })}
</div> </div>
)} )}
</div> </div>
) )
} }

View File

@ -9,7 +9,8 @@ vi.mock('../Provider', () => ({
state: { state: {
selectedMap: 'world' as const, selectedMap: 'world' as const,
selectedContinent: 'all', selectedContinent: 'all',
difficulty: 'easy', includeSizes: ['huge', 'large', 'medium'],
assistanceLevel: 'helpful',
regionsFound: ['france', 'germany'], regionsFound: ['france', 'germany'],
currentPrompt: 'spain', currentPrompt: 'spain',
gameMode: 'cooperative' as const, gameMode: 'cooperative' as const,
@ -20,7 +21,7 @@ vi.mock('../Provider', () => ({
})) }))
vi.mock('../maps', () => ({ vi.mock('../maps', () => ({
getFilteredMapDataSync: () => getFilteredMapDataBySizesSync: () =>
({ ({
id: 'world', id: 'world',
name: 'World Map', name: 'World Map',
@ -107,7 +108,8 @@ describe('PlayingPhase', () => {
state: { state: {
selectedMap: 'world' as const, selectedMap: 'world' as const,
selectedContinent: 'all', selectedContinent: 'all',
difficulty: 'easy', includeSizes: ['huge', 'large', 'medium'],
assistanceLevel: 'helpful',
regionsFound: ['france', 'germany'], regionsFound: ['france', 'germany'],
currentPrompt: null, currentPrompt: null,
gameMode: 'cooperative' as const, gameMode: 'cooperative' as const,
@ -142,7 +144,8 @@ describe('PlayingPhase', () => {
state: { state: {
selectedMap: 'world' as const, selectedMap: 'world' as const,
selectedContinent: 'all', selectedContinent: 'all',
difficulty: 'easy', includeSizes: ['huge', 'large', 'medium'],
assistanceLevel: 'helpful',
regionsFound: [], regionsFound: [],
currentPrompt: 'spain', currentPrompt: 'spain',
gameMode: 'cooperative' as const, gameMode: 'cooperative' as const,
@ -159,8 +162,8 @@ describe('PlayingPhase', () => {
expect(mockClickRegion).toHaveBeenCalledWith('spain', 'Spain') expect(mockClickRegion).toHaveBeenCalledWith('spain', 'Spain')
}) })
it('uses correct map data from getFilteredMapDataSync', () => { it('uses correct map data from getFilteredMapDataBySizesSync', () => {
const mockGetFilteredMapDataSync = vi.fn().mockReturnValue({ const mockGetFilteredMapDataBySizesSync = vi.fn().mockReturnValue({
id: 'usa', id: 'usa',
name: 'USA Map', name: 'USA Map',
viewBox: '0 0 2000 1000', viewBox: '0 0 2000 1000',
@ -170,11 +173,16 @@ describe('PlayingPhase', () => {
], ],
}) })
vi.mocked(vi.importActual('../maps')).getFilteredMapDataSync = mockGetFilteredMapDataSync vi.mocked(vi.importActual('../maps')).getFilteredMapDataBySizesSync =
mockGetFilteredMapDataBySizesSync
render(<PlayingPhase />) render(<PlayingPhase />)
expect(mockGetFilteredMapDataSync).toHaveBeenCalledWith('world', 'all', 'easy') expect(mockGetFilteredMapDataBySizesSync).toHaveBeenCalledWith('world', 'all', [
'huge',
'large',
'medium',
])
}) })
}) })
@ -184,7 +192,8 @@ describe('PlayingPhase - Different Scenarios', () => {
state: { state: {
selectedMap: 'world' as const, selectedMap: 'world' as const,
selectedContinent: 'all', selectedContinent: 'all',
difficulty: 'easy', includeSizes: ['huge', 'large', 'medium'],
assistanceLevel: 'helpful',
regionsFound: [], regionsFound: [],
currentPrompt: 'spain', currentPrompt: 'spain',
gameMode: 'cooperative' as const, gameMode: 'cooperative' as const,
@ -203,7 +212,8 @@ describe('PlayingPhase - Different Scenarios', () => {
state: { state: {
selectedMap: 'world' as const, selectedMap: 'world' as const,
selectedContinent: 'all', selectedContinent: 'all',
difficulty: 'easy', includeSizes: ['huge', 'large', 'medium'],
assistanceLevel: 'helpful',
regionsFound: ['spain', 'italy', 'portugal'], regionsFound: ['spain', 'italy', 'portugal'],
currentPrompt: null, currentPrompt: null,
gameMode: 'cooperative' as const, gameMode: 'cooperative' as const,
@ -217,12 +227,13 @@ describe('PlayingPhase - Different Scenarios', () => {
expect(screen.getByText('Progress: 3/3')).toBeInTheDocument() expect(screen.getByText('Progress: 3/3')).toBeInTheDocument()
}) })
it('renders with hard difficulty', () => { it('renders with no assistance mode', () => {
vi.mocked(vi.importActual('../Provider')).useKnowYourWorld = () => ({ vi.mocked(vi.importActual('../Provider')).useKnowYourWorld = () => ({
state: { state: {
selectedMap: 'world' as const, selectedMap: 'world' as const,
selectedContinent: 'all', selectedContinent: 'all',
difficulty: 'hard', includeSizes: ['huge', 'large', 'medium', 'small', 'tiny'],
assistanceLevel: 'none',
regionsFound: [], regionsFound: [],
currentPrompt: 'luxembourg', currentPrompt: 'luxembourg',
gameMode: 'race' as const, gameMode: 'race' as const,

View File

@ -4,7 +4,7 @@ import { useCallback, useMemo } from 'react'
import { css } from '@styled/css' import { css } from '@styled/css'
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels' import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'
import { useKnowYourWorld } from '../Provider' import { useKnowYourWorld } from '../Provider'
import { getFilteredMapDataSync } from '../maps' import { getFilteredMapDataBySizesSync } from '../maps'
import { MapRenderer } from './MapRenderer' import { MapRenderer } from './MapRenderer'
import { GameInfoPanel } from './GameInfoPanel' import { GameInfoPanel } from './GameInfoPanel'
import { useViewerId } from '@/lib/arcade/game-sdk' import { useViewerId } from '@/lib/arcade/game-sdk'
@ -39,10 +39,10 @@ export function PlayingPhase() {
[localPlayerId, viewerId, sendCursorUpdate] [localPlayerId, viewerId, sendCursorUpdate]
) )
const mapData = getFilteredMapDataSync( const mapData = getFilteredMapDataBySizesSync(
state.selectedMap, state.selectedMap,
state.selectedContinent, state.selectedContinent,
state.difficulty state.includeSizes
) )
const totalRegions = mapData.regions.length const totalRegions = mapData.regions.length
const foundCount = state.regionsFound.length const foundCount = state.regionsFound.length
@ -55,15 +55,22 @@ export function PlayingPhase() {
const currentRegionName = currentRegion?.name ?? null const currentRegionName = currentRegion?.name ?? null
const currentRegionId = currentRegion?.id ?? null const currentRegionId = currentRegion?.id ?? null
// Debug warning if prompt not found in filtered regions (indicates server/client filter mismatch) // Error if prompt not found in filtered regions (indicates server/client filter mismatch)
if (state.currentPrompt && !currentRegion) { if (state.currentPrompt && !currentRegion) {
console.warn('[PlayingPhase] Prompt not in filtered regions - server/client filter mismatch:', { const errorInfo = {
currentPrompt: state.currentPrompt, currentPrompt: state.currentPrompt,
difficulty: state.difficulty, includeSizes: state.includeSizes,
selectedMap: state.selectedMap,
selectedContinent: state.selectedContinent, selectedContinent: state.selectedContinent,
clientFilteredCount: mapData.regions.length, clientFilteredCount: mapData.regions.length,
serverRegionsToFindCount: state.regionsToFind.length, serverRegionsToFindCount: state.regionsToFind.length,
}) clientRegionIds: mapData.regions.map((r) => r.id).slice(0, 10), // First 10 for debugging
}
console.error('[PlayingPhase] CRITICAL: Prompt not in filtered regions!', errorInfo)
throw new Error(
`Server/client filter mismatch: prompt "${state.currentPrompt}" not found in client's ${mapData.regions.length} filtered regions. ` +
`Server has ${state.regionsToFind.length} regions. includeSizes=${JSON.stringify(state.includeSizes)}`
)
} }
return ( return (

View File

@ -226,7 +226,10 @@ export const ALL_REGION_SIZES: RegionSize[] = ['huge', 'large', 'medium', 'small
/** /**
* Display configuration for each region size * Display configuration for each region size
*/ */
export const REGION_SIZE_CONFIG: Record<RegionSize, { label: string; emoji: string; description: string }> = { export const REGION_SIZE_CONFIG: Record<
RegionSize,
{ label: string; emoji: string; description: string }
> = {
huge: { huge: {
label: 'Major', label: 'Major',
emoji: '🌍', emoji: '🌍',