Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa868e3f7f | ||
|
|
b19437b7dc |
@@ -1,3 +1,10 @@
|
||||
## [4.0.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.0.0...v4.0.1) (2025-10-16)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **arcade:** move config validation to game definitions ([b19437b](https://github.com/antialias/soroban-abacus-flashcards/commit/b19437b7dc418f194fb60e12f1c17034024eca2a)), closes [#3](https://github.com/antialias/soroban-abacus-flashcards/issues/3)
|
||||
|
||||
## [4.0.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.24.0...v4.0.0) (2025-10-16)
|
||||
|
||||
|
||||
|
||||
@@ -34,10 +34,28 @@ const defaultConfig: MathSprintConfig = {
|
||||
timePerQuestion: 30,
|
||||
}
|
||||
|
||||
// Config validation function
|
||||
function validateMathSprintConfig(config: unknown): config is MathSprintConfig {
|
||||
return (
|
||||
typeof config === 'object' &&
|
||||
config !== null &&
|
||||
'difficulty' in config &&
|
||||
'questionsPerRound' in config &&
|
||||
'timePerQuestion' in config &&
|
||||
['easy', 'medium', 'hard'].includes((config as any).difficulty) &&
|
||||
typeof (config as any).questionsPerRound === 'number' &&
|
||||
typeof (config as any).timePerQuestion === 'number' &&
|
||||
(config as any).questionsPerRound >= 5 &&
|
||||
(config as any).questionsPerRound <= 20 &&
|
||||
(config as any).timePerQuestion >= 10
|
||||
)
|
||||
}
|
||||
|
||||
export const mathSprintGame = defineGame<MathSprintConfig, MathSprintState, MathSprintMove>({
|
||||
manifest,
|
||||
Provider: MathSprintProvider,
|
||||
GameComponent,
|
||||
validator: mathSprintValidator,
|
||||
defaultConfig,
|
||||
validateConfig: validateMathSprintConfig,
|
||||
})
|
||||
|
||||
@@ -34,6 +34,23 @@ const defaultConfig: NumberGuesserConfig = {
|
||||
roundsToWin: 3,
|
||||
}
|
||||
|
||||
// Config validation function
|
||||
function validateNumberGuesserConfig(config: unknown): config is NumberGuesserConfig {
|
||||
return (
|
||||
typeof config === 'object' &&
|
||||
config !== null &&
|
||||
'minNumber' in config &&
|
||||
'maxNumber' in config &&
|
||||
'roundsToWin' in config &&
|
||||
typeof config.minNumber === 'number' &&
|
||||
typeof config.maxNumber === 'number' &&
|
||||
typeof config.roundsToWin === 'number' &&
|
||||
config.minNumber >= 1 &&
|
||||
config.maxNumber > config.minNumber &&
|
||||
config.roundsToWin >= 1
|
||||
)
|
||||
}
|
||||
|
||||
// Export game definition
|
||||
export const numberGuesserGame = defineGame<
|
||||
NumberGuesserConfig,
|
||||
@@ -45,4 +62,5 @@ export const numberGuesserGame = defineGame<
|
||||
GameComponent,
|
||||
validator: numberGuesserValidator,
|
||||
defaultConfig,
|
||||
validateConfig: validateNumberGuesserConfig,
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
DEFAULT_NUMBER_GUESSER_CONFIG,
|
||||
DEFAULT_MATH_SPRINT_CONFIG,
|
||||
} from './game-configs'
|
||||
import { getGame } from './game-registry'
|
||||
|
||||
/**
|
||||
* Extended game name type that includes both registered validators and legacy games
|
||||
@@ -176,8 +177,20 @@ export async function deleteAllGameConfigs(roomId: string): Promise<void> {
|
||||
/**
|
||||
* Validate a game config at runtime
|
||||
* Returns true if the config is valid for the given game
|
||||
*
|
||||
* NEW: Uses game registry validation functions instead of switch statements.
|
||||
* Games now own their own validation logic!
|
||||
*/
|
||||
export function validateGameConfig(gameName: ExtendedGameName, config: any): boolean {
|
||||
// Try to get game from registry
|
||||
const game = getGame(gameName)
|
||||
|
||||
// If game has a validateConfig function, use it
|
||||
if (game?.validateConfig) {
|
||||
return game.validateConfig(config)
|
||||
}
|
||||
|
||||
// Fallback for legacy games without registry (e.g., complement-race, matching, memory-quiz)
|
||||
switch (gameName) {
|
||||
case 'matching':
|
||||
return (
|
||||
@@ -206,31 +219,8 @@ export function validateGameConfig(gameName: ExtendedGameName, config: any): boo
|
||||
// TODO: Add validation when complement-race settings are defined
|
||||
return typeof config === 'object' && config !== null
|
||||
|
||||
case 'number-guesser':
|
||||
return (
|
||||
typeof config === 'object' &&
|
||||
config !== null &&
|
||||
typeof config.minNumber === 'number' &&
|
||||
typeof config.maxNumber === 'number' &&
|
||||
typeof config.roundsToWin === 'number' &&
|
||||
config.minNumber >= 1 &&
|
||||
config.maxNumber > config.minNumber &&
|
||||
config.roundsToWin >= 1
|
||||
)
|
||||
|
||||
case 'math-sprint':
|
||||
return (
|
||||
typeof config === 'object' &&
|
||||
config !== null &&
|
||||
['easy', 'medium', 'hard'].includes(config.difficulty) &&
|
||||
typeof config.questionsPerRound === 'number' &&
|
||||
typeof config.timePerQuestion === 'number' &&
|
||||
config.questionsPerRound >= 5 &&
|
||||
config.questionsPerRound <= 20 &&
|
||||
config.timePerQuestion >= 10
|
||||
)
|
||||
|
||||
default:
|
||||
return false
|
||||
// If no validator found, accept any object
|
||||
return typeof config === 'object' && config !== null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ export interface DefineGameOptions<
|
||||
|
||||
/** Default configuration for the game */
|
||||
defaultConfig: TConfig
|
||||
|
||||
/** Optional: Runtime config validation function */
|
||||
validateConfig?: (config: unknown) => config is TConfig
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +66,7 @@ export function defineGame<
|
||||
TState extends GameState,
|
||||
TMove extends GameMove,
|
||||
>(options: DefineGameOptions<TConfig, TState, TMove>): GameDefinition<TConfig, TState, TMove> {
|
||||
const { manifest, Provider, GameComponent, validator, defaultConfig } = options
|
||||
const { manifest, Provider, GameComponent, validator, defaultConfig, validateConfig } = options
|
||||
|
||||
// Validate that manifest.name matches the game identifier
|
||||
if (!manifest.name) {
|
||||
@@ -76,5 +79,6 @@ export function defineGame<
|
||||
GameComponent,
|
||||
validator,
|
||||
defaultConfig,
|
||||
validateConfig,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,4 +77,13 @@ export interface GameDefinition<
|
||||
|
||||
/** Default configuration */
|
||||
defaultConfig: TConfig
|
||||
|
||||
/**
|
||||
* Validate a config object at runtime
|
||||
* Returns true if config is valid for this game
|
||||
*
|
||||
* @param config - Configuration object to validate
|
||||
* @returns true if valid, false otherwise
|
||||
*/
|
||||
validateConfig?: (config: unknown) => config is TConfig
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.1",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user