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 6b50e0b2..bc14275b 100644 --- a/apps/web/src/arcade-games/know-your-world/Provider.tsx +++ b/apps/web/src/arcade-games/know-your-world/Provider.tsx @@ -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(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) || {} + const currentConfig = (currentGameConfig['know-your-world'] as Record) || {} + + 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} 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 d3753aef..81611696 100644 --- a/apps/web/src/arcade-games/know-your-world/Validator.ts +++ b/apps/web/src/arcade-games/know-your-world/Validator.ts @@ -5,7 +5,7 @@ import type { KnowYourWorldState, GuessRecord, } from './types' -import { getMapData } from './maps' +import { getMapData, getFilteredMapData } from './maps' export class KnowYourWorldValidator implements GameValidator @@ -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, diff --git a/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx b/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx index ebed86e8..97f09abb 100644 --- a/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx +++ b/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx @@ -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 */} - - + + - ) } diff --git a/apps/web/src/arcade-games/know-your-world/components/PlayingPhase.tsx b/apps/web/src/arcade-games/know-your-world/components/PlayingPhase.tsx index d33c4d59..7297621b 100644 --- a/apps/web/src/arcade-games/know-your-world/components/PlayingPhase.tsx +++ b/apps/web/src/arcade-games/know-your-world/components/PlayingPhase.tsx @@ -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 diff --git a/apps/web/src/arcade-games/know-your-world/components/SetupPhase.tsx b/apps/web/src/arcade-games/know-your-world/components/SetupPhase.tsx index fd09e0d7..a5f00845 100644 --- a/apps/web/src/arcade-games/know-your-world/components/SetupPhase.tsx +++ b/apps/web/src/arcade-games/know-your-world/components/SetupPhase.tsx @@ -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 (
+ {/* Continent Selection (only for World map) */} + {state.selectedMap === 'world' && ( +
+

+ Focus on Continent 🌐 +

+
+ {/* All continents option */} + + + {/* Individual continents */} + {CONTINENTS.map((continent) => ( + + ))} +
+
+ )} + {/* Mode Selection */}

{ @@ -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() { })} >

💡 Study Tips:

-
    +
    • Look for patterns - neighboring regions, shapes, sizes
    • Group regions mentally by area (e.g., Northeast, Southwest)
    • Focus on the tricky small ones that are hard to see
    • diff --git a/apps/web/src/arcade-games/know-your-world/continents.ts b/apps/web/src/arcade-games/know-your-world/continents.ts new file mode 100644 index 00000000..dedc8d2a --- /dev/null +++ b/apps/web/src/arcade-games/know-your-world/continents.ts @@ -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 = { + // 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) +} diff --git a/apps/web/src/arcade-games/know-your-world/index.ts b/apps/web/src/arcade-games/know-your-world/index.ts index f137685e..9c960321 100644 --- a/apps/web/src/arcade-games/know-your-world/index.ts +++ b/apps/web/src/arcade-games/know-your-world/index.ts @@ -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) ) } diff --git a/apps/web/src/arcade-games/know-your-world/maps.ts b/apps/web/src/arcade-games/know-your-world/maps.ts index 94e7785d..222f80f9 100644 --- a/apps/web/src/arcade-games/know-your-world/maps.ts +++ b/apps/web/src/arcade-games/know-your-world/maps.ts @@ -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, + } +} diff --git a/apps/web/src/arcade-games/know-your-world/types.ts b/apps/web/src/arcade-games/know-your-world/types.ts index ff04c7c4..ce6b82ab 100644 --- a/apps/web/src/arcade-games/know-your-world/types.ts +++ b/apps/web/src/arcade-games/know-your-world/types.ts @@ -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' + } + }