soroban-abacus-flashcards/apps/web/src/arcade-games/know-your-world/Validator.ts

439 lines
13 KiB
TypeScript

import type { GameValidator, ValidationResult } from '@/lib/arcade/game-sdk'
import type {
KnowYourWorldConfig,
KnowYourWorldMove,
KnowYourWorldState,
GuessRecord,
} from './types'
/**
* 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<typeof import('./maps').getFilteredMapData>
) {
const { getFilteredMapData } = await import('./maps')
return getFilteredMapData(...args)
}
export class KnowYourWorldValidator
implements GameValidator<KnowYourWorldState, KnowYourWorldMove>
{
async validateMove(
state: KnowYourWorldState,
move: KnowYourWorldMove
): Promise<ValidationResult> {
switch (move.type) {
case 'START_GAME':
return await this.validateStartGame(state, move.data)
case 'CLICK_REGION':
return this.validateClickRegion(state, move.playerId, move.data)
case 'NEXT_ROUND':
return await this.validateNextRound(state)
case 'END_GAME':
return this.validateEndGame(state)
case 'END_STUDY':
return this.validateEndStudy(state)
case 'RETURN_TO_SETUP':
return this.validateReturnToSetup(state)
case 'SET_MAP':
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_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' }
}
}
private async validateStartGame(state: KnowYourWorldState, data: any): Promise<ValidationResult> {
if (state.gamePhase !== 'setup') {
return { valid: false, error: 'Can only start from setup phase' }
}
const { activePlayers, playerMetadata, selectedMap, gameMode, difficulty } = 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)
const regionIds = mapData.regions.map((r) => r.id)
const shuffledRegions = this.shuffleArray([...regionIds])
// Initialize scores and attempts
const scores: Record<string, number> = {}
const attempts: Record<string, number> = {}
for (const playerId of activePlayers) {
scores[playerId] = 0
attempts[playerId] = 0
}
// Check if we should go to study phase or directly to playing
const shouldStudy = state.studyDuration > 0
const newState: KnowYourWorldState = {
...state,
gamePhase: shouldStudy ? 'studying' : 'playing',
activePlayers,
playerMetadata,
selectedMap,
gameMode,
difficulty,
studyTimeRemaining: shouldStudy ? state.studyDuration : 0,
studyStartTime: shouldStudy ? Date.now() : 0,
currentPrompt: shouldStudy ? null : shuffledRegions[0],
regionsToFind: shuffledRegions.slice(shouldStudy ? 0 : 1),
regionsFound: [],
currentPlayer: activePlayers[0],
scores,
attempts,
guessHistory: [],
startTime: Date.now(),
}
return { valid: true, newState }
}
private validateClickRegion(
state: KnowYourWorldState,
playerId: string,
data: any
): ValidationResult {
if (state.gamePhase !== 'playing') {
return { valid: false, error: 'Can only click regions during playing phase' }
}
if (!state.currentPrompt) {
return { valid: false, error: 'No region to find' }
}
const { regionId, regionName } = data
// Turn-based mode: Check if it's this player's turn
if (state.gameMode === 'turn-based' && state.currentPlayer !== playerId) {
return { valid: false, error: 'Not your turn' }
}
const isCorrect = regionId === state.currentPrompt
const guessRecord: GuessRecord = {
playerId,
regionId,
regionName,
correct: isCorrect,
attempts: 1,
timestamp: Date.now(),
}
if (isCorrect) {
// Correct guess!
const newScores = { ...state.scores }
const newRegionsFound = [...state.regionsFound, regionId]
const guessHistory = [...state.guessHistory, guessRecord]
// Award points based on mode
if (state.gameMode === 'cooperative') {
// In cooperative mode, all players share the score
for (const pid of state.activePlayers) {
newScores[pid] = (newScores[pid] || 0) + 10
}
} else {
// In race and turn-based, only the player who guessed gets points
newScores[playerId] = (newScores[playerId] || 0) + 10
}
// Check if all regions found
if (state.regionsToFind.length === 0) {
// Game complete!
const newState: KnowYourWorldState = {
...state,
gamePhase: 'results',
currentPrompt: null,
regionsFound: newRegionsFound,
scores: newScores,
guessHistory,
endTime: Date.now(),
}
return { valid: true, newState }
}
// Move to next region
const nextPrompt = state.regionsToFind[0]
const remainingRegions = state.regionsToFind.slice(1)
// For turn-based mode, rotate to next player
let nextPlayer = state.currentPlayer
if (state.gameMode === 'turn-based') {
const currentIndex = state.activePlayers.indexOf(state.currentPlayer)
const nextIndex = (currentIndex + 1) % state.activePlayers.length
nextPlayer = state.activePlayers[nextIndex]
}
const newState: KnowYourWorldState = {
...state,
currentPrompt: nextPrompt,
regionsToFind: remainingRegions,
regionsFound: newRegionsFound,
currentPlayer: nextPlayer,
scores: newScores,
guessHistory,
}
return { valid: true, newState }
} else {
// Incorrect guess
const newAttempts = { ...state.attempts }
newAttempts[playerId] = (newAttempts[playerId] || 0) + 1
const guessHistory = [...state.guessHistory, guessRecord]
// For turn-based mode, rotate to next player after wrong guess
let nextPlayer = state.currentPlayer
if (state.gameMode === 'turn-based') {
const currentIndex = state.activePlayers.indexOf(state.currentPlayer)
const nextIndex = (currentIndex + 1) % state.activePlayers.length
nextPlayer = state.activePlayers[nextIndex]
}
const newState: KnowYourWorldState = {
...state,
attempts: newAttempts,
guessHistory,
currentPlayer: nextPlayer,
}
return {
valid: true,
newState,
error: `Incorrect! Try again. Looking for: ${state.currentPrompt}`,
}
}
}
private async validateNextRound(state: KnowYourWorldState): Promise<ValidationResult> {
if (state.gamePhase !== 'results') {
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(
state.selectedMap,
state.selectedContinent,
state.difficulty
)
const regionIds = mapData.regions.map((r) => r.id)
const shuffledRegions = this.shuffleArray([...regionIds])
// Reset game state but keep players and config
const scores: Record<string, number> = {}
const attempts: Record<string, number> = {}
for (const playerId of state.activePlayers) {
scores[playerId] = 0
attempts[playerId] = 0
}
// Check if we should go to study phase or directly to playing
const shouldStudy = state.studyDuration > 0
const newState: KnowYourWorldState = {
...state,
gamePhase: shouldStudy ? 'studying' : 'playing',
studyTimeRemaining: shouldStudy ? state.studyDuration : 0,
studyStartTime: shouldStudy ? Date.now() : 0,
currentPrompt: shouldStudy ? null : shuffledRegions[0],
regionsToFind: shuffledRegions.slice(shouldStudy ? 0 : 1),
regionsFound: [],
currentPlayer: state.activePlayers[0],
scores,
attempts,
guessHistory: [],
startTime: Date.now(),
endTime: undefined,
}
return { valid: true, newState }
}
private validateEndGame(state: KnowYourWorldState): ValidationResult {
const newState: KnowYourWorldState = {
...state,
gamePhase: 'results',
currentPrompt: null,
endTime: Date.now(),
}
return { valid: true, newState }
}
private validateSetMap(
state: KnowYourWorldState,
selectedMap: 'world' | 'usa'
): ValidationResult {
if (state.gamePhase !== 'setup') {
return { valid: false, error: 'Can only change map during setup' }
}
const newState: KnowYourWorldState = {
...state,
selectedMap,
}
return { valid: true, newState }
}
private validateSetMode(
state: KnowYourWorldState,
gameMode: 'cooperative' | 'race' | 'turn-based'
): ValidationResult {
if (state.gamePhase !== 'setup') {
return { valid: false, error: 'Can only change mode during setup' }
}
const newState: KnowYourWorldState = {
...state,
gameMode,
}
return { valid: true, newState }
}
private validateSetDifficulty(state: KnowYourWorldState, difficulty: string): ValidationResult {
if (state.gamePhase !== 'setup') {
return { valid: false, error: 'Can only change difficulty during setup' }
}
const newState: KnowYourWorldState = {
...state,
difficulty,
}
return { valid: true, newState }
}
private validateSetStudyDuration(
state: KnowYourWorldState,
studyDuration: 0 | 30 | 60 | 120
): ValidationResult {
if (state.gamePhase !== 'setup') {
return { valid: false, error: 'Can only change study duration during setup' }
}
const newState: KnowYourWorldState = {
...state,
studyDuration,
}
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' }
}
// Transition from studying to playing
// Set the first prompt from the regions to find
const currentPrompt = state.regionsToFind[0] || null
const remainingRegions = state.regionsToFind.slice(1)
const newState: KnowYourWorldState = {
...state,
gamePhase: 'playing',
currentPrompt,
regionsToFind: remainingRegions,
studyTimeRemaining: 0,
}
return { valid: true, newState }
}
private validateReturnToSetup(state: KnowYourWorldState): ValidationResult {
if (state.gamePhase === 'setup') {
return { valid: false, error: 'Already in setup phase' }
}
// Return to setup, preserving config settings but resetting game state
const newState: KnowYourWorldState = {
...state,
gamePhase: 'setup',
currentPrompt: null,
regionsToFind: [],
regionsFound: [],
currentPlayer: '',
scores: {},
attempts: {},
guessHistory: [],
startTime: 0,
endTime: undefined,
studyTimeRemaining: 0,
studyStartTime: 0,
}
return { valid: true, newState }
}
isGameComplete(state: KnowYourWorldState): boolean {
return state.gamePhase === 'results'
}
getInitialState(config: unknown): KnowYourWorldState {
const typedConfig = config as KnowYourWorldConfig
return {
gamePhase: 'setup',
selectedMap: typedConfig?.selectedMap || 'world',
gameMode: typedConfig?.gameMode || 'cooperative',
difficulty: typedConfig?.difficulty || 'easy',
studyDuration: typedConfig?.studyDuration || 0,
selectedContinent: typedConfig?.selectedContinent || 'all',
studyTimeRemaining: 0,
studyStartTime: 0,
currentPrompt: null,
regionsToFind: [],
regionsFound: [],
currentPlayer: '',
scores: {},
attempts: {},
guessHistory: [],
startTime: 0,
activePlayers: [],
playerMetadata: {},
}
}
// Helper: Shuffle array (Fisher-Yates)
private shuffleArray<T>(array: T[]): T[] {
const shuffled = [...array]
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
}
return shuffled
}
}
export const knowYourWorldValidator = new KnowYourWorldValidator()