From 444eb5c6f4ee15ee9e525873ad966335fd2348f2 Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Wed, 22 Oct 2025 22:31:35 +0100 Subject: [PATCH] docs: add UNIFIED_API v2.0 to vim-snake --- UNIFIED_API.md | 765 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 765 insertions(+) create mode 100644 UNIFIED_API.md diff --git a/UNIFIED_API.md b/UNIFIED_API.md new file mode 100644 index 0000000..38f14ee --- /dev/null +++ b/UNIFIED_API.md @@ -0,0 +1,765 @@ +# 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. [Creating a New Game](#creating-a-new-game) +8. [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; // 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 ( +
+ {boardState.map(cell => )} +
+ ); +} +``` + +**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; + 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(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 */ }; +``` + +--- + +## 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(null); + + function renderBoard() { + // Your rendering logic + } + + return { + containerRef, + renderBoard, + // Additional methods + }; +} +``` + +**useCursor:** +```typescript +export function useCursor(/* dependencies */) { + const [position, setPosition] = useState({ row: 0, col: 0 }); + const [mode, setMode] = useState('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(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('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 ( +
+
+ +

Status: {game.gameStatus}

+

Score: {game.scoreManager.finalScore || 0} / 1000

+
+ ); +} +``` + +**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