Implement platform-controlled GameOptions for complete game behavior customization
- Add GameOptions with gameMode, downwardSpeed, boardDimensions, and wordList - Platform now controls ALL game behavior through options - changeGameMode now accepts full GameOptions instead of just mode - downwardSpeed as single px/s value applies to any mode - wordList is mode-agnostic, platform provides what they need - All options are fully optional with sensible defaults - Update example app to use new options API
This commit is contained in:
+873
@@ -0,0 +1,873 @@
|
||||
# Unified API Specification
|
||||
|
||||
**Version:** 2.0
|
||||
**Last Updated:** 2025-01-22
|
||||
|
||||
Complete specification for building VIMazing games. This document defines the architecture, requirements, and patterns that all games must follow to maintain consistency while allowing game-specific customization.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Philosophy](#philosophy)
|
||||
2. [Architecture Overview](#architecture-overview)
|
||||
3. [Core Requirements](#core-requirements)
|
||||
4. [Game-Specific Flexibility](#game-specific-flexibility)
|
||||
5. [Hook Specifications](#hook-specifications)
|
||||
6. [Type System](#type-system)
|
||||
7. [Game Instructions Export](#game-instructions-export)
|
||||
8. [Creating a New Game](#creating-a-new-game)
|
||||
9. [Integration Guide](#integration-guide)
|
||||
|
||||
---
|
||||
|
||||
## Philosophy
|
||||
|
||||
The Unified API is designed around three core principles:
|
||||
|
||||
### 1. Consistent Entry Point
|
||||
Every game uses `useGame(options?, platformHook?)` as its single entry point. This ensures:
|
||||
- Predictable initialization across all games
|
||||
- Easy platform integration
|
||||
- Consistent developer experience
|
||||
|
||||
### 2. Composable Architecture
|
||||
Games are built from independent, composable hooks that manage distinct concerns:
|
||||
- **useBoard** - Level/puzzle generation and rendering
|
||||
- **useCursor** - Player input and interaction
|
||||
- **useScore** - Metrics and performance tracking
|
||||
- **useGameStatus** - Game lifecycle state machine
|
||||
|
||||
### 3. Freedom Within Constraints
|
||||
The API defines WHAT must be exposed, not HOW it works internally. Games have complete freedom in:
|
||||
- Scoring algorithms
|
||||
- Rendering strategies
|
||||
- Internal state management
|
||||
- Additional game-specific features
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### The Unified Pattern
|
||||
|
||||
```
|
||||
useGame(options, platformHook?)
|
||||
├─ useBoard() → Game-specific board/level management
|
||||
├─ useCursor() → Player input and interaction
|
||||
├─ useGameStatus() → Lifecycle state machine
|
||||
└─ useScore() → Metrics and scoring (game-specific)
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User Input
|
||||
↓
|
||||
useCursor (validates, processes)
|
||||
↓
|
||||
useBoard (updates state)
|
||||
↓
|
||||
useScore (tracks metrics)
|
||||
↓
|
||||
useGameStatus (manages lifecycle)
|
||||
↓
|
||||
GameManager (unified interface)
|
||||
↓
|
||||
Platform/UI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Requirements
|
||||
|
||||
These elements MUST be present in every VIMazing game:
|
||||
|
||||
### 1. Entry Point
|
||||
|
||||
```typescript
|
||||
export function useGame(
|
||||
options: GameOptions,
|
||||
platformHook?: unknown
|
||||
): GameManager
|
||||
```
|
||||
|
||||
**Required:**
|
||||
- Accept options object (game-specific structure)
|
||||
- Support optional platform hook function
|
||||
- Return GameManager interface
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// vim-sudoku
|
||||
useGame({ difficulty: 'easy', timeLimit: 600 })
|
||||
|
||||
// vim-maze
|
||||
useGame({ rows: 24, cols: 32, timeLimit: 600 })
|
||||
```
|
||||
|
||||
### 2. GameManager Interface
|
||||
|
||||
Every game must return a GameManager with these minimum fields:
|
||||
|
||||
```typescript
|
||||
type GameManager = {
|
||||
// Required Core
|
||||
containerRef: RefObject<HTMLDivElement | null>; // DOM mounting point
|
||||
renderBoard: () => void; // Force re-render
|
||||
|
||||
// Required State Management
|
||||
gameStatus: GameStatus;
|
||||
setGameStatus: (status: GameStatus) => void;
|
||||
startGame: () => void;
|
||||
quitGame: () => void;
|
||||
|
||||
// Required Managers
|
||||
cursor: CursorManager; // Player interaction
|
||||
scoreManager: ScoreManager; // Metrics and scoring
|
||||
|
||||
// Required Input Tracking
|
||||
keyLog: KeyLogEntry[];
|
||||
clearKeyLog: () => void;
|
||||
getKeyLog: () => KeyLogEntry[];
|
||||
|
||||
// Optional: Game-specific additions
|
||||
// [additional fields as needed]
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Game Status State Machine
|
||||
|
||||
All games must implement this lifecycle:
|
||||
|
||||
```
|
||||
waiting → started → game-won
|
||||
↓
|
||||
game-over
|
||||
```
|
||||
|
||||
**Required States:**
|
||||
- `waiting` - Initial state, game not started
|
||||
- `started` - Game in progress
|
||||
- `game-won` - Successfully completed
|
||||
- `game-over` - Failed to complete
|
||||
|
||||
**Optional States:**
|
||||
- `paused` - Temporarily stopped
|
||||
- Game-specific states (e.g., vim-maze's `has-key`)
|
||||
|
||||
### 4. Cursor/Input Manager
|
||||
|
||||
```typescript
|
||||
type CursorManager = {
|
||||
position: () => Coord; // Current position
|
||||
mode: () => CursorMode; // Current mode
|
||||
|
||||
// Movement (VIM-style)
|
||||
moveLeft: (count?: number) => void;
|
||||
moveRight: (count?: number) => void;
|
||||
moveUp: (count?: number) => void;
|
||||
moveDown: (count?: number) => void;
|
||||
|
||||
// Anchors (VIM-style)
|
||||
moveToStart: () => void; // ^ or 0
|
||||
moveToEnd: () => void; // $
|
||||
moveToTop: () => void; // gg
|
||||
moveToBottom: () => void; // G
|
||||
|
||||
// Repeat (VIM-style)
|
||||
repeatLastMotion: () => void; // .
|
||||
|
||||
// Optional: Game-specific methods
|
||||
// [additional methods as needed]
|
||||
};
|
||||
```
|
||||
|
||||
### 5. Score Manager (Minimal Interface)
|
||||
|
||||
```typescript
|
||||
type ScoreManager = {
|
||||
// Required Timing
|
||||
timeValue: number; // Current time in milliseconds
|
||||
|
||||
// Required Metrics
|
||||
totalKeystrokes: number; // Total keys pressed
|
||||
|
||||
// Required Result
|
||||
finalScore: number | null; // 0-1000 (null until game-won)
|
||||
|
||||
// Optional: Game-specific metrics
|
||||
// [additional fields as needed]
|
||||
};
|
||||
```
|
||||
|
||||
**Scoring Requirements:**
|
||||
- Score range: 0-1000 (capped)
|
||||
- Score calculated on game-won
|
||||
- Based on time and keystrokes (minimum)
|
||||
- Can include game-specific penalties/bonuses
|
||||
|
||||
---
|
||||
|
||||
## Game-Specific Flexibility
|
||||
|
||||
These elements CAN vary between games:
|
||||
|
||||
### 1. GameOptions Structure
|
||||
|
||||
Each game defines its own options:
|
||||
|
||||
```typescript
|
||||
// vim-sudoku
|
||||
type GameOptions = {
|
||||
difficulty?: 'easy' | 'medium' | 'hard';
|
||||
timeLimit?: number;
|
||||
removedCells?: number;
|
||||
};
|
||||
|
||||
// vim-maze
|
||||
type GameOptions = {
|
||||
rows: number;
|
||||
cols: number;
|
||||
timeLimit?: number;
|
||||
};
|
||||
|
||||
// vim-snake (hypothetical)
|
||||
type GameOptions = {
|
||||
boardSize?: number;
|
||||
speed?: 'slow' | 'medium' | 'fast';
|
||||
obstacles?: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
**Recommended Defaults:**
|
||||
- Always provide sensible defaults
|
||||
- Make most options optional
|
||||
- Use consistent naming (e.g., `timeLimit` always in seconds)
|
||||
|
||||
### 2. Scoring Implementation
|
||||
|
||||
Complete freedom in how scoring works internally:
|
||||
|
||||
**vim-sudoku approach:**
|
||||
```typescript
|
||||
// Simple formula: time + keystrokes + hints
|
||||
const baseScore = 1000 - timePenalty - keystrokePenalty - hintPenalty;
|
||||
const finalScore = min(1000, baseScore * difficultyMultiplier);
|
||||
```
|
||||
|
||||
**vim-maze approach:**
|
||||
```typescript
|
||||
// Size-based: time + keystrokes × maze size
|
||||
const baseScore = 1000 - timePenalty - keystrokePenalty;
|
||||
const sizeMultiplier = max(1.0, (rows × cols) / 500);
|
||||
const finalScore = min(1000, baseScore × sizeMultiplier);
|
||||
```
|
||||
|
||||
**vim-snake approach (hypothetical):**
|
||||
```typescript
|
||||
// Performance-based: length achieved vs time
|
||||
const lengthScore = (snakeLength / maxLength) × 500;
|
||||
const timeBonus = (timeRemaining / timeLimit) × 500;
|
||||
const finalScore = min(1000, lengthScore + timeBonus);
|
||||
```
|
||||
|
||||
### 3. Rendering Strategy
|
||||
|
||||
Games choose their own rendering approach:
|
||||
|
||||
**DOM Rendering (vim-maze, vim-sudoku):**
|
||||
```typescript
|
||||
class GameRenderer {
|
||||
constructor(container: HTMLElement) { }
|
||||
render(data: GameData): void {
|
||||
// Direct DOM manipulation
|
||||
container.innerHTML = generateHTML(data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**React Rendering (hypothetical):**
|
||||
```typescript
|
||||
function GameBoard({ boardState }: Props) {
|
||||
return (
|
||||
<div>
|
||||
{boardState.map(cell => <Cell key={cell.id} {...cell} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Canvas Rendering (hypothetical):**
|
||||
```typescript
|
||||
class CanvasRenderer {
|
||||
render(ctx: CanvasRenderingContext2D, gameState: State): void {
|
||||
// Direct canvas drawing
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Internal State Management
|
||||
|
||||
Games manage state however they need:
|
||||
|
||||
**vim-sudoku:**
|
||||
- Board as 81-character string
|
||||
- Initial board (immutable)
|
||||
- Solution board (for validation)
|
||||
- No complex animation state
|
||||
|
||||
**vim-maze:**
|
||||
- 2D array of maze cells
|
||||
- Hero position tracking
|
||||
- Animation state for multi-step movements
|
||||
- Distance calculations for pathfinding
|
||||
|
||||
**vim-snake (hypothetical):**
|
||||
- Snake segments array
|
||||
- Food position
|
||||
- Direction queue
|
||||
- Collision detection
|
||||
|
||||
### 5. Additional Features
|
||||
|
||||
Games can add unique features:
|
||||
|
||||
**vim-sudoku specific:**
|
||||
- Hint system (Shift+H)
|
||||
- Edit modes (i/r/c for different entry styles)
|
||||
- Cell validation (given vs user-entered)
|
||||
|
||||
**vim-maze specific:**
|
||||
- Key pickup mechanic
|
||||
- Wall collision detection
|
||||
- Entrance/exit system
|
||||
- Distance tracking
|
||||
|
||||
**vim-snake specific (hypothetical):**
|
||||
- Power-ups
|
||||
- Speed changes
|
||||
- Obstacle generation
|
||||
- Multiplayer
|
||||
|
||||
---
|
||||
|
||||
## Hook Specifications
|
||||
|
||||
### useBoard
|
||||
|
||||
**Purpose:** Manage game board/level state and rendering.
|
||||
|
||||
**Flexibility:** Complete freedom in implementation.
|
||||
|
||||
**Minimum Interface:**
|
||||
```typescript
|
||||
type BoardManager = {
|
||||
containerRef: RefObject<HTMLDivElement | null>;
|
||||
renderBoard: () => void;
|
||||
|
||||
// Game-specific additions allowed
|
||||
};
|
||||
```
|
||||
|
||||
**Common Patterns:**
|
||||
```typescript
|
||||
// Pattern 1: Generator + Renderer classes
|
||||
const generator = new GameGenerator(options);
|
||||
const renderer = new GameRenderer(container);
|
||||
generator.generate();
|
||||
renderer.render(generator.getData());
|
||||
|
||||
// Pattern 2: Direct state management
|
||||
const [board, setBoard] = useState(generateInitial());
|
||||
renderBoard(board);
|
||||
|
||||
// Pattern 3: Canvas-based
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
drawBoard(canvasRef.current, boardState);
|
||||
```
|
||||
|
||||
### useCursor
|
||||
|
||||
**Purpose:** Handle player input and interaction.
|
||||
|
||||
**Required:** VIM-style navigation (hjkl, counts, anchors, repeat).
|
||||
|
||||
**Minimum Interface:**
|
||||
```typescript
|
||||
type CursorManager = {
|
||||
position: () => Coord;
|
||||
mode: () => CursorMode;
|
||||
|
||||
// VIM motions (required)
|
||||
moveLeft: (count?: number) => void;
|
||||
moveRight: (count?: number) => void;
|
||||
moveUp: (count?: number) => void;
|
||||
moveDown: (count?: number) => void;
|
||||
moveToStart: () => void;
|
||||
moveToEnd: () => void;
|
||||
moveToTop: () => void;
|
||||
moveToBottom: () => void;
|
||||
repeatLastMotion: () => void;
|
||||
|
||||
// Game-specific additions allowed
|
||||
};
|
||||
```
|
||||
|
||||
**Mode System:**
|
||||
All games use at minimum:
|
||||
- `normal` - Navigation mode
|
||||
- `edit` - Interaction mode (if applicable)
|
||||
|
||||
Games can add modes as needed (e.g., sudoku's distinction between i/r/c entry modes).
|
||||
|
||||
### useScore
|
||||
|
||||
**Purpose:** Track metrics and calculate final score.
|
||||
|
||||
**Flexibility:** Almost complete freedom.
|
||||
|
||||
**Minimum Interface:**
|
||||
```typescript
|
||||
type ScoreManager = {
|
||||
timeValue: number;
|
||||
totalKeystrokes: number;
|
||||
finalScore: number | null;
|
||||
|
||||
// Game-specific metrics allowed
|
||||
};
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
- Track time in milliseconds
|
||||
- Track total keystrokes
|
||||
- Calculate score 0-1000 (capped)
|
||||
- Set finalScore only on game-won
|
||||
|
||||
**Optional but Recommended:**
|
||||
- Game-over conditions based on time/performance
|
||||
- Additional metrics for display
|
||||
- Difficulty multipliers
|
||||
|
||||
### useGameStatus
|
||||
|
||||
**Purpose:** Manage game lifecycle state machine.
|
||||
|
||||
**Required States:**
|
||||
```typescript
|
||||
type GameStatus =
|
||||
| 'waiting'
|
||||
| 'started'
|
||||
| 'game-won'
|
||||
| 'game-over'
|
||||
| ... // Optional game-specific states
|
||||
```
|
||||
|
||||
**Minimum Interface:**
|
||||
```typescript
|
||||
type GameStatusManager = {
|
||||
gameStatus: GameStatus;
|
||||
setGameStatus: (status: GameStatus) => void;
|
||||
startGame: () => void;
|
||||
quitGame: () => void;
|
||||
|
||||
// Optional: togglePause, reset, etc.
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Type System
|
||||
|
||||
### Required Core Types
|
||||
|
||||
```typescript
|
||||
// Coordinates (all games use this)
|
||||
type Coord = { row: number; col: number };
|
||||
|
||||
// Key logging (all games track this)
|
||||
type KeyLogEntry = { key: string; timestamp: number };
|
||||
|
||||
// Game status (required states)
|
||||
type GameStatus = 'waiting' | 'started' | 'game-won' | 'game-over' | ...;
|
||||
|
||||
// Cursor mode (at minimum)
|
||||
type CursorMode = 'normal' | 'edit' | ...;
|
||||
```
|
||||
|
||||
### Type Organization
|
||||
|
||||
Recommended structure:
|
||||
```
|
||||
src/
|
||||
├── types.ts # Game-specific exports + core types
|
||||
├── useBoard/
|
||||
│ └── types.ts # Board-specific types
|
||||
├── useCursor/
|
||||
│ └── types.ts # Cursor-specific types
|
||||
├── useGameStatus/
|
||||
│ └── types.ts # Status-specific types
|
||||
└── useScore/
|
||||
└── types.ts # Score-specific types
|
||||
```
|
||||
|
||||
**Main types.ts pattern:**
|
||||
```typescript
|
||||
// Re-export all module types
|
||||
export * from './useBoard/types';
|
||||
export * from './useCursor/types';
|
||||
export * from './useGameStatus/types';
|
||||
export * from './useScore/types';
|
||||
|
||||
// Game-specific types
|
||||
export type GameOptions = { /* game-specific */ };
|
||||
export type GameManager = { /* unified interface */ };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Game Instructions Export
|
||||
|
||||
### Required Export
|
||||
|
||||
All games must export a `gameInfo` object containing structured documentation for platform consumption.
|
||||
|
||||
**File:** `src/gameInfo.ts`
|
||||
|
||||
```typescript
|
||||
export const gameInfo = {
|
||||
name: string; // Game name
|
||||
description: string; // Brief description
|
||||
|
||||
controls: {
|
||||
navigation: Array<{ keys: string; description: string }>;
|
||||
// Additional control sections as needed
|
||||
game: Array<{ keys: string; description: string }>;
|
||||
};
|
||||
|
||||
rules: {
|
||||
// Game-specific rule sections
|
||||
};
|
||||
|
||||
scoring: {
|
||||
formula: string;
|
||||
range: string;
|
||||
penalties: Array<{ factor: string; formula: string; example: string }>;
|
||||
examples: Array<any>;
|
||||
};
|
||||
|
||||
gameOver: {
|
||||
conditions: Array<{ type: string; trigger: string; message: string }>;
|
||||
};
|
||||
|
||||
objective: string; // How to win (one sentence)
|
||||
winCondition: string; // Specific win trigger
|
||||
} as const;
|
||||
|
||||
export type GameInfo = typeof gameInfo;
|
||||
```
|
||||
|
||||
**Export from main index:**
|
||||
```typescript
|
||||
// src/index.ts
|
||||
export { gameInfo } from './gameInfo';
|
||||
export type { GameInfo } from './gameInfo';
|
||||
```
|
||||
|
||||
### Purpose
|
||||
|
||||
The `gameInfo` export allows platforms to:
|
||||
- Render in-game help screens
|
||||
- Generate tutorials
|
||||
- Display control references
|
||||
- Show scoring breakdowns
|
||||
- Explain game mechanics
|
||||
|
||||
Without needing to parse documentation or README files.
|
||||
|
||||
### Usage by Platforms
|
||||
|
||||
```typescript
|
||||
import { gameInfo } from '@vimazing/your-game';
|
||||
|
||||
// Render controls
|
||||
gameInfo.controls.navigation.forEach(control => {
|
||||
renderControl(control.keys, control.description);
|
||||
});
|
||||
|
||||
// Show scoring formula
|
||||
displayScoringInfo(gameInfo.scoring.formula, gameInfo.scoring.penalties);
|
||||
|
||||
// Display game-over conditions
|
||||
gameInfo.gameOver.conditions.forEach(condition => {
|
||||
showCondition(condition.type, condition.trigger);
|
||||
});
|
||||
```
|
||||
|
||||
### Required Sections
|
||||
|
||||
**Minimum structure:**
|
||||
- `name` - Game name
|
||||
- `description` - Brief description
|
||||
- `controls.navigation` - VIM motion controls (required)
|
||||
- `controls.game` - Game lifecycle controls (space, q, p)
|
||||
- `rules` - At least one rule section
|
||||
- `scoring.formula` - Score calculation
|
||||
- `scoring.range` - Score bounds (always "0 - 1000 points")
|
||||
- `gameOver.conditions` - At least one condition
|
||||
- `objective` - Win goal
|
||||
- `winCondition` - Specific trigger
|
||||
|
||||
**Optional sections:**
|
||||
- Additional control groups (editing, actions, etc.)
|
||||
- Visual feedback explanations
|
||||
- Hints/power-ups/special mechanics
|
||||
- Examples and breakdowns
|
||||
|
||||
### Examples
|
||||
|
||||
See implementations:
|
||||
- **vim-sudoku/src/gameInfo.ts** - Complex puzzle game with modes, hints
|
||||
- **vim-maze/src/gameInfo.ts** - Navigation game with maze mechanics
|
||||
- **vimazing-game-skeleton/src/gameInfo.ts** - Template with TODOs
|
||||
|
||||
---
|
||||
|
||||
## Creating a New Game
|
||||
|
||||
### Step 1: Define Your Game
|
||||
|
||||
Answer these questions:
|
||||
- What is the game mechanic?
|
||||
- What does the board/level look like?
|
||||
- How does the player interact?
|
||||
- What constitutes winning/losing?
|
||||
- How should scoring work?
|
||||
|
||||
### Step 2: Design GameOptions
|
||||
|
||||
```typescript
|
||||
export type GameOptions = {
|
||||
// Required configuration
|
||||
[key: string]: any;
|
||||
|
||||
// Recommended: timeLimit
|
||||
timeLimit?: number; // Seconds
|
||||
};
|
||||
```
|
||||
|
||||
### Step 3: Implement Core Hooks
|
||||
|
||||
**useBoard:**
|
||||
```typescript
|
||||
export function useBoard(/* game-specific params */) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
function renderBoard() {
|
||||
// Your rendering logic
|
||||
}
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
renderBoard,
|
||||
// Additional methods
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**useCursor:**
|
||||
```typescript
|
||||
export function useCursor(/* dependencies */) {
|
||||
const [position, setPosition] = useState<Coord>({ row: 0, col: 0 });
|
||||
const [mode, setMode] = useState<CursorMode>('normal');
|
||||
|
||||
// Implement VIM motions
|
||||
function moveLeft(count = 1) { /* ... */ }
|
||||
function moveRight(count = 1) { /* ... */ }
|
||||
// ... etc
|
||||
|
||||
return {
|
||||
position: () => position,
|
||||
mode: () => mode,
|
||||
moveLeft,
|
||||
moveRight,
|
||||
// ... etc
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**useScore:**
|
||||
```typescript
|
||||
export function useScore(/* dependencies */) {
|
||||
const [timeValue, setTimeValue] = useState(0);
|
||||
const [finalScore, setFinalScore] = useState<number | null>(null);
|
||||
const totalKeystrokes = keyLog.length;
|
||||
|
||||
// Calculate score on game-won
|
||||
useEffect(() => {
|
||||
if (gameStatus === 'game-won') {
|
||||
const score = calculateScore(timeValue, totalKeystrokes);
|
||||
setFinalScore(Math.min(1000, Math.max(0, score)));
|
||||
}
|
||||
}, [gameStatus]);
|
||||
|
||||
return {
|
||||
timeValue,
|
||||
totalKeystrokes,
|
||||
finalScore,
|
||||
// Additional metrics
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**useGameStatus:**
|
||||
```typescript
|
||||
export function useGameStatus() {
|
||||
const [gameStatus, setGameStatus] = useState<GameStatus>('waiting');
|
||||
|
||||
function startGame() {
|
||||
setGameStatus('started');
|
||||
}
|
||||
|
||||
function quitGame() {
|
||||
setGameStatus('waiting');
|
||||
}
|
||||
|
||||
return {
|
||||
gameStatus,
|
||||
setGameStatus,
|
||||
startGame,
|
||||
quitGame,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Compose in useGame
|
||||
|
||||
```typescript
|
||||
export function useGame(
|
||||
options: GameOptions,
|
||||
platformHook?: unknown
|
||||
): GameManager {
|
||||
const board = useBoard(/* options */);
|
||||
const gameStatusManager = useGameStatus();
|
||||
const cursor = useCursor(/* dependencies */);
|
||||
const scoreManager = useScore(/* dependencies */);
|
||||
|
||||
const gameManager: GameManager = {
|
||||
containerRef: board.containerRef,
|
||||
renderBoard: board.renderBoard,
|
||||
gameStatus: gameStatusManager.gameStatus,
|
||||
setGameStatus: gameStatusManager.setGameStatus,
|
||||
startGame: gameStatusManager.startGame,
|
||||
quitGame: gameStatusManager.quitGame,
|
||||
cursor,
|
||||
scoreManager,
|
||||
keyLog: [], // Implement key tracking
|
||||
clearKeyLog: () => {},
|
||||
getKeyLog: () => [],
|
||||
};
|
||||
|
||||
if (typeof platformHook === 'function') {
|
||||
platformHook(gameManager);
|
||||
}
|
||||
|
||||
return gameManager;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Export Public API
|
||||
|
||||
```typescript
|
||||
// src/index.ts
|
||||
export { useGame } from './useGame';
|
||||
export type { GameManager, GameOptions } from './types';
|
||||
export type { CursorManager } from './useCursor/types';
|
||||
export type { ScoreManager } from './useScore/types';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Guide
|
||||
|
||||
### For Game Consumers
|
||||
|
||||
**Basic Usage:**
|
||||
```typescript
|
||||
import { useGame } from '@vimazing/your-game';
|
||||
import '@vimazing/your-game/game.css';
|
||||
|
||||
function GameComponent() {
|
||||
const game = useGame(options);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div ref={game.containerRef} />
|
||||
<button onClick={game.startGame}>Start</button>
|
||||
<p>Status: {game.gameStatus}</p>
|
||||
<p>Score: {game.scoreManager.finalScore || 0} / 1000</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**With Platform Hook:**
|
||||
```typescript
|
||||
function platformHook(gameManager: GameManager) {
|
||||
// Analytics
|
||||
trackGameStart(gameManager.gameStatus);
|
||||
|
||||
// Custom keybindings
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'h') trackHelp();
|
||||
});
|
||||
|
||||
// Monitor completion
|
||||
const interval = setInterval(() => {
|
||||
if (gameManager.gameStatus === 'game-won') {
|
||||
trackVictory(gameManager.scoreManager.finalScore);
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
const game = useGame(options, platformHook);
|
||||
```
|
||||
|
||||
### For Platform Developers
|
||||
|
||||
All games expose the same core interface:
|
||||
```typescript
|
||||
interface UnifiedGame {
|
||||
useGame(options: any, platformHook?: Function): GameManager;
|
||||
}
|
||||
```
|
||||
|
||||
**Integration checklist:**
|
||||
- ✅ Game exposes `useGame` entry point
|
||||
- ✅ Returns `GameManager` with required fields
|
||||
- ✅ Accepts optional platform hook
|
||||
- ✅ Implements core game status states
|
||||
- ✅ Provides VIM-style cursor navigation
|
||||
- ✅ Scores on 0-1000 scale
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
**v2.0** (2025-01-22)
|
||||
- Rewritten for game-agnostic specification
|
||||
- Clarified requirements vs flexibility
|
||||
- Added comprehensive creation guide
|
||||
- Removed game-specific implementation details
|
||||
- Simplified scoring requirements
|
||||
|
||||
**v1.0** (2024)
|
||||
- Initial unified API documentation
|
||||
- Based on vim-maze implementation
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT © VIMazing Project
|
||||
+3
-3
@@ -5,13 +5,13 @@ import { useKeyBindings } from "./useKeyBindings";
|
||||
|
||||
function App() {
|
||||
const [gameMode, setGameMode] = useState<GameMode>("letters");
|
||||
const gameManager = useGame({ initialGameMode: gameMode }, useKeyBindings);
|
||||
const gameManager = useGame({ gameMode }, useKeyBindings);
|
||||
const { containerRef, gameStatus, scoreManager } = gameManager;
|
||||
|
||||
useEffect(() => {
|
||||
if (scoreManager.score === 5 && gameMode === "letters") {
|
||||
setGameMode("words");
|
||||
gameManager.changeGameMode("words");
|
||||
gameManager.changeGameMode({ gameMode: "words" });
|
||||
}
|
||||
}, [scoreManager.score, gameMode, gameManager]);
|
||||
|
||||
@@ -32,7 +32,7 @@ function App() {
|
||||
key={mode}
|
||||
onClick={(e) => {
|
||||
setGameMode(mode);
|
||||
gameManager.changeGameMode(mode);
|
||||
gameManager.changeGameMode({ gameMode: mode });
|
||||
e.currentTarget.blur(); // Remove focus from button
|
||||
}}
|
||||
className={`px-3 py-1 rounded capitalize font-medium transition ${gameMode === mode
|
||||
|
||||
+5
-2
@@ -61,7 +61,10 @@ export type FallingLetter = {
|
||||
export type KeyLogEntry = { key: string; timestamp: number };
|
||||
|
||||
export type GameOptions = {
|
||||
initialGameMode?: GameMode;
|
||||
gameMode?: GameMode; // Game mode (defaults to 'letters')
|
||||
downwardSpeed?: number; // Falling text speed in px/s (defaults to mode's built-in speed)
|
||||
boardDimensions?: [number, number]; // Canvas dimensions [width, height] (defaults to [800, 600])
|
||||
wordList?: string[]; // Custom word/sentence/paragraph list (optional, mode-agnostic)
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -171,7 +174,7 @@ export type GameManager = {
|
||||
|
||||
// Game-specific
|
||||
handleTypedLetter: (letter: string) => void;
|
||||
changeGameMode: (mode: GameMode) => void;
|
||||
changeGameMode: (options: GameOptions) => void;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -6,16 +6,17 @@ import { useGameStatus } from "../useGameStatus";
|
||||
import { useScore } from "../useScore";
|
||||
|
||||
export function useGame(
|
||||
options?: GameOptions,
|
||||
initialOptions?: GameOptions,
|
||||
platformHook?: unknown
|
||||
): GameManager {
|
||||
const [keyLog, setKeyLog] = useState<KeyLogEntry[]>([]);
|
||||
const [gameMode, setGameModeState] = useState<GameMode>(options?.initialGameMode ?? 'letters');
|
||||
const [options, setOptions] = useState<GameOptions>(initialOptions ?? {});
|
||||
const gameMode = options.gameMode ?? 'letters';
|
||||
const gameModeRef = useRef<GameMode>(gameMode);
|
||||
|
||||
const boardManager = useBoard();
|
||||
const cursorManager = useCursor();
|
||||
const gameStatusManager = useGameStatus(undefined, gameMode);
|
||||
const gameStatusManager = useGameStatus(undefined, options);
|
||||
|
||||
// Wrap renderBoard to include player position and lasers
|
||||
const renderBoardWrapped = (letters: any = [], lasers: Laser[] = [], shipState: ShipState = 'normal', shipExplosionTime: number = 0) => {
|
||||
@@ -68,8 +69,8 @@ export function useGame(
|
||||
gameStatusManager.setGameMode(gameMode);
|
||||
}, [gameMode]);
|
||||
|
||||
const changeGameMode = (mode: GameMode) => {
|
||||
setGameModeState(mode);
|
||||
const changeGameMode = (newOptions: GameOptions) => {
|
||||
setOptions(newOptions);
|
||||
// Don't quit - just switch modes and let game continue
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useRef } from "react";
|
||||
import type { GameStatus, FallingLetter, ShipState, GameMode, Laser } from "../types";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import type { GameStatus, FallingLetter, ShipState, GameMode, Laser, GameOptions } from "../types";
|
||||
import { getRandomWord } from "../wordLists";
|
||||
|
||||
const LETTER_SPEEDS: Record<GameMode, number> = {
|
||||
@@ -15,7 +15,24 @@ const MARGIN_RIGHT = 50; // Safe margin from right edge
|
||||
const CANVAS_WIDTH = 800;
|
||||
const SPACESHIP_Y = 600 - 40; // Position of the ship on canvas
|
||||
|
||||
export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: FallingLetter[], lasers: Laser[]) => void, gameMode: GameMode = 'letters') {
|
||||
export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: FallingLetter[], lasers: Laser[]) => void, options: GameOptions = {}) {
|
||||
const gameMode = options.gameMode ?? 'letters';
|
||||
const downwardSpeed = options.downwardSpeed;
|
||||
const wordList = options.wordList;
|
||||
|
||||
// Store options in refs so they can be accessed in the game loop
|
||||
const downwardSpeedRef = useRef<number | undefined>(downwardSpeed);
|
||||
const wordListRef = useRef<string[] | undefined>(wordList);
|
||||
|
||||
// Update refs when options change
|
||||
useEffect(() => {
|
||||
downwardSpeedRef.current = downwardSpeed;
|
||||
}, [downwardSpeed]);
|
||||
|
||||
useEffect(() => {
|
||||
wordListRef.current = wordList;
|
||||
}, [wordList]);
|
||||
|
||||
const [gameStatus, setGameStatus] = useState<GameStatus>("waiting");
|
||||
const shipStateRef = useRef<ShipState>("normal");
|
||||
const shipExplosionTimeRef = useRef<number>(0);
|
||||
@@ -39,7 +56,7 @@ export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: Fal
|
||||
|
||||
// Spawn new letter/word if enough time has passed and no letters active
|
||||
if (currentTime - lastSpawnRef.current > SPAWN_INTERVAL && lettersRef.current.length < MAX_ACTIVE_LETTERS) {
|
||||
const fullText = getRandomWord(gameModeRef.current);
|
||||
const fullText = getRandomWord(gameModeRef.current, wordListRef.current);
|
||||
const firstChar = fullText[0].toUpperCase();
|
||||
|
||||
// For sentences/paragraphs, spawn centered. For letters/words, spawn randomly
|
||||
@@ -62,7 +79,7 @@ export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: Fal
|
||||
}
|
||||
|
||||
// Update letter positions
|
||||
const speed = LETTER_SPEEDS[gameModeRef.current];
|
||||
const speed = downwardSpeedRef.current ?? LETTER_SPEEDS[gameModeRef.current];
|
||||
lettersRef.current.forEach(letter => {
|
||||
letter.y += (speed * deltaTime) / 1000;
|
||||
});
|
||||
|
||||
+4
-2
@@ -41,7 +41,9 @@ export function getRandomItem<T>(array: readonly T[]): T {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
|
||||
export function getRandomWord(gameMode: 'letters' | 'words' | 'sentences' | 'paragraphs'): string {
|
||||
const list = wordLists[gameMode];
|
||||
export function getRandomWord(gameMode: 'letters' | 'words' | 'sentences' | 'paragraphs', customList?: string[]): string {
|
||||
// Use custom list if provided, otherwise use default for the game mode
|
||||
// Letters mode always uses default alphabet
|
||||
const list = customList && gameMode !== 'letters' ? customList : wordLists[gameMode];
|
||||
return String(getRandomItem(list));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user