Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e393b42aa | ||
|
|
d7d8d8b1e3 | ||
|
|
51593eb44f | ||
|
|
b47b1cc03f | ||
|
|
eed468c6c4 |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,3 +1,25 @@
|
||||
## [4.0.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.0.2...v4.0.3) (2025-10-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **math-sprint:** remove unused import and autoFocus attribute ([51593eb](https://github.com/antialias/soroban-abacus-flashcards/commit/51593eb44f93e369d6a773ee80e5f5cf50f3be67))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **arcade:** implement Phase 3 - infer config types from game definitions ([eed468c](https://github.com/antialias/soroban-abacus-flashcards/commit/eed468c6c4057e3c09a1e8df88551a9336c490c5))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* **arcade:** update README with Phase 3 type inference architecture ([b47b1cc](https://github.com/antialias/soroban-abacus-flashcards/commit/b47b1cc03f4b5fcfe8340653ca8a5dd903833481))
|
||||
|
||||
|
||||
### Styles
|
||||
|
||||
* **math-sprint:** apply Biome formatting ([d7d8d8b](https://github.com/antialias/soroban-abacus-flashcards/commit/d7d8d8b1e32f9c9bb73d076f5d611210f809eca8))
|
||||
|
||||
## [4.0.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.0.1...v4.0.2) (2025-10-16)
|
||||
|
||||
|
||||
|
||||
@@ -38,12 +38,44 @@ A modular, plugin-based architecture for building multiplayer arcade games with
|
||||
|
||||
## Architecture
|
||||
|
||||
### Key Improvements
|
||||
|
||||
**✨ Phase 3: Type Inference (January 2025)**
|
||||
|
||||
Config types are now **automatically inferred** from game definitions for modern games. No more manual type definitions!
|
||||
|
||||
```typescript
|
||||
// Before Phase 3: Manual type definition
|
||||
export interface NumberGuesserGameConfig {
|
||||
minNumber: number
|
||||
maxNumber: number
|
||||
roundsToWin: number
|
||||
}
|
||||
|
||||
// After Phase 3: Inferred from game definition
|
||||
export type NumberGuesserGameConfig = InferGameConfig<typeof numberGuesserGame>
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Add a game → Config types automatically available system-wide
|
||||
- Single source of truth (the game definition)
|
||||
- Eliminates 10-15 lines of boilerplate per game
|
||||
|
||||
### System Components
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Validator Registry │
|
||||
│ - Server-side validators (isomorphic) │
|
||||
│ - Single source of truth for game names │
|
||||
│ - Auto-derived GameName type │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Game Registry │
|
||||
│ - Registers all available games │
|
||||
│ - Client-side game definitions │
|
||||
│ - React components (Provider, GameComponent) │
|
||||
│ - Provides game discovery │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
@@ -53,18 +85,27 @@ A modular, plugin-based architecture for building multiplayer arcade games with
|
||||
│ - Stable API surface for games │
|
||||
│ - React hooks (useArcadeSession, useRoomData, etc.) │
|
||||
│ - Type definitions and utilities │
|
||||
│ - defineGame() helper │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Individual Games │
|
||||
│ number-guesser/ │
|
||||
│ ├── index.ts (Game definition) │
|
||||
│ ├── Validator.ts (Server validation) │
|
||||
│ ├── index.ts (Game definition + validation) │
|
||||
│ ├── Validator.ts (Server validation logic) │
|
||||
│ ├── Provider.tsx (Client state management) │
|
||||
│ ├── GameComponent.tsx (Main UI) │
|
||||
│ ├── types.ts (TypeScript types) │
|
||||
│ └── components/ (Phase UIs) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Type System (NEW) │
|
||||
│ - Config types inferred from game definitions │
|
||||
│ - GameConfigByName auto-derived │
|
||||
│ - RoomGameConfig auto-derived │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -130,9 +171,12 @@ interface GameDefinition<TConfig, TState, TMove> {
|
||||
GameComponent: GameComponent // Main UI component
|
||||
validator: GameValidator // Server-side validation
|
||||
defaultConfig: TConfig // Default game settings
|
||||
validateConfig?: (config: unknown) => config is TConfig // Runtime config validation
|
||||
}
|
||||
```
|
||||
|
||||
**Key Concept**: The `defaultConfig` property serves as the source of truth for config types. TypeScript can infer the config type from `typeof game.defaultConfig`, eliminating the need for manual type definitions in `game-configs.ts`.
|
||||
|
||||
#### GameState
|
||||
|
||||
The complete game state that's synchronized across all clients:
|
||||
@@ -430,15 +474,32 @@ const defaultConfig: MyGameConfig = {
|
||||
timer: 30,
|
||||
}
|
||||
|
||||
// Runtime config validation (optional but recommended)
|
||||
function validateMyGameConfig(config: unknown): config is MyGameConfig {
|
||||
return (
|
||||
typeof config === 'object' &&
|
||||
config !== null &&
|
||||
'difficulty' in config &&
|
||||
'timer' in config &&
|
||||
typeof config.difficulty === 'number' &&
|
||||
typeof config.timer === 'number' &&
|
||||
config.difficulty >= 1 &&
|
||||
config.timer >= 10
|
||||
)
|
||||
}
|
||||
|
||||
export const myGame = defineGame<MyGameConfig, MyGameState, MyGameMove>({
|
||||
manifest,
|
||||
Provider: MyGameProvider,
|
||||
GameComponent,
|
||||
validator: myGameValidator,
|
||||
defaultConfig,
|
||||
validateConfig: validateMyGameConfig, // Self-contained validation
|
||||
})
|
||||
```
|
||||
|
||||
**Phase 3 Benefit**: After defining your game, the config type will be automatically inferred in `game-configs.ts`. You don't need to manually add type definitions - just add a type-only import and use `InferGameConfig<typeof myGame>`.
|
||||
|
||||
### Step 7: Register Game
|
||||
|
||||
#### 7a. Register Validator (Server-Side)
|
||||
@@ -480,6 +541,46 @@ registerGame(myGame)
|
||||
|
||||
**Important**: Both steps are required for a working game. The validator registry handles server logic, while the game registry handles client UI.
|
||||
|
||||
#### 7c. Add Config Type Inference (Optional but Recommended)
|
||||
|
||||
Update `src/lib/arcade/game-configs.ts` to infer your game's config type:
|
||||
|
||||
```typescript
|
||||
// Add type-only import (won't load React components)
|
||||
import type { myGame } from '@/arcade-games/my-game'
|
||||
|
||||
// Utility type (already defined)
|
||||
type InferGameConfig<T> = T extends { defaultConfig: infer Config } ? Config : never
|
||||
|
||||
// Infer your config type
|
||||
export type MyGameConfig = InferGameConfig<typeof myGame>
|
||||
|
||||
// Add to GameConfigByName
|
||||
export type GameConfigByName = {
|
||||
// ... other games
|
||||
'my-game': MyGameConfig // TypeScript infers the type automatically!
|
||||
}
|
||||
|
||||
// RoomGameConfig is auto-derived from GameConfigByName
|
||||
export type RoomGameConfig = {
|
||||
[K in keyof GameConfigByName]?: GameConfigByName[K]
|
||||
}
|
||||
|
||||
// Add default config constant
|
||||
export const DEFAULT_MY_GAME_CONFIG: MyGameConfig = {
|
||||
difficulty: 1,
|
||||
timer: 30,
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Config type automatically matches your game definition
|
||||
- No manual type definition needed
|
||||
- Single source of truth (your game's `defaultConfig`)
|
||||
- TypeScript will error if you reference undefined properties
|
||||
|
||||
**Note**: You still need to manually add the default config constant. This is a small amount of duplication but necessary for server-side code that can't import the full game definition.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
@@ -92,13 +92,14 @@ export function MathSprintProvider({ children }: { children: ReactNode }) {
|
||||
)
|
||||
|
||||
// Arcade session integration
|
||||
const { state, sendMove, exitSession, lastError, clearError } =
|
||||
useArcadeSession<MathSprintState>({
|
||||
const { state, sendMove, exitSession, lastError, clearError } = useArcadeSession<MathSprintState>(
|
||||
{
|
||||
userId: viewerId || '',
|
||||
roomId: roomData?.id,
|
||||
initialState,
|
||||
applyMove: (state) => state, // Server handles all state updates
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Action: Start game
|
||||
const startGame = useCallback(() => {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
import type { GameValidator, ValidationResult } from '@/lib/arcade/game-sdk'
|
||||
import { TEAM_MOVE } from '@/lib/arcade/validation/types'
|
||||
import type {
|
||||
Difficulty,
|
||||
MathSprintConfig,
|
||||
@@ -16,9 +15,7 @@ import type {
|
||||
Question,
|
||||
} from './types'
|
||||
|
||||
export class MathSprintValidator
|
||||
implements GameValidator<MathSprintState, MathSprintMove>
|
||||
{
|
||||
export class MathSprintValidator implements GameValidator<MathSprintState, MathSprintMove> {
|
||||
/**
|
||||
* Validate a game move
|
||||
*/
|
||||
@@ -222,11 +219,7 @@ export class MathSprintValidator
|
||||
return { valid: true, newState }
|
||||
}
|
||||
|
||||
private validateSetConfig(
|
||||
state: MathSprintState,
|
||||
field: string,
|
||||
value: any
|
||||
): ValidationResult {
|
||||
private validateSetConfig(state: MathSprintState, field: string, value: any): ValidationResult {
|
||||
if (state.gamePhase !== 'setup') {
|
||||
return { valid: false, error: 'Cannot change config during game' }
|
||||
}
|
||||
@@ -255,11 +248,7 @@ export class MathSprintValidator
|
||||
return questions
|
||||
}
|
||||
|
||||
private generateQuestion(
|
||||
difficulty: Difficulty,
|
||||
operation: Operation,
|
||||
id: string
|
||||
): Question {
|
||||
private generateQuestion(difficulty: Difficulty, operation: Operation, id: string): Question {
|
||||
let operand1: number
|
||||
let operand2: number
|
||||
let correctAnswer: number
|
||||
|
||||
@@ -80,7 +80,9 @@ export function PlayingPhase() {
|
||||
marginBottom: '8px',
|
||||
})}
|
||||
>
|
||||
<span className={css({ fontSize: 'sm', fontWeight: 'semibold' })}>Question {progress}</span>
|
||||
<span className={css({ fontSize: 'sm', fontWeight: 'semibold' })}>
|
||||
Question {progress}
|
||||
</span>
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.600' })}>
|
||||
{state.difficulty.charAt(0).toUpperCase() + state.difficulty.slice(1)}
|
||||
</span>
|
||||
@@ -211,7 +213,6 @@ export function PlayingPhase() {
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type your answer..."
|
||||
autoFocus
|
||||
className={css({
|
||||
flex: 1,
|
||||
padding: '12px 16px',
|
||||
@@ -334,7 +335,9 @@ export function PlayingPhase() {
|
||||
{player?.name}
|
||||
</span>
|
||||
</div>
|
||||
<span className={css({ fontSize: 'sm', fontWeight: 'bold', color: 'purple.600' })}>
|
||||
<span
|
||||
className={css({ fontSize: 'sm', fontWeight: 'bold', color: 'purple.600' })}
|
||||
>
|
||||
{score} pts
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -137,7 +137,9 @@ export function SetupPhase() {
|
||||
width: '100%',
|
||||
})}
|
||||
/>
|
||||
<div className={css({ display: 'flex', justifyContent: 'space-between', fontSize: 'xs' })}>
|
||||
<div
|
||||
className={css({ display: 'flex', justifyContent: 'space-between', fontSize: 'xs' })}
|
||||
>
|
||||
<span>5</span>
|
||||
<span>10</span>
|
||||
<span>15</span>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/**
|
||||
* Shared game configuration types
|
||||
*
|
||||
* This is the single source of truth for all game settings.
|
||||
* ARCHITECTURE: Phase 3 - Type Inference
|
||||
* - Modern games (number-guesser, math-sprint): Types inferred from game definitions
|
||||
* - Legacy games (matching, memory-quiz, complement-race): Manual types until migrated
|
||||
*
|
||||
* These types are used across:
|
||||
* - Database storage (room_game_configs table)
|
||||
* - Validators (getInitialState method signatures)
|
||||
@@ -11,7 +14,37 @@
|
||||
|
||||
import type { DifficultyLevel } from '@/app/arcade/memory-quiz/types'
|
||||
import type { Difficulty, GameType } from '@/app/games/matching/context/types'
|
||||
import type { Difficulty as MathSprintDifficulty } from '@/arcade-games/math-sprint/types'
|
||||
|
||||
// Type-only imports (won't load React components at runtime)
|
||||
import type { numberGuesserGame } from '@/arcade-games/number-guesser'
|
||||
import type { mathSprintGame } from '@/arcade-games/math-sprint'
|
||||
|
||||
/**
|
||||
* Utility type: Extract config type from a game definition
|
||||
* Uses TypeScript's infer keyword to extract the TConfig generic
|
||||
*/
|
||||
type InferGameConfig<T> = T extends { defaultConfig: infer Config } ? Config : never
|
||||
|
||||
// ============================================================================
|
||||
// Modern Games (Type Inference from Game Definitions)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Configuration for number-guesser game
|
||||
* INFERRED from numberGuesserGame.defaultConfig
|
||||
*/
|
||||
export type NumberGuesserGameConfig = InferGameConfig<typeof numberGuesserGame>
|
||||
|
||||
/**
|
||||
* Configuration for math-sprint game
|
||||
* INFERRED from mathSprintGame.defaultConfig
|
||||
*/
|
||||
export type MathSprintGameConfig = InferGameConfig<typeof mathSprintGame>
|
||||
|
||||
// ============================================================================
|
||||
// Legacy Games (Manual Type Definitions)
|
||||
// TODO: Migrate these games to the modular system for type inference
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Configuration for matching (memory pairs) game
|
||||
@@ -41,31 +74,21 @@ export interface ComplementRaceGameConfig {
|
||||
placeholder?: never
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for number-guesser game
|
||||
*/
|
||||
export interface NumberGuesserGameConfig {
|
||||
minNumber: number
|
||||
maxNumber: number
|
||||
roundsToWin: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for math-sprint game
|
||||
*/
|
||||
export interface MathSprintGameConfig {
|
||||
difficulty: MathSprintDifficulty
|
||||
questionsPerRound: number
|
||||
timePerQuestion: number
|
||||
}
|
||||
// ============================================================================
|
||||
// Combined Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Union type of all game configs for type-safe access
|
||||
* Modern games use inferred types, legacy games use manual types
|
||||
*/
|
||||
export type GameConfigByName = {
|
||||
// Legacy games (manual types)
|
||||
matching: MatchingGameConfig
|
||||
'memory-quiz': MemoryQuizGameConfig
|
||||
'complement-race': ComplementRaceGameConfig
|
||||
|
||||
// Modern games (inferred types)
|
||||
'number-guesser': NumberGuesserGameConfig
|
||||
'math-sprint': MathSprintGameConfig
|
||||
}
|
||||
@@ -73,13 +96,11 @@ export type GameConfigByName = {
|
||||
/**
|
||||
* Room's game configuration object (nested by game name)
|
||||
* This matches the structure stored in room_game_configs table
|
||||
*
|
||||
* AUTO-DERIVED: Adding a game to GameConfigByName automatically adds it here
|
||||
*/
|
||||
export interface RoomGameConfig {
|
||||
matching?: MatchingGameConfig
|
||||
'memory-quiz'?: MemoryQuizGameConfig
|
||||
'complement-race'?: ComplementRaceGameConfig
|
||||
'number-guesser'?: NumberGuesserGameConfig
|
||||
'math-sprint'?: MathSprintGameConfig
|
||||
export type RoomGameConfig = {
|
||||
[K in keyof GameConfigByName]?: GameConfigByName[K]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.0.2",
|
||||
"version": "4.0.3",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user