Files
vim-maze/UNIFIED_API.md
T

17 KiB
Raw Blame History

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
  2. Architecture Overview
  3. Core Requirements
  4. Game-Specific Flexibility
  5. Hook Specifications
  6. Type System
  7. Creating a New Game
  8. 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

export function useGame(
  options: GameOptions,
  platformHook?: unknown
): GameManager

Required:

  • Accept options object (game-specific structure)
  • Support optional platform hook function
  • Return GameManager interface

Example:

// 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:

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

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)

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:

// 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:

// Simple formula: time + keystrokes + hints
const baseScore = 1000 - timePenalty - keystrokePenalty - hintPenalty;
const finalScore = min(1000, baseScore * difficultyMultiplier);

vim-maze approach:

// 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):

// 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):

class GameRenderer {
  constructor(container: HTMLElement) { }
  render(data: GameData): void {
    // Direct DOM manipulation
    container.innerHTML = generateHTML(data);
  }
}

React Rendering (hypothetical):

function GameBoard({ boardState }: Props) {
  return (
    <div>
      {boardState.map(cell => <Cell key={cell.id} {...cell} />)}
    </div>
  );
}

Canvas Rendering (hypothetical):

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:

type BoardManager = {
  containerRef: RefObject<HTMLDivElement | null>;
  renderBoard: () => void;
  
  // Game-specific additions allowed
};

Common Patterns:

// 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:

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:

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:

type GameStatus = 
  | 'waiting'
  | 'started'
  | 'game-won'
  | 'game-over'
  | ...  // Optional game-specific states

Minimum Interface:

type GameStatusManager = {
  gameStatus: GameStatus;
  setGameStatus: (status: GameStatus) => void;
  startGame: () => void;
  quitGame: () => void;
  
  // Optional: togglePause, reset, etc.
};

Type System

Required Core Types

// 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:

// 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

export type GameOptions = {
  // Required configuration
  [key: string]: any;
  
  // Recommended: timeLimit
  timeLimit?: number;  // Seconds
};

Step 3: Implement Core Hooks

useBoard:

export function useBoard(/* game-specific params */) {
  const containerRef = useRef<HTMLDivElement>(null);
  
  function renderBoard() {
    // Your rendering logic
  }
  
  return {
    containerRef,
    renderBoard,
    // Additional methods
  };
}

useCursor:

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:

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:

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

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

// 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:

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:

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:

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