feat: add continent filtering to Know Your World game
Add continent-based filtering for the world map to make the game more playable by reducing visual clutter: Continent Selection: - Add continent selector to setup screen (only for world map) - 7 continents + "All" option: Africa, Asia, Europe, North America, South America, Oceania, Antarctica - Each continent button shows emoji and name Map Filtering: - Filter world map regions by selected continent using ISO 3166-1 country codes - Calculate bounding box for filtered regions - Automatically crop and scale viewBox to show only selected continent - Add 10% padding around continent bounding box for better visibility Technical Implementation: - Create continents.ts with comprehensive country-to-continent mappings (256 countries) - Add getFilteredMapData() function to filter regions and adjust viewBox - Add calculateBoundingBox() to compute min/max coordinates from SVG paths - Add selectedContinent field to game state and config (persisted to database) - Add SET_CONTINENT move type and validator - Update all map rendering components (StudyPhase, PlayingPhase, MapRenderer) Benefits: - Solves "too many small countries" problem on world map - Allows focused study of specific geographic regions - Dynamic viewBox adjustment provides optimal zoom level - Maintains full world map option for comprehensive play 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
25e24a7cbc
commit
7bb03b8409
|
|
@ -30,6 +30,7 @@ interface KnowYourWorldContextValue {
|
|||
setMode: (mode: 'cooperative' | 'race' | 'turn-based') => void
|
||||
setDifficulty: (difficulty: 'easy' | 'hard') => void
|
||||
setStudyDuration: (duration: 0 | 30 | 60 | 120) => void
|
||||
setContinent: (continent: import('./continents').ContinentId | 'all') => void
|
||||
}
|
||||
|
||||
const KnowYourWorldContext = createContext<KnowYourWorldContextValue | null>(null)
|
||||
|
|
@ -59,12 +60,30 @@ export function KnowYourWorldProvider({ children }: { children: React.ReactNode
|
|||
const studyDuration: 0 | 30 | 60 | 120 =
|
||||
rawDuration === 30 || rawDuration === 60 || rawDuration === 120 ? rawDuration : 0
|
||||
|
||||
// Validate selectedContinent
|
||||
const rawContinent = gameConfig?.selectedContinent
|
||||
const validContinents = [
|
||||
'all',
|
||||
'africa',
|
||||
'asia',
|
||||
'europe',
|
||||
'north-america',
|
||||
'south-america',
|
||||
'oceania',
|
||||
'antarctica',
|
||||
]
|
||||
const selectedContinent: import('./continents').ContinentId | 'all' =
|
||||
typeof rawContinent === 'string' && validContinents.includes(rawContinent)
|
||||
? (rawContinent as any)
|
||||
: 'all'
|
||||
|
||||
return {
|
||||
gamePhase: 'setup' as const,
|
||||
selectedMap: (gameConfig?.selectedMap as 'world' | 'usa') || 'world',
|
||||
gameMode: (gameConfig?.gameMode as 'cooperative' | 'race' | 'turn-based') || 'cooperative',
|
||||
difficulty: (gameConfig?.difficulty as 'easy' | 'hard') || 'easy',
|
||||
studyDuration,
|
||||
selectedContinent,
|
||||
studyTimeRemaining: 0,
|
||||
studyStartTime: 0,
|
||||
currentPrompt: null,
|
||||
|
|
@ -277,6 +296,36 @@ export function KnowYourWorldProvider({ children }: { children: React.ReactNode
|
|||
})
|
||||
}, [viewerId, sendMove])
|
||||
|
||||
// Setup Action: Set Continent
|
||||
const setContinent = useCallback(
|
||||
(selectedContinent: import('./continents').ContinentId | 'all') => {
|
||||
sendMove({
|
||||
type: 'SET_CONTINENT',
|
||||
playerId: viewerId || 'player-1',
|
||||
userId: viewerId || '',
|
||||
data: { selectedContinent },
|
||||
})
|
||||
|
||||
// Persist to database
|
||||
if (roomData?.id) {
|
||||
const currentGameConfig = (roomData.gameConfig as Record<string, any>) || {}
|
||||
const currentConfig = (currentGameConfig['know-your-world'] as Record<string, any>) || {}
|
||||
|
||||
updateGameConfig({
|
||||
roomId: roomData.id,
|
||||
gameConfig: {
|
||||
...currentGameConfig,
|
||||
'know-your-world': {
|
||||
...currentConfig,
|
||||
selectedContinent,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
[viewerId, sendMove, roomData, updateGameConfig]
|
||||
)
|
||||
|
||||
// Action: Return to Setup
|
||||
const returnToSetup = useCallback(() => {
|
||||
sendMove({
|
||||
|
|
@ -304,6 +353,7 @@ export function KnowYourWorldProvider({ children }: { children: React.ReactNode
|
|||
setMode,
|
||||
setDifficulty,
|
||||
setStudyDuration,
|
||||
setContinent,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import type {
|
|||
KnowYourWorldState,
|
||||
GuessRecord,
|
||||
} from './types'
|
||||
import { getMapData } from './maps'
|
||||
import { getMapData, getFilteredMapData } from './maps'
|
||||
|
||||
export class KnowYourWorldValidator
|
||||
implements GameValidator<KnowYourWorldState, KnowYourWorldMove>
|
||||
|
|
@ -32,6 +32,8 @@ export class KnowYourWorldValidator
|
|||
return this.validateSetDifficulty(state, move.data.difficulty)
|
||||
case 'SET_STUDY_DURATION':
|
||||
return this.validateSetStudyDuration(state, move.data.studyDuration)
|
||||
case 'SET_CONTINENT':
|
||||
return this.validateSetContinent(state, move.data.selectedContinent)
|
||||
default:
|
||||
return { valid: false, error: 'Unknown move type' }
|
||||
}
|
||||
|
|
@ -56,10 +58,11 @@ export class KnowYourWorldValidator
|
|||
return { valid: false, error: 'Need at least 1 player' }
|
||||
}
|
||||
|
||||
// Get map data and shuffle regions
|
||||
const mapData = getMapData(selectedMap)
|
||||
// Get map data and shuffle regions (with continent filter if applicable)
|
||||
const mapData = getFilteredMapData(selectedMap, state.selectedContinent)
|
||||
console.log('[KnowYourWorld Validator] Map data loaded:', {
|
||||
map: mapData.id,
|
||||
continent: state.selectedContinent,
|
||||
regionsCount: mapData.regions.length,
|
||||
})
|
||||
const regionIds = mapData.regions.map((r) => r.id)
|
||||
|
|
@ -220,8 +223,8 @@ export class KnowYourWorldValidator
|
|||
return { valid: false, error: 'Can only start next round from results' }
|
||||
}
|
||||
|
||||
// Get map data and shuffle regions
|
||||
const mapData = getMapData(state.selectedMap)
|
||||
// Get map data and shuffle regions (with continent filter if applicable)
|
||||
const mapData = getFilteredMapData(state.selectedMap, state.selectedContinent)
|
||||
const regionIds = mapData.regions.map((r) => r.id)
|
||||
const shuffledRegions = this.shuffleArray([...regionIds])
|
||||
|
||||
|
|
@ -330,6 +333,22 @@ export class KnowYourWorldValidator
|
|||
return { valid: true, newState }
|
||||
}
|
||||
|
||||
private validateSetContinent(
|
||||
state: KnowYourWorldState,
|
||||
selectedContinent: import('./continents').ContinentId | 'all'
|
||||
): ValidationResult {
|
||||
if (state.gamePhase !== 'setup') {
|
||||
return { valid: false, error: 'Can only change continent during setup' }
|
||||
}
|
||||
|
||||
const newState: KnowYourWorldState = {
|
||||
...state,
|
||||
selectedContinent,
|
||||
}
|
||||
|
||||
return { valid: true, newState }
|
||||
}
|
||||
|
||||
private validateEndStudy(state: KnowYourWorldState): ValidationResult {
|
||||
if (state.gamePhase !== 'studying') {
|
||||
return { valid: false, error: 'Can only end study during studying phase' }
|
||||
|
|
@ -389,6 +408,7 @@ export class KnowYourWorldValidator
|
|||
gameMode: typedConfig?.gameMode || 'cooperative',
|
||||
difficulty: typedConfig?.difficulty || 'easy',
|
||||
studyDuration: typedConfig?.studyDuration || 0,
|
||||
selectedContinent: typedConfig?.selectedContinent || 'all',
|
||||
studyTimeRemaining: 0,
|
||||
studyStartTime: 0,
|
||||
currentPrompt: null,
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ function isSmallRegion(bbox: BoundingBox, viewBox: string): boolean {
|
|||
// Thresholds (relative to map size)
|
||||
const minWidth = mapWidth * 0.025 // 2.5% of map width
|
||||
const minHeight = mapHeight * 0.025 // 2.5% of map height
|
||||
const minArea = (mapWidth * mapHeight) * 0.001 // 0.1% of total map area
|
||||
const minArea = mapWidth * mapHeight * 0.001 // 0.1% of total map area
|
||||
|
||||
return bbox.width < minWidth || bbox.height < minHeight || bbox.area < minArea
|
||||
}
|
||||
|
|
@ -116,10 +116,7 @@ export function MapRenderer({
|
|||
regionId: region.id,
|
||||
regionName: region.name,
|
||||
regionCenter: region.center,
|
||||
labelPosition: [
|
||||
region.center[0] + offsetX,
|
||||
region.center[1] + offsetY,
|
||||
],
|
||||
labelPosition: [region.center[0] + offsetX, region.center[1] + offsetY],
|
||||
isFound: regionsFound.includes(region.id),
|
||||
})
|
||||
}
|
||||
|
|
@ -229,8 +226,10 @@ export function MapRenderer({
|
|||
y={label.labelPosition[1] - 12}
|
||||
width={label.regionName.length * 6 + 10}
|
||||
height={20}
|
||||
fill={label.isFound ? (isDark ? '#22c55e' : '#86efac') : (isDark ? '#1f2937' : '#ffffff')}
|
||||
stroke={label.isFound ? '#16a34a' : (isDark ? '#60a5fa' : '#3b82f6')}
|
||||
fill={
|
||||
label.isFound ? (isDark ? '#22c55e' : '#86efac') : isDark ? '#1f2937' : '#ffffff'
|
||||
}
|
||||
stroke={label.isFound ? '#16a34a' : isDark ? '#60a5fa' : '#3b82f6'}
|
||||
strokeWidth={2}
|
||||
rx={4}
|
||||
style={{
|
||||
|
|
@ -269,22 +268,11 @@ export function MapRenderer({
|
|||
|
||||
{/* Arrow marker definition */}
|
||||
<defs>
|
||||
<marker
|
||||
id="arrowhead"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="8"
|
||||
refY="3"
|
||||
orient="auto"
|
||||
>
|
||||
<polygon
|
||||
points="0 0, 10 3, 0 6"
|
||||
fill={isDark ? '#60a5fa' : '#3b82f6'}
|
||||
/>
|
||||
<marker id="arrowhead" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
|
||||
<polygon points="0 0, 10 3, 0 6" fill={isDark ? '#60a5fa' : '#3b82f6'} />
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useEffect } from 'react'
|
|||
import { css } from '@styled/css'
|
||||
import { useTheme } from '@/contexts/ThemeContext'
|
||||
import { useKnowYourWorld } from '../Provider'
|
||||
import { getMapData } from '../maps'
|
||||
import { getFilteredMapData } from '../maps'
|
||||
import { MapRenderer } from './MapRenderer'
|
||||
|
||||
export function PlayingPhase() {
|
||||
|
|
@ -12,7 +12,7 @@ export function PlayingPhase() {
|
|||
const isDark = resolvedTheme === 'dark'
|
||||
const { state, clickRegion, lastError, clearError } = useKnowYourWorld()
|
||||
|
||||
const mapData = getMapData(state.selectedMap)
|
||||
const mapData = getFilteredMapData(state.selectedMap, state.selectedContinent)
|
||||
const totalRegions = mapData.regions.length
|
||||
const foundCount = state.regionsFound.length
|
||||
const progress = (foundCount / totalRegions) * 100
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@
|
|||
import { css } from '@styled/css'
|
||||
import { useTheme } from '@/contexts/ThemeContext'
|
||||
import { useKnowYourWorld } from '../Provider'
|
||||
import { CONTINENTS } from '../continents'
|
||||
|
||||
export function SetupPhase() {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const isDark = resolvedTheme === 'dark'
|
||||
const { state, startGame, setMap, setMode, setDifficulty, setStudyDuration } = useKnowYourWorld()
|
||||
const { state, startGame, setMap, setMode, setDifficulty, setStudyDuration, setContinent } =
|
||||
useKnowYourWorld()
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -108,6 +110,94 @@ export function SetupPhase() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Continent Selection (only for World map) */}
|
||||
{state.selectedMap === 'world' && (
|
||||
<div data-section="continent-selection">
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: '2xl',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '4',
|
||||
color: isDark ? 'gray.100' : 'gray.900',
|
||||
})}
|
||||
>
|
||||
Focus on Continent 🌐
|
||||
</h2>
|
||||
<div
|
||||
className={css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||
gap: '3',
|
||||
})}
|
||||
>
|
||||
{/* All continents option */}
|
||||
<button
|
||||
data-action="select-all-continents"
|
||||
onClick={() => setContinent('all')}
|
||||
className={css({
|
||||
padding: '3',
|
||||
rounded: 'lg',
|
||||
border: '2px solid',
|
||||
borderColor: state.selectedContinent === 'all' ? 'blue.500' : 'transparent',
|
||||
bg:
|
||||
state.selectedContinent === 'all'
|
||||
? isDark
|
||||
? 'blue.900'
|
||||
: 'blue.50'
|
||||
: isDark
|
||||
? 'gray.800'
|
||||
: 'gray.100',
|
||||
color: isDark ? 'gray.100' : 'gray.900',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
_hover: {
|
||||
borderColor: 'blue.400',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div className={css({ fontSize: '2xl', marginBottom: '1' })}>🌍</div>
|
||||
<div className={css({ fontSize: 'sm', fontWeight: 'bold' })}>All</div>
|
||||
<div className={css({ fontSize: '2xs', color: isDark ? 'gray.400' : 'gray.600' })}>
|
||||
Whole world
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Individual continents */}
|
||||
{CONTINENTS.map((continent) => (
|
||||
<button
|
||||
key={continent.id}
|
||||
data-action={`select-${continent.id}-continent`}
|
||||
onClick={() => setContinent(continent.id)}
|
||||
className={css({
|
||||
padding: '3',
|
||||
rounded: 'lg',
|
||||
border: '2px solid',
|
||||
borderColor:
|
||||
state.selectedContinent === continent.id ? 'blue.500' : 'transparent',
|
||||
bg:
|
||||
state.selectedContinent === continent.id
|
||||
? isDark
|
||||
? 'blue.900'
|
||||
: 'blue.50'
|
||||
: isDark
|
||||
? 'gray.800'
|
||||
: 'gray.100',
|
||||
color: isDark ? 'gray.100' : 'gray.900',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
_hover: {
|
||||
borderColor: 'blue.400',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div className={css({ fontSize: '2xl', marginBottom: '1' })}>{continent.emoji}</div>
|
||||
<div className={css({ fontSize: 'sm', fontWeight: 'bold' })}>{continent.name}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mode Selection */}
|
||||
<div data-section="mode-selection">
|
||||
<h2
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'
|
|||
import { css } from '@styled/css'
|
||||
import { useTheme } from '@/contexts/ThemeContext'
|
||||
import { useKnowYourWorld } from '../Provider'
|
||||
import { getMapData } from '../maps'
|
||||
import { getFilteredMapData } from '../maps'
|
||||
import type { MapRegion } from '../types'
|
||||
import { getRegionColor, getLabelTextColor, getLabelTextShadow } from '../mapColors'
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ export function StudyPhase() {
|
|||
|
||||
const [timeRemaining, setTimeRemaining] = useState(state.studyTimeRemaining)
|
||||
|
||||
const mapData = getMapData(state.selectedMap)
|
||||
const mapData = getFilteredMapData(state.selectedMap, state.selectedContinent)
|
||||
|
||||
// Countdown timer
|
||||
useEffect(() => {
|
||||
|
|
@ -102,7 +102,14 @@ export function StudyPhase() {
|
|||
className={css({
|
||||
fontSize: '4xl',
|
||||
fontWeight: 'bold',
|
||||
color: timeRemaining <= 10 ? (isDark ? 'red.400' : 'red.600') : isDark ? 'blue.200' : 'blue.800',
|
||||
color:
|
||||
timeRemaining <= 10
|
||||
? isDark
|
||||
? 'red.400'
|
||||
: 'red.600'
|
||||
: isDark
|
||||
? 'blue.200'
|
||||
: 'blue.800',
|
||||
fontFeatureSettings: '"tnum"',
|
||||
})}
|
||||
>
|
||||
|
|
@ -200,7 +207,15 @@ export function StudyPhase() {
|
|||
})}
|
||||
>
|
||||
<p className={css({ fontWeight: 'semibold', marginBottom: '2' })}>💡 Study Tips:</p>
|
||||
<ul className={css({ listStyle: 'disc', paddingLeft: '5', display: 'flex', flexDirection: 'column', gap: '1' })}>
|
||||
<ul
|
||||
className={css({
|
||||
listStyle: 'disc',
|
||||
paddingLeft: '5',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1',
|
||||
})}
|
||||
>
|
||||
<li>Look for patterns - neighboring regions, shapes, sizes</li>
|
||||
<li>Group regions mentally by area (e.g., Northeast, Southwest)</li>
|
||||
<li>Focus on the tricky small ones that are hard to see</li>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,309 @@
|
|||
/**
|
||||
* Continent mappings for world countries
|
||||
* Maps ISO 3166-1 alpha-2 country codes to continents
|
||||
*/
|
||||
|
||||
export type ContinentId =
|
||||
| 'africa'
|
||||
| 'asia'
|
||||
| 'europe'
|
||||
| 'north-america'
|
||||
| 'south-america'
|
||||
| 'oceania'
|
||||
| 'antarctica'
|
||||
|
||||
export interface Continent {
|
||||
id: ContinentId
|
||||
name: string
|
||||
emoji: string
|
||||
}
|
||||
|
||||
export const CONTINENTS: Continent[] = [
|
||||
{ id: 'africa', name: 'Africa', emoji: '🌍' },
|
||||
{ id: 'asia', name: 'Asia', emoji: '🌏' },
|
||||
{ id: 'europe', name: 'Europe', emoji: '🇪🇺' },
|
||||
{ id: 'north-america', name: 'North America', emoji: '🌎' },
|
||||
{ id: 'south-america', name: 'South America', emoji: '🌎' },
|
||||
{ id: 'oceania', name: 'Oceania', emoji: '🌏' },
|
||||
{ id: 'antarctica', name: 'Antarctica', emoji: '🇦🇶' },
|
||||
]
|
||||
|
||||
/**
|
||||
* Map of country codes to continents
|
||||
* Based on ISO 3166-1 alpha-2 codes from @svg-maps/world
|
||||
*/
|
||||
export const COUNTRY_TO_CONTINENT: Record<string, ContinentId> = {
|
||||
// Africa
|
||||
dz: 'africa', // Algeria
|
||||
ao: 'africa', // Angola
|
||||
bj: 'africa', // Benin
|
||||
bw: 'africa', // Botswana
|
||||
bf: 'africa', // Burkina Faso
|
||||
bi: 'africa', // Burundi
|
||||
cm: 'africa', // Cameroon
|
||||
cv: 'africa', // Cape Verde
|
||||
cf: 'africa', // Central African Republic
|
||||
td: 'africa', // Chad
|
||||
km: 'africa', // Comoros
|
||||
cg: 'africa', // Congo (Brazzaville)
|
||||
cd: 'africa', // Congo (Kinshasa)
|
||||
ci: 'africa', // Côte d'Ivoire
|
||||
dj: 'africa', // Djibouti
|
||||
eg: 'africa', // Egypt
|
||||
gq: 'africa', // Equatorial Guinea
|
||||
er: 'africa', // Eritrea
|
||||
sz: 'africa', // Eswatini
|
||||
et: 'africa', // Ethiopia
|
||||
ga: 'africa', // Gabon
|
||||
gm: 'africa', // Gambia
|
||||
gh: 'africa', // Ghana
|
||||
gn: 'africa', // Guinea
|
||||
gw: 'africa', // Guinea-Bissau
|
||||
ke: 'africa', // Kenya
|
||||
ls: 'africa', // Lesotho
|
||||
lr: 'africa', // Liberia
|
||||
ly: 'africa', // Libya
|
||||
mg: 'africa', // Madagascar
|
||||
mw: 'africa', // Malawi
|
||||
ml: 'africa', // Mali
|
||||
mr: 'africa', // Mauritania
|
||||
mu: 'africa', // Mauritius
|
||||
yt: 'africa', // Mayotte
|
||||
ma: 'africa', // Morocco
|
||||
mz: 'africa', // Mozambique
|
||||
na: 'africa', // Namibia
|
||||
ne: 'africa', // Niger
|
||||
ng: 'africa', // Nigeria
|
||||
re: 'africa', // Réunion
|
||||
rw: 'africa', // Rwanda
|
||||
st: 'africa', // São Tomé and Príncipe
|
||||
sn: 'africa', // Senegal
|
||||
sc: 'africa', // Seychelles
|
||||
sl: 'africa', // Sierra Leone
|
||||
so: 'africa', // Somalia
|
||||
za: 'africa', // South Africa
|
||||
ss: 'africa', // South Sudan
|
||||
sd: 'africa', // Sudan
|
||||
tz: 'africa', // Tanzania
|
||||
tg: 'africa', // Togo
|
||||
tn: 'africa', // Tunisia
|
||||
ug: 'africa', // Uganda
|
||||
eh: 'africa', // Western Sahara
|
||||
zm: 'africa', // Zambia
|
||||
zw: 'africa', // Zimbabwe
|
||||
|
||||
// Asia
|
||||
af: 'asia', // Afghanistan
|
||||
am: 'asia', // Armenia
|
||||
az: 'asia', // Azerbaijan
|
||||
bh: 'asia', // Bahrain
|
||||
bd: 'asia', // Bangladesh
|
||||
bt: 'asia', // Bhutan
|
||||
bn: 'asia', // Brunei
|
||||
kh: 'asia', // Cambodia
|
||||
cn: 'asia', // China
|
||||
cx: 'asia', // Christmas Island
|
||||
cc: 'asia', // Cocos Islands
|
||||
ge: 'asia', // Georgia
|
||||
hk: 'asia', // Hong Kong
|
||||
in: 'asia', // India
|
||||
id: 'asia', // Indonesia
|
||||
ir: 'asia', // Iran
|
||||
iq: 'asia', // Iraq
|
||||
il: 'asia', // Israel
|
||||
jp: 'asia', // Japan
|
||||
jo: 'asia', // Jordan
|
||||
kz: 'asia', // Kazakhstan
|
||||
kp: 'asia', // North Korea
|
||||
kr: 'asia', // South Korea
|
||||
kw: 'asia', // Kuwait
|
||||
kg: 'asia', // Kyrgyzstan
|
||||
la: 'asia', // Laos
|
||||
lb: 'asia', // Lebanon
|
||||
mo: 'asia', // Macau
|
||||
my: 'asia', // Malaysia
|
||||
mv: 'asia', // Maldives
|
||||
mn: 'asia', // Mongolia
|
||||
mm: 'asia', // Myanmar
|
||||
np: 'asia', // Nepal
|
||||
om: 'asia', // Oman
|
||||
pk: 'asia', // Pakistan
|
||||
ps: 'asia', // Palestine
|
||||
ph: 'asia', // Philippines
|
||||
qa: 'asia', // Qatar
|
||||
sa: 'asia', // Saudi Arabia
|
||||
sg: 'asia', // Singapore
|
||||
lk: 'asia', // Sri Lanka
|
||||
sy: 'asia', // Syria
|
||||
tw: 'asia', // Taiwan
|
||||
tj: 'asia', // Tajikistan
|
||||
th: 'asia', // Thailand
|
||||
tl: 'asia', // Timor-Leste
|
||||
tr: 'asia', // Turkey (transcontinental, but primarily Asian)
|
||||
tm: 'asia', // Turkmenistan
|
||||
ae: 'asia', // United Arab Emirates
|
||||
uz: 'asia', // Uzbekistan
|
||||
vn: 'asia', // Vietnam
|
||||
ye: 'asia', // Yemen
|
||||
|
||||
// Europe
|
||||
ax: 'europe', // Åland Islands
|
||||
al: 'europe', // Albania
|
||||
ad: 'europe', // Andorra
|
||||
at: 'europe', // Austria
|
||||
by: 'europe', // Belarus
|
||||
be: 'europe', // Belgium
|
||||
ba: 'europe', // Bosnia and Herzegovina
|
||||
bg: 'europe', // Bulgaria
|
||||
hr: 'europe', // Croatia
|
||||
cy: 'europe', // Cyprus
|
||||
cz: 'europe', // Czech Republic
|
||||
dk: 'europe', // Denmark
|
||||
ee: 'europe', // Estonia
|
||||
fo: 'europe', // Faroe Islands
|
||||
fi: 'europe', // Finland
|
||||
fr: 'europe', // France
|
||||
de: 'europe', // Germany
|
||||
gi: 'europe', // Gibraltar
|
||||
gr: 'europe', // Greece
|
||||
gg: 'europe', // Guernsey
|
||||
hu: 'europe', // Hungary
|
||||
is: 'europe', // Iceland
|
||||
ie: 'europe', // Ireland
|
||||
im: 'europe', // Isle of Man
|
||||
it: 'europe', // Italy
|
||||
je: 'europe', // Jersey
|
||||
xk: 'europe', // Kosovo
|
||||
lv: 'europe', // Latvia
|
||||
li: 'europe', // Liechtenstein
|
||||
lt: 'europe', // Lithuania
|
||||
lu: 'europe', // Luxembourg
|
||||
mk: 'europe', // North Macedonia
|
||||
mt: 'europe', // Malta
|
||||
md: 'europe', // Moldova
|
||||
mc: 'europe', // Monaco
|
||||
me: 'europe', // Montenegro
|
||||
nl: 'europe', // Netherlands
|
||||
no: 'europe', // Norway
|
||||
pl: 'europe', // Poland
|
||||
pt: 'europe', // Portugal
|
||||
ro: 'europe', // Romania
|
||||
ru: 'europe', // Russia (transcontinental, but primarily European for map purposes)
|
||||
sm: 'europe', // San Marino
|
||||
rs: 'europe', // Serbia
|
||||
sk: 'europe', // Slovakia
|
||||
si: 'europe', // Slovenia
|
||||
es: 'europe', // Spain
|
||||
sj: 'europe', // Svalbard and Jan Mayen
|
||||
se: 'europe', // Sweden
|
||||
ch: 'europe', // Switzerland
|
||||
ua: 'europe', // Ukraine
|
||||
gb: 'europe', // United Kingdom
|
||||
va: 'europe', // Vatican City
|
||||
|
||||
// North America
|
||||
ai: 'north-america', // Anguilla
|
||||
ag: 'north-america', // Antigua and Barbuda
|
||||
aw: 'north-america', // Aruba
|
||||
bs: 'north-america', // Bahamas
|
||||
bb: 'north-america', // Barbados
|
||||
bz: 'north-america', // Belize
|
||||
bm: 'north-america', // Bermuda
|
||||
bq: 'north-america', // Caribbean Netherlands
|
||||
vg: 'north-america', // British Virgin Islands
|
||||
ca: 'north-america', // Canada
|
||||
ky: 'north-america', // Cayman Islands
|
||||
cr: 'north-america', // Costa Rica
|
||||
cu: 'north-america', // Cuba
|
||||
cw: 'north-america', // Curaçao
|
||||
dm: 'north-america', // Dominica
|
||||
do: 'north-america', // Dominican Republic
|
||||
sv: 'north-america', // El Salvador
|
||||
gl: 'north-america', // Greenland
|
||||
gd: 'north-america', // Grenada
|
||||
gp: 'north-america', // Guadeloupe
|
||||
gt: 'north-america', // Guatemala
|
||||
ht: 'north-america', // Haiti
|
||||
hn: 'north-america', // Honduras
|
||||
jm: 'north-america', // Jamaica
|
||||
mq: 'north-america', // Martinique
|
||||
mx: 'north-america', // Mexico
|
||||
ms: 'north-america', // Montserrat
|
||||
ni: 'north-america', // Nicaragua
|
||||
pa: 'north-america', // Panama
|
||||
pm: 'north-america', // Saint Pierre and Miquelon
|
||||
kn: 'north-america', // Saint Kitts and Nevis
|
||||
lc: 'north-america', // Saint Lucia
|
||||
vc: 'north-america', // Saint Vincent and the Grenadines
|
||||
sx: 'north-america', // Sint Maarten
|
||||
tt: 'north-america', // Trinidad and Tobago
|
||||
tc: 'north-america', // Turks and Caicos Islands
|
||||
us: 'north-america', // United States
|
||||
vi: 'north-america', // U.S. Virgin Islands
|
||||
|
||||
// South America
|
||||
ar: 'south-america', // Argentina
|
||||
bo: 'south-america', // Bolivia
|
||||
br: 'south-america', // Brazil
|
||||
cl: 'south-america', // Chile
|
||||
co: 'south-america', // Colombia
|
||||
ec: 'south-america', // Ecuador
|
||||
fk: 'south-america', // Falkland Islands
|
||||
gf: 'south-america', // French Guiana
|
||||
gy: 'south-america', // Guyana
|
||||
py: 'south-america', // Paraguay
|
||||
pe: 'south-america', // Peru
|
||||
sr: 'south-america', // Suriname
|
||||
uy: 'south-america', // Uruguay
|
||||
ve: 'south-america', // Venezuela
|
||||
|
||||
// Oceania
|
||||
as: 'oceania', // American Samoa
|
||||
au: 'oceania', // Australia
|
||||
ck: 'oceania', // Cook Islands
|
||||
fj: 'oceania', // Fiji
|
||||
pf: 'oceania', // French Polynesia
|
||||
gu: 'oceania', // Guam
|
||||
ki: 'oceania', // Kiribati
|
||||
mh: 'oceania', // Marshall Islands
|
||||
fm: 'oceania', // Micronesia
|
||||
nr: 'oceania', // Nauru
|
||||
nc: 'oceania', // New Caledonia
|
||||
nz: 'oceania', // New Zealand
|
||||
nu: 'oceania', // Niue
|
||||
nf: 'oceania', // Norfolk Island
|
||||
mp: 'oceania', // Northern Mariana Islands
|
||||
pw: 'oceania', // Palau
|
||||
pg: 'oceania', // Papua New Guinea
|
||||
pn: 'oceania', // Pitcairn Islands
|
||||
ws: 'oceania', // Samoa
|
||||
sb: 'oceania', // Solomon Islands
|
||||
tk: 'oceania', // Tokelau
|
||||
to: 'oceania', // Tonga
|
||||
tv: 'oceania', // Tuvalu
|
||||
vu: 'oceania', // Vanuatu
|
||||
wf: 'oceania', // Wallis and Futuna
|
||||
|
||||
// Antarctica
|
||||
aq: 'antarctica', // Antarctica
|
||||
bv: 'antarctica', // Bouvet Island
|
||||
tf: 'antarctica', // French Southern Territories
|
||||
hm: 'antarctica', // Heard Island and McDonald Islands
|
||||
gs: 'antarctica', // South Georgia and the South Sandwich Islands
|
||||
}
|
||||
|
||||
/**
|
||||
* Get continent for a country code
|
||||
*/
|
||||
export function getContinentForCountry(countryCode: string): ContinentId | null {
|
||||
return COUNTRY_TO_CONTINENT[countryCode.toLowerCase()] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all country codes for a continent
|
||||
*/
|
||||
export function getCountriesInContinent(continentId: ContinentId): string[] {
|
||||
return Object.entries(COUNTRY_TO_CONTINENT)
|
||||
.filter(([_, continent]) => continent === continentId)
|
||||
.map(([countryCode]) => countryCode)
|
||||
}
|
||||
|
|
@ -32,9 +32,21 @@ const defaultConfig: KnowYourWorldConfig = {
|
|||
gameMode: 'cooperative',
|
||||
difficulty: 'easy',
|
||||
studyDuration: 0,
|
||||
selectedContinent: 'all',
|
||||
}
|
||||
|
||||
function validateKnowYourWorldConfig(config: unknown): config is KnowYourWorldConfig {
|
||||
const validContinents = [
|
||||
'all',
|
||||
'africa',
|
||||
'asia',
|
||||
'europe',
|
||||
'north-america',
|
||||
'south-america',
|
||||
'oceania',
|
||||
'antarctica',
|
||||
]
|
||||
|
||||
return (
|
||||
typeof config === 'object' &&
|
||||
config !== null &&
|
||||
|
|
@ -42,6 +54,7 @@ function validateKnowYourWorldConfig(config: unknown): config is KnowYourWorldCo
|
|||
'gameMode' in config &&
|
||||
'difficulty' in config &&
|
||||
'studyDuration' in config &&
|
||||
'selectedContinent' in config &&
|
||||
(config.selectedMap === 'world' || config.selectedMap === 'usa') &&
|
||||
(config.gameMode === 'cooperative' ||
|
||||
config.gameMode === 'race' ||
|
||||
|
|
@ -50,7 +63,9 @@ function validateKnowYourWorldConfig(config: unknown): config is KnowYourWorldCo
|
|||
(config.studyDuration === 0 ||
|
||||
config.studyDuration === 30 ||
|
||||
config.studyDuration === 60 ||
|
||||
config.studyDuration === 120)
|
||||
config.studyDuration === 120) &&
|
||||
typeof config.selectedContinent === 'string' &&
|
||||
validContinents.includes(config.selectedContinent)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-ignore - ESM/CommonJS compatibility
|
||||
// @ts-expect-error - ESM/CommonJS compatibility
|
||||
import World from '@svg-maps/world'
|
||||
// @ts-ignore - ESM/CommonJS compatibility
|
||||
// @ts-expect-error - ESM/CommonJS compatibility
|
||||
import USA from '@svg-maps/usa'
|
||||
import type { MapData, MapRegion } from './types'
|
||||
|
||||
|
|
@ -20,7 +20,11 @@ function calculatePathCenter(pathString: string): [number, number] {
|
|||
|
||||
while ((match = commandRegex.exec(pathString)) !== null) {
|
||||
const command = match[1]
|
||||
const params = match[2].trim().match(/-?\d+\.?\d*/g)?.map(Number) || []
|
||||
const params =
|
||||
match[2]
|
||||
.trim()
|
||||
.match(/-?\d+\.?\d*/g)
|
||||
?.map(Number) || []
|
||||
|
||||
switch (command) {
|
||||
case 'M': // Move to (absolute)
|
||||
|
|
@ -240,3 +244,124 @@ export function getRegionById(mapId: 'world' | 'usa', regionId: string) {
|
|||
const mapData = getMapData(mapId)
|
||||
return mapData.regions.find((r) => r.id === regionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bounding box for a set of SVG paths
|
||||
*/
|
||||
export interface BoundingBox {
|
||||
minX: number
|
||||
maxX: number
|
||||
minY: number
|
||||
maxY: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
function calculateBoundingBox(paths: string[]): BoundingBox {
|
||||
let minX = Infinity
|
||||
let maxX = -Infinity
|
||||
let minY = Infinity
|
||||
let maxY = -Infinity
|
||||
|
||||
for (const path of paths) {
|
||||
// Extract all numbers from path string
|
||||
const numbers = path.match(/-?\d+\.?\d*/g)?.map(Number) || []
|
||||
|
||||
// Assume pairs of x,y coordinates
|
||||
for (let i = 0; i < numbers.length - 1; i += 2) {
|
||||
const x = numbers[i]
|
||||
const y = numbers[i + 1]
|
||||
|
||||
minX = Math.min(minX, x)
|
||||
maxX = Math.max(maxX, x)
|
||||
minY = Math.min(minY, y)
|
||||
maxY = Math.max(maxY, y)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
minX,
|
||||
maxX,
|
||||
minY,
|
||||
maxY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter world map regions by continent
|
||||
*/
|
||||
import { getContinentForCountry, type ContinentId } from './continents'
|
||||
|
||||
export function filterRegionsByContinent(
|
||||
regions: MapRegion[],
|
||||
continentId: ContinentId | 'all'
|
||||
): MapRegion[] {
|
||||
if (continentId === 'all') {
|
||||
return regions
|
||||
}
|
||||
|
||||
return regions.filter((region) => {
|
||||
const continent = getContinentForCountry(region.id)
|
||||
return continent === continentId
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate adjusted viewBox for a continent
|
||||
* Adds padding around the bounding box
|
||||
*/
|
||||
export function calculateContinentViewBox(
|
||||
regions: MapRegion[],
|
||||
continentId: ContinentId | 'all',
|
||||
originalViewBox: string
|
||||
): string {
|
||||
if (continentId === 'all') {
|
||||
return originalViewBox
|
||||
}
|
||||
|
||||
const filteredRegions = filterRegionsByContinent(regions, continentId)
|
||||
|
||||
if (filteredRegions.length === 0) {
|
||||
return originalViewBox
|
||||
}
|
||||
|
||||
const paths = filteredRegions.map((r) => r.path)
|
||||
const bbox = calculateBoundingBox(paths)
|
||||
|
||||
// Add 10% padding on each side
|
||||
const paddingX = bbox.width * 0.1
|
||||
const paddingY = bbox.height * 0.1
|
||||
|
||||
const newMinX = bbox.minX - paddingX
|
||||
const newMinY = bbox.minY - paddingY
|
||||
const newWidth = bbox.width + 2 * paddingX
|
||||
const newHeight = bbox.height + 2 * paddingY
|
||||
|
||||
return `${newMinX} ${newMinY} ${newWidth} ${newHeight}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filtered map data for a continent
|
||||
*/
|
||||
export function getFilteredMapData(
|
||||
mapId: 'world' | 'usa',
|
||||
continentId: ContinentId | 'all'
|
||||
): MapData {
|
||||
const mapData = getMapData(mapId)
|
||||
|
||||
// Continent filtering only applies to world map
|
||||
if (mapId !== 'world' || continentId === 'all') {
|
||||
return mapData
|
||||
}
|
||||
|
||||
const filteredRegions = filterRegionsByContinent(mapData.regions, continentId)
|
||||
const adjustedViewBox = calculateContinentViewBox(mapData.regions, continentId, mapData.viewBox)
|
||||
|
||||
return {
|
||||
...mapData,
|
||||
regions: filteredRegions,
|
||||
viewBox: adjustedViewBox,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { GameConfig, GameMove, GameState } from '@/lib/arcade/game-sdk'
|
||||
import type { ContinentId } from './continents'
|
||||
|
||||
// Game configuration (persisted to database)
|
||||
export interface KnowYourWorldConfig extends GameConfig {
|
||||
|
|
@ -6,6 +7,7 @@ export interface KnowYourWorldConfig extends GameConfig {
|
|||
gameMode: 'cooperative' | 'race' | 'turn-based'
|
||||
difficulty: 'easy' | 'hard'
|
||||
studyDuration: 0 | 30 | 60 | 120 // seconds (0 = skip study mode)
|
||||
selectedContinent: ContinentId | 'all' // continent filter for world map ('all' = no filter)
|
||||
}
|
||||
|
||||
// Map data structures
|
||||
|
|
@ -42,6 +44,7 @@ export interface KnowYourWorldState extends GameState {
|
|||
gameMode: 'cooperative' | 'race' | 'turn-based'
|
||||
difficulty: 'easy' | 'hard'
|
||||
studyDuration: 0 | 30 | 60 | 120 // seconds (0 = skip study mode)
|
||||
selectedContinent: ContinentId | 'all' // continent filter for world map ('all' = no filter)
|
||||
|
||||
// Study phase
|
||||
studyTimeRemaining: number // seconds remaining in study phase
|
||||
|
|
@ -156,3 +159,12 @@ export type KnowYourWorldMove =
|
|||
timestamp: number
|
||||
data: {}
|
||||
}
|
||||
| {
|
||||
type: 'SET_CONTINENT'
|
||||
playerId: string
|
||||
userId: string
|
||||
timestamp: number
|
||||
data: {
|
||||
selectedContinent: ContinentId | 'all'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue