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 exec:*)",
"Bash(node --input-type=module -e:*)",
"Bash(npm test:*)"
"Bash(npm test:*)",
"Bash(npx tsx:*)",
"Bash(tsc:*)"
],
"deny": [],
"ask": []

View File

@ -553,7 +553,8 @@ export class KnowYourWorldValidator
// Determine re-ask position based on assistance level
// Guided/Helpful: re-ask soon (after 2-3 regions) to reinforce learning
// 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
// 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
className={css({
fontWeight: '500',
color: isChecked
? 'white'
: isDark
? 'gray.200'
: 'gray.700',
color: isChecked ? 'white' : isDark ? 'gray.200' : 'gray.700',
})}
>
{config.label}
@ -908,8 +904,7 @@ export function DrillDownMapSelector({
{peers.map((peer) => {
// Check if this is a planet (joke at world level)
const isPlanet = 'isPlanet' in peer && peer.isPlanet
const planetData =
'planetData' in peer ? (peer.planetData as PlanetData | null) : null
const planetData = 'planetData' in peer ? (peer.planetData as PlanetData | null) : null
// Calculate viewBox for this peer's continent (only for non-planets)
const peerContinentId = peer.path[0]
@ -1105,7 +1100,6 @@ export function DrillDownMapSelector({
})}
</div>
)}
</div>
)
}

View File

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

View File

@ -4,7 +4,7 @@ import { useCallback, useMemo } from 'react'
import { css } from '@styled/css'
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'
import { useKnowYourWorld } from '../Provider'
import { getFilteredMapDataSync } from '../maps'
import { getFilteredMapDataBySizesSync } from '../maps'
import { MapRenderer } from './MapRenderer'
import { GameInfoPanel } from './GameInfoPanel'
import { useViewerId } from '@/lib/arcade/game-sdk'
@ -39,10 +39,10 @@ export function PlayingPhase() {
[localPlayerId, viewerId, sendCursorUpdate]
)
const mapData = getFilteredMapDataSync(
const mapData = getFilteredMapDataBySizesSync(
state.selectedMap,
state.selectedContinent,
state.difficulty
state.includeSizes
)
const totalRegions = mapData.regions.length
const foundCount = state.regionsFound.length
@ -55,15 +55,22 @@ export function PlayingPhase() {
const currentRegionName = currentRegion?.name ?? 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) {
console.warn('[PlayingPhase] Prompt not in filtered regions - server/client filter mismatch:', {
const errorInfo = {
currentPrompt: state.currentPrompt,
difficulty: state.difficulty,
includeSizes: state.includeSizes,
selectedMap: state.selectedMap,
selectedContinent: state.selectedContinent,
clientFilteredCount: mapData.regions.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 (

View File

@ -226,7 +226,10 @@ export const ALL_REGION_SIZES: RegionSize[] = ['huge', 'large', 'medium', 'small
/**
* 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: {
label: 'Major',
emoji: '🌍',