From 9499e4e8b51ea48aac067473e36d469f62e2b9b1 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Wed, 26 Nov 2025 21:00:20 -0600 Subject: [PATCH] feat(know-your-world): separate region filtering from assistance level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the "difficulty" setting into two distinct concepts: - Region sizes: Checkbox selection for which regions to include (huge, large, medium, small, tiny) - Assistance level: Dropdown for gameplay features (hints, hot/cold feedback, etc.) Changes: - Add region size checkboxes with per-category counts in SetupPhase - Add assistance level dropdown with feature badges - Update Validator to handle new move types (SET_REGION_SIZES, SET_ASSISTANCE_LEVEL) - Show excluded regions as dimmed/grayed out on the setup map - Update MapRenderer to use assistanceLevel instead of difficulty - Update game-configs.ts with new default config fields πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../arcade-games/know-your-world/Provider.tsx | 91 +- .../arcade-games/know-your-world/Validator.ts | 139 ++- .../components/DrillDownMapSelector.tsx | 59 +- .../components/GameInfoPanel.tsx | 110 ++- .../components/MapRenderer.stories.tsx | 6 +- .../components/MapRenderer.tsx | 147 ++- .../components/MapSelectorMap.tsx | 70 +- .../components/PlayingPhase.tsx | 23 +- .../know-your-world/components/SetupPhase.tsx | 392 ++++++-- .../src/arcade-games/know-your-world/index.ts | 14 +- .../src/arcade-games/know-your-world/maps.ts | 842 +++++++++++++++++- .../src/arcade-games/know-your-world/types.ts | 52 +- .../utils/adaptiveZoomSearch.ts | 5 +- apps/web/src/lib/arcade/game-configs.ts | 3 +- 14 files changed, 1725 insertions(+), 228 deletions(-) diff --git a/apps/web/src/arcade-games/know-your-world/Provider.tsx b/apps/web/src/arcade-games/know-your-world/Provider.tsx index 10c8be07..bfc813d3 100644 --- a/apps/web/src/arcade-games/know-your-world/Provider.tsx +++ b/apps/web/src/arcade-games/know-your-world/Provider.tsx @@ -10,7 +10,8 @@ import { useViewerId, } from '@/lib/arcade/game-sdk' import { buildPlayerOwnershipFromRoomData } from '@/lib/arcade/player-ownership.client' -import type { KnowYourWorldState, KnowYourWorldMove } from './types' +import type { KnowYourWorldState, AssistanceLevel } from './types' +import type { RegionSize } from './maps' interface KnowYourWorldContextValue { state: KnowYourWorldState @@ -24,13 +25,15 @@ interface KnowYourWorldContextValue { nextRound: () => void endGame: () => void giveUp: () => void + requestHint: () => void endStudy: () => void returnToSetup: () => void // Setup actions setMap: (map: 'world' | 'usa') => void setMode: (mode: 'cooperative' | 'race' | 'turn-based') => void - setDifficulty: (difficulty: string) => void + setRegionSizes: (sizes: RegionSize[]) => void + setAssistanceLevel: (level: AssistanceLevel) => void setStudyDuration: (duration: 0 | 30 | 60 | 120) => void setContinent: (continent: import('./continents').ContinentId | 'all') => void @@ -94,11 +97,27 @@ export function KnowYourWorldProvider({ children }: { children: React.ReactNode ? (rawContinent as any) : 'all' + // Validate includeSizes - should be an array of valid size strings + const validSizes: RegionSize[] = ['huge', 'large', 'medium', 'small', 'tiny'] + const rawSizes = gameConfig?.includeSizes + const includeSizes: RegionSize[] = Array.isArray(rawSizes) + ? rawSizes.filter((s: string) => validSizes.includes(s as RegionSize)) + : ['huge', 'large', 'medium'] // Default to most regions + + // Validate assistanceLevel + const validAssistanceLevels = ['guided', 'helpful', 'standard', 'none'] + const rawAssistance = gameConfig?.assistanceLevel + const assistanceLevel: AssistanceLevel = + typeof rawAssistance === 'string' && validAssistanceLevels.includes(rawAssistance) + ? (rawAssistance as AssistanceLevel) + : 'helpful' + return { gamePhase: 'setup' as const, selectedMap: (gameConfig?.selectedMap as 'world' | 'usa') || 'world', gameMode: (gameConfig?.gameMode as 'cooperative' | 'race' | 'turn-based') || 'cooperative', - difficulty: gameConfig?.difficulty || 'medium', + includeSizes, + assistanceLevel, studyDuration, selectedContinent, studyTimeRemaining: 0, @@ -117,6 +136,8 @@ export function KnowYourWorldProvider({ children }: { children: React.ReactNode playerMetadata: {}, giveUpReveal: null, giveUpVotes: [], + hintsUsed: 0, + hintActive: null, } }, [roomData]) @@ -170,7 +191,8 @@ export function KnowYourWorldProvider({ children }: { children: React.ReactNode playerMetadata, selectedMap: state.selectedMap, gameMode: state.gameMode, - difficulty: state.difficulty, + includeSizes: state.includeSizes, + assistanceLevel: state.assistanceLevel, }, }) }, [ @@ -181,7 +203,8 @@ export function KnowYourWorldProvider({ children }: { children: React.ReactNode sendMove, state.selectedMap, state.gameMode, - state.difficulty, + state.includeSizes, + state.assistanceLevel, ]) // Action: Click Region @@ -241,6 +264,16 @@ export function KnowYourWorldProvider({ children }: { children: React.ReactNode }) }, [viewerId, sendMove, state.currentPlayer, activePlayers]) + // Action: Request Hint (highlight current region briefly) + const requestHint = useCallback(() => { + sendMove({ + type: 'REQUEST_HINT', + playerId: state.currentPlayer || activePlayers[0] || '', + userId: viewerId || '', + data: {}, + }) + }, [viewerId, sendMove, state.currentPlayer, activePlayers]) + // Setup Action: Set Map const setMap = useCallback( (selectedMap: 'world' | 'usa') => { @@ -301,14 +334,14 @@ export function KnowYourWorldProvider({ children }: { children: React.ReactNode [viewerId, sendMove, roomData, updateGameConfig, activePlayers] ) - // Setup Action: Set Difficulty - const setDifficulty = useCallback( - (difficulty: string) => { + // Setup Action: Set Region Sizes (which sizes to include) + const setRegionSizes = useCallback( + (includeSizes: RegionSize[]) => { sendMove({ - type: 'SET_DIFFICULTY', - playerId: activePlayers[0] || '', // Use first active player + type: 'SET_REGION_SIZES', + playerId: activePlayers[0] || '', userId: viewerId || '', - data: { difficulty }, + data: { includeSizes }, }) // Persist to database @@ -322,7 +355,37 @@ export function KnowYourWorldProvider({ children }: { children: React.ReactNode ...currentGameConfig, 'know-your-world': { ...currentConfig, - difficulty, + includeSizes, + }, + }, + }) + } + }, + [viewerId, sendMove, roomData, updateGameConfig, activePlayers] + ) + + // Setup Action: Set Assistance Level + const setAssistanceLevel = useCallback( + (assistanceLevel: AssistanceLevel) => { + sendMove({ + type: 'SET_ASSISTANCE_LEVEL', + playerId: activePlayers[0] || '', + userId: viewerId || '', + data: { assistanceLevel }, + }) + + // Persist to database + if (roomData?.id) { + const currentGameConfig = (roomData.gameConfig as Record) || {} + const currentConfig = (currentGameConfig['know-your-world'] as Record) || {} + + updateGameConfig({ + roomId: roomData.id, + gameConfig: { + ...currentGameConfig, + 'know-your-world': { + ...currentConfig, + assistanceLevel, }, }, }) @@ -431,11 +494,13 @@ export function KnowYourWorldProvider({ children }: { children: React.ReactNode nextRound, endGame, giveUp, + requestHint, endStudy, returnToSetup, setMap, setMode, - setDifficulty, + setRegionSizes, + setAssistanceLevel, setStudyDuration, setContinent, otherPlayerCursors, diff --git a/apps/web/src/arcade-games/know-your-world/Validator.ts b/apps/web/src/arcade-games/know-your-world/Validator.ts index b090bf3c..d0a725a9 100644 --- a/apps/web/src/arcade-games/know-your-world/Validator.ts +++ b/apps/web/src/arcade-games/know-your-world/Validator.ts @@ -4,17 +4,19 @@ import type { KnowYourWorldMove, KnowYourWorldState, GuessRecord, + AssistanceLevel, } from './types' +import type { RegionSize } from './maps' /** * Lazy-load map functions to avoid importing ES modules at module init time * This is critical for server-side usage where ES modules can't be required */ -async function getFilteredMapDataLazy( - ...args: Parameters +async function getFilteredMapDataBySizesLazy( + ...args: Parameters ) { - const { getFilteredMapData } = await import('./maps') - return getFilteredMapData(...args) + const { getFilteredMapDataBySizes } = await import('./maps') + return getFilteredMapDataBySizes(...args) } export class KnowYourWorldValidator @@ -41,14 +43,18 @@ export class KnowYourWorldValidator return this.validateSetMap(state, move.data.selectedMap) case 'SET_MODE': return this.validateSetMode(state, move.data.gameMode) - case 'SET_DIFFICULTY': - return this.validateSetDifficulty(state, move.data.difficulty) + case 'SET_REGION_SIZES': + return this.validateSetRegionSizes(state, move.data.includeSizes) + case 'SET_ASSISTANCE_LEVEL': + return this.validateSetAssistanceLevel(state, move.data.assistanceLevel) case 'SET_STUDY_DURATION': return this.validateSetStudyDuration(state, move.data.studyDuration) case 'SET_CONTINENT': return this.validateSetContinent(state, move.data.selectedContinent) case 'GIVE_UP': return await this.validateGiveUp(state, move.playerId, move.userId) + case 'REQUEST_HINT': + return this.validateRequestHint(state, move.playerId, move.timestamp) default: return { valid: false, error: 'Unknown move type' } } @@ -63,14 +69,19 @@ export class KnowYourWorldValidator return { valid: false, error: 'Can only start from setup phase' } } - const { activePlayers, playerMetadata, selectedMap, gameMode, difficulty } = data + const { activePlayers, playerMetadata, selectedMap, gameMode, includeSizes, assistanceLevel } = + data if (!activePlayers || activePlayers.length === 0) { return { valid: false, error: 'Need at least 1 player' } } - // Get map data and shuffle regions (with continent and difficulty filters) - const mapData = await getFilteredMapDataLazy(selectedMap, state.selectedContinent, difficulty) + // Get map data and shuffle regions (with continent and size filters) + const mapData = await getFilteredMapDataBySizesLazy( + selectedMap, + state.selectedContinent, + includeSizes || state.includeSizes + ) const regionIds = mapData.regions.map((r) => r.id) const shuffledRegions = this.shuffleArray([...regionIds]) @@ -96,7 +107,8 @@ export class KnowYourWorldValidator playerMetadata, selectedMap, gameMode, - difficulty, + includeSizes: includeSizes || state.includeSizes, + assistanceLevel: assistanceLevel || state.assistanceLevel, studyTimeRemaining: shouldStudy ? state.studyDuration : 0, studyStartTime: shouldStudy ? Date.now() : 0, currentPrompt: shouldStudy ? null : shuffledRegions[0], @@ -110,6 +122,8 @@ export class KnowYourWorldValidator startTime: Date.now(), giveUpReveal: null, giveUpVotes: [], + hintsUsed: 0, + hintActive: null, } return { valid: true, newState } @@ -179,6 +193,7 @@ export class KnowYourWorldValidator endTime: Date.now(), giveUpReveal: null, giveUpVotes: [], // Clear votes when game ends + hintActive: null, activeUserIds, } return { valid: true, newState } @@ -206,6 +221,7 @@ export class KnowYourWorldValidator guessHistory, giveUpReveal: null, giveUpVotes: [], // Clear votes when moving to next region + hintActive: null, // Clear hint when moving to next region activeUserIds, } @@ -236,7 +252,9 @@ export class KnowYourWorldValidator return { valid: true, newState, - error: `Incorrect! Try again. Looking for: ${state.currentPrompt}`, + // Error message includes clicked region name for client to format based on difficulty + // Format: "CLICKED:[regionName]" so client can parse and format appropriately + error: `CLICKED:${regionName}`, } } } @@ -255,11 +273,11 @@ export class KnowYourWorldValidator return { valid: false, error: 'Can only start next round from results' } } - // Get map data and shuffle regions (with continent and difficulty filters) - const mapData = await getFilteredMapDataLazy( + // Get map data and shuffle regions (with continent and size filters) + const mapData = await getFilteredMapDataBySizesLazy( state.selectedMap, state.selectedContinent, - state.difficulty + state.includeSizes ) const regionIds = mapData.regions.map((r) => r.id) const shuffledRegions = this.shuffleArray([...regionIds]) @@ -292,6 +310,8 @@ export class KnowYourWorldValidator endTime: undefined, giveUpReveal: null, giveUpVotes: [], + hintsUsed: 0, + hintActive: null, } return { valid: true, newState } @@ -340,14 +360,33 @@ export class KnowYourWorldValidator return { valid: true, newState } } - private validateSetDifficulty(state: KnowYourWorldState, difficulty: string): ValidationResult { + private validateSetRegionSizes( + state: KnowYourWorldState, + includeSizes: RegionSize[] + ): ValidationResult { if (state.gamePhase !== 'setup') { - return { valid: false, error: 'Can only change difficulty during setup' } + return { valid: false, error: 'Can only change region sizes during setup' } } const newState: KnowYourWorldState = { ...state, - difficulty, + includeSizes, + } + + return { valid: true, newState } + } + + private validateSetAssistanceLevel( + state: KnowYourWorldState, + assistanceLevel: AssistanceLevel + ): ValidationResult { + if (state.gamePhase !== 'setup') { + return { valid: false, error: 'Can only change assistance level during setup' } + } + + const newState: KnowYourWorldState = { + ...state, + assistanceLevel, } return { valid: true, newState } @@ -493,10 +532,10 @@ export class KnowYourWorldValidator activeUserIds: string[] ): Promise { // Get region info for the reveal - const mapData = await getFilteredMapDataLazy( + const mapData = await getFilteredMapDataBySizesLazy( state.selectedMap, state.selectedContinent, - state.difficulty + state.includeSizes ) const region = mapData.regions.find((r) => r.id === state.currentPrompt) @@ -511,10 +550,11 @@ export class KnowYourWorldValidator ? existingGivenUp : [...existingGivenUp, region.id] - // Determine re-ask position based on difficulty - // Easy: re-ask soon (after 2-3 regions) - // Hard: re-ask at the end - const reaskDelay = state.difficulty === 'easy' ? 3 : state.regionsToFind.length + // 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 reaskDelay = isHighAssistance ? 3 : state.regionsToFind.length // Build new regions queue: take next regions, then insert given-up region at appropriate position const remainingRegions = [...state.regionsToFind] @@ -547,12 +587,44 @@ export class KnowYourWorldValidator timestamp: Date.now(), }, giveUpVotes: [], // Clear votes after give up is executed + hintActive: null, // Clear hint when moving to next region activeUserIds, } return { valid: true, newState } } + private validateRequestHint( + state: KnowYourWorldState, + playerId: string, + timestamp: number + ): ValidationResult { + if (state.gamePhase !== 'playing') { + return { valid: false, error: 'Can only request hints during playing phase' } + } + + if (!state.currentPrompt) { + return { valid: false, error: 'No region to hint' } + } + + // For turn-based: check if it's this player's turn + if (state.gameMode === 'turn-based' && state.currentPlayer !== playerId) { + return { valid: false, error: 'Not your turn' } + } + + // Set hint active with current region + const newState: KnowYourWorldState = { + ...state, + hintsUsed: (state.hintsUsed ?? 0) + 1, + hintActive: { + regionId: state.currentPrompt, + timestamp, + }, + } + + return { valid: true, newState } + } + isGameComplete(state: KnowYourWorldState): boolean { return state.gamePhase === 'results' } @@ -560,11 +632,28 @@ export class KnowYourWorldValidator getInitialState(config: unknown): KnowYourWorldState { const typedConfig = config as KnowYourWorldConfig + // Validate includeSizes - should be an array of valid size strings + const validSizes: RegionSize[] = ['huge', 'large', 'medium', 'small', 'tiny'] + const rawSizes = typedConfig?.includeSizes + const includeSizes: RegionSize[] = Array.isArray(rawSizes) + ? rawSizes.filter((s: string) => validSizes.includes(s as RegionSize)) + : ['huge', 'large', 'medium'] // Default + + // Validate assistanceLevel + const validAssistanceLevels: AssistanceLevel[] = ['guided', 'helpful', 'standard', 'none'] + const rawAssistance = typedConfig?.assistanceLevel + const assistanceLevel: AssistanceLevel = validAssistanceLevels.includes( + rawAssistance as AssistanceLevel + ) + ? (rawAssistance as AssistanceLevel) + : 'helpful' // Default + return { gamePhase: 'setup', selectedMap: typedConfig?.selectedMap || 'world', gameMode: typedConfig?.gameMode || 'cooperative', - difficulty: typedConfig?.difficulty || 'easy', + includeSizes, + assistanceLevel, studyDuration: typedConfig?.studyDuration || 0, selectedContinent: typedConfig?.selectedContinent || 'all', studyTimeRemaining: 0, @@ -583,6 +672,8 @@ export class KnowYourWorldValidator playerMetadata: {}, giveUpReveal: null, giveUpVotes: [], + hintsUsed: 0, + hintActive: null, } } diff --git a/apps/web/src/arcade-games/know-your-world/components/DrillDownMapSelector.tsx b/apps/web/src/arcade-games/know-your-world/components/DrillDownMapSelector.tsx index 672a7acf..03b4cd2d 100644 --- a/apps/web/src/arcade-games/know-your-world/components/DrillDownMapSelector.tsx +++ b/apps/web/src/arcade-games/know-your-world/components/DrillDownMapSelector.tsx @@ -14,7 +14,9 @@ import { getSubMapsForContinent, parseViewBox, calculateFitCropViewBox, + getFilteredMapDataBySizesSync, } from '../maps' +import type { RegionSize } from '../maps' import { CONTINENTS, getContinentForCountry, @@ -30,17 +32,29 @@ import { */ export type SelectionPath = [] | [ContinentId] | [ContinentId, string] +/** + * Planet data type for joke placeholder + */ +interface PlanetData { + id: string + name: string + color: string + size: number + hasStripes?: boolean + hasRings?: boolean +} + /** * Joke placeholder: Other planets for when viewing Earth (world map) * Just for fun - these don't actually do anything */ -const PLANETS = [ +const PLANETS: PlanetData[] = [ { id: 'mercury', name: 'Mercury', color: '#b0b0b0', size: 0.38 }, { id: 'venus', name: 'Venus', color: '#e6c87a', size: 0.95 }, { id: 'mars', name: 'Mars', color: '#c1440e', size: 0.53 }, { id: 'jupiter', name: 'Jupiter', color: '#d8ca9d', size: 2.0, hasStripes: true }, { id: 'saturn', name: 'Saturn', color: '#ead6b8', size: 1.7, hasRings: true }, -] as const +] interface DrillDownMapSelectorProps { /** Callback when selection changes (map/continent for game start) */ @@ -51,6 +65,8 @@ interface DrillDownMapSelectorProps { selectedMap: 'world' | 'usa' /** Current selected continent (for initial state sync) */ selectedContinent: ContinentId | 'all' + /** Region sizes to include (for showing excluded regions dimmed) */ + includeSizes: RegionSize[] } interface BreadcrumbItem { @@ -65,6 +81,7 @@ export function DrillDownMapSelector({ onStartGame, selectedMap, selectedContinent, + includeSizes, }: DrillDownMapSelectorProps) { const { resolvedTheme } = useTheme() const isDark = resolvedTheme === 'dark' @@ -216,6 +233,40 @@ export function DrillDownMapSelector({ return groups }, [currentLevel]) + // Calculate excluded regions based on includeSizes + // These are regions that exist but are filtered out by size settings + const excludedRegions = useMemo(() => { + // Determine which map we're looking at + const mapId = currentLevel === 2 && path[1] ? 'usa' : 'world' + const continentId: ContinentId | 'all' = + currentLevel >= 1 && path[0] ? path[0] : selectedContinent + + // Get all regions (unfiltered by size) + const allRegionsMapData = getFilteredMapDataBySizesSync( + mapId as 'world' | 'usa', + continentId, + ['huge', 'large', 'medium', 'small', 'tiny'] // All sizes + ) + const allRegionIds = new Set(allRegionsMapData.regions.map((r) => r.id)) + + // Get filtered regions (based on current includeSizes) + const filteredMapData = getFilteredMapDataBySizesSync( + mapId as 'world' | 'usa', + continentId, + includeSizes + ) + const filteredRegionIds = new Set(filteredMapData.regions.map((r) => r.id)) + + // Excluded = all regions minus filtered regions + const excluded: string[] = [] + for (const regionId of allRegionIds) { + if (!filteredRegionIds.has(regionId)) { + excluded.push(regionId) + } + } + return excluded + }, [currentLevel, path, selectedContinent, includeSizes]) + // Compute the label to display for the hovered region // Shows the next drill-down level name, not the individual region name const hoveredLabel = useMemo(() => { @@ -562,6 +613,7 @@ export function DrillDownMapSelector({ currentLevel === 0 && selectedContinent !== 'all' ? selectedContinent : null } hoverableRegions={currentLevel === 1 ? highlightedRegions : undefined} + excludedRegions={excludedRegions} /> {/* Zoom Out Button - positioned inside map, upper right */} @@ -679,7 +731,8 @@ 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 : 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] diff --git a/apps/web/src/arcade-games/know-your-world/components/GameInfoPanel.tsx b/apps/web/src/arcade-games/know-your-world/components/GameInfoPanel.tsx index 3cc03093..35677e12 100644 --- a/apps/web/src/arcade-games/know-your-world/components/GameInfoPanel.tsx +++ b/apps/web/src/arcade-games/know-your-world/components/GameInfoPanel.tsx @@ -1,11 +1,11 @@ 'use client' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useState, useMemo } from 'react' import { css } from '@styled/css' import { useTheme } from '@/contexts/ThemeContext' import { useKnowYourWorld } from '../Provider' import type { MapData } from '../types' -import { getCountryFlagEmoji } from '../maps' +import { getCountryFlagEmoji, WORLD_MAP, USA_MAP, DEFAULT_DIFFICULTY_CONFIG } from '../maps' // Animation duration in ms - must match MapRenderer const GIVE_UP_ANIMATION_DURATION = 2000 @@ -34,11 +34,62 @@ export function GameInfoPanel({ selectedMap === 'world' && currentRegionId ? getCountryFlagEmoji(currentRegionId) : '' const { resolvedTheme } = useTheme() const isDark = resolvedTheme === 'dark' - const { state, lastError, clearError, giveUp } = useKnowYourWorld() + const { state, lastError, clearError, giveUp, requestHint } = useKnowYourWorld() + + // Get current difficulty level config + const currentDifficultyLevel = useMemo(() => { + const mapDiffConfig = + (selectedMap === 'world' ? WORLD_MAP : USA_MAP).difficultyConfig || DEFAULT_DIFFICULTY_CONFIG + return ( + mapDiffConfig.levels.find((level) => level.id === state.difficulty) || mapDiffConfig.levels[0] + ) + }, [selectedMap, state.difficulty]) + + // Parse error message and format based on difficulty config + const formattedError = useMemo(() => { + if (!lastError) return null + + // Check for "CLICKED:" prefix which indicates a wrong click + if (lastError.startsWith('CLICKED:')) { + const regionName = lastError.slice('CLICKED:'.length) + if (currentDifficultyLevel?.wrongClickShowsName) { + return `That was ${regionName}` + } + return null // Just show "Wrong!" without region name + } + + // Other errors pass through as-is + return lastError + }, [lastError, currentDifficultyLevel]) // Track if animation is in progress (local state based on timestamp) const [isAnimating, setIsAnimating] = useState(false) + // Determine if hints are available based on difficulty config + const hintsAvailable = useMemo(() => { + const hintsMode = currentDifficultyLevel?.hintsMode + if (hintsMode === 'none') return false + if (hintsMode === 'limited') { + const limit = currentDifficultyLevel?.hintLimit ?? 0 + return (state.hintsUsed ?? 0) < limit + } + return hintsMode === 'onRequest' + }, [currentDifficultyLevel, state.hintsUsed]) + + // Calculate remaining hints for limited mode + const remainingHints = useMemo(() => { + if (currentDifficultyLevel?.hintsMode !== 'limited') return null + const limit = currentDifficultyLevel?.hintLimit ?? 0 + return Math.max(0, limit - (state.hintsUsed ?? 0)) + }, [currentDifficultyLevel, state.hintsUsed]) + + // Handle hint request + const handleHint = useCallback(() => { + if (hintsAvailable && state.gamePhase === 'playing' && !isAnimating) { + requestHint() + } + }, [hintsAvailable, state.gamePhase, isAnimating, requestHint]) + // Check if animation is in progress based on timestamp useEffect(() => { if (!state.giveUpReveal?.timestamp) { @@ -188,6 +239,51 @@ export function GameInfoPanel({ {foundCount}/{totalRegions} + + {/* Hint button - only show if hints are enabled */} + {currentDifficultyLevel?.hintsMode !== 'none' && ( + + )} {/* Error Display - only shows when error exists */} @@ -208,7 +304,9 @@ export function GameInfoPanel({ })} > ⚠️ -
Wrong! {lastError}
+
+ Wrong!{formattedError ? ` ${formattedError}` : ''} +