game-dev
npx skills add https://github.com/hypersocialinc/cli-games --skill game-dev
Agent 安装分布
Skill 文档
Game Dev Skill
This skill enables rapid creation of terminal-based games for @hypersocial/cli-games.
Quick Start
- Get game concept and name from user
- Create game file at
src/games/{name}/index.ts - Register in
src/games/index.ts - Follow patterns below for consistent look and feel
Architecture Overview
All games share these characteristics:
- GameController interface:
stop()method andisRunninggetter - State machine:
gameStarted,gameOver,paused,wonflags - Shared pause menu: Import from
../shared/menu - Shared effects: Import from
../shared/effects - Theme awareness: Use
getCurrentThemeColor()from../utils - Terminal rendering: ANSI escape codes, alternate buffer mode
File Structure
src/games/
âââ {gamename}/
â âââ index.ts # Main game file (required)
â âââ effects.ts # Complex games: separate effects
âââ shared/
â âââ menu.ts # Shared pause menu system
â âââ effects.ts # Shared particle, popup, shake, flash utilities
â âââ index.ts # Re-exports
âââ gameTransitions.ts # Quit/switch game helpers
âââ utils.ts # Theme colors, utilities
âââ index.ts # Game registry
Required Imports
import type { Terminal } from '@xterm/xterm';
import { getCurrentThemeColor } from '../utils';
import { dispatchGameQuit, dispatchGameSwitch, dispatchGamesMenu } from '../gameTransitions';
import { PAUSE_MENU_ITEMS, renderSimpleMenu, navigateMenu } from '../shared/menu';
For effects, import from the shared module:
import {
type Particle,
type ScorePopup,
spawnParticles,
updateParticles,
addScorePopup,
updatePopups,
triggerShake,
applyShake,
createShakeState,
MAX_PARTICLES,
PARTICLE_CHARS,
} from '../shared/effects';
GameController Interface
Every game exports a controller with this shape:
export interface {Name}Controller {
stop: () => void;
isRunning: boolean;
}
State Machine Pattern
let running = true;
let gameStarted = false;
let gameOver = false;
let paused = false;
let pauseMenuSelection = 0;
let won = false;
State transitions:
- Start:
!gameStarted && !paused-> any key ->gameStarted = true - Pause: ESC toggles
paused, resetpauseMenuSelection = 0 - Game Over: collision/loss ->
gameOver = true, optionallywon = true - Quit: Q key or menu selection ->
controller.stop()+dispatchGameQuit()
Pause Menu Integration
CRITICAL: Always use the shared menu system:
import { PAUSE_MENU_ITEMS, renderSimpleMenu, navigateMenu } from '../shared/menu';
// In render():
if (paused) {
output += renderSimpleMenu(PAUSE_MENU_ITEMS, pauseMenuSelection, {
centerX: Math.floor(cols / 2),
startY: pauseY + 2,
showShortcuts: false,
});
}
// In key handler:
if (paused) {
const { newSelection, confirmed } = navigateMenu(
pauseMenuSelection,
PAUSE_MENU_ITEMS.length,
key,
domEvent
);
if (newSelection !== pauseMenuSelection) {
pauseMenuSelection = newSelection;
return;
}
if (confirmed) {
switch (pauseMenuSelection) {
case 0: paused = false; break; // Resume
case 1: initGame(); gameStarted = true; paused = false; break; // Restart
case 2: controller.stop(); dispatchGameQuit(terminal); break; // Quit
case 3: dispatchGamesMenu(terminal); break; // List Games
case 4: dispatchGameSwitch(terminal); break; // Next Game
}
}
}
Terminal Size Handling
Always check minimum size and show helpful resize message:
const MIN_COLS = 40;
const MIN_ROWS = 20;
// In render():
if (cols < MIN_COLS || rows < MIN_ROWS) {
const msg1 = 'Terminal too small!';
const needWidth = cols < MIN_COLS;
const needHeight = rows < MIN_ROWS;
let hint = needWidth && needHeight ? 'Make pane larger'
: needWidth ? 'Make pane wider ->' : 'Make pane taller';
const msg2 = `Need: ${MIN_COLS}x${MIN_ROWS} Have: ${cols}x${rows}`;
// Center and render messages...
return;
}
Glitch Title Effect
Every game has a glitchy ASCII title:
const title = [
'{TITLE_LINE_1}',
'{TITLE_LINE_2}',
];
let glitchFrame = 0;
// In render():
glitchFrame = (glitchFrame + 1) % 60;
const glitchOffset = glitchFrame >= 55 ? Math.floor(Math.random() * 3) - 1 : 0;
const titleX = Math.floor((cols - title[0].length) / 2) + glitchOffset;
if (glitchFrame >= 55 && glitchFrame < 58) {
output += `\x1b[1;${titleX}H\x1b[91m${title[0]}\x1b[0m`;
output += `\x1b[2;${titleX + 1}H\x1b[96m${title[1]}\x1b[0m`;
} else {
output += `\x1b[1;${titleX}H${themeColor}\x1b[1m${title[0]}\x1b[0m`;
output += `\x1b[2;${titleX}H${themeColor}\x1b[1m${title[1]}\x1b[0m`;
}
Game Loop Setup
setTimeout(() => {
if (!running) return;
// Enter alternate buffer, hide cursor
terminal.write('\x1b[?1049h');
terminal.write('\x1b[?25l');
initGame();
gameStarted = false;
const renderInterval = setInterval(() => {
if (!running) { clearInterval(renderInterval); return; }
render();
}, 50); // 20 FPS
const gameInterval = setInterval(() => {
if (!running) { clearInterval(gameInterval); return; }
update();
}, 50);
const keyListener = terminal.onKey(({ domEvent }) => {
if (!running) { keyListener.dispose(); return; }
domEvent.preventDefault();
domEvent.stopPropagation();
// Handle input...
});
// Override stop to clean up
const originalStop = controller.stop;
controller.stop = () => {
clearInterval(renderInterval);
clearInterval(gameInterval);
keyListener.dispose();
originalStop();
};
}, 50);
Registering a Game
In src/games/index.ts, add a direct import and entry:
// 1. Add the import at the top with existing imports:
import { run{Name}Game } from './{name}';
// 2. Add to the games array:
export const games: GameInfo[] = [
// ... existing games
{ id: '{name}', name: '{Name}', description: 'Game description', run: run{Name}Game },
];
// 3. Add to the individual game runner exports:
export {
// ... existing exports
run{Name}Game,
};
Effect Patterns
See patterns/effects.md for:
- Particle systems (burst, trail, firework)
- Screen shake
- Score popups
- Flash effects
- Kill streaks and combos
Use the shared effects module at src/games/shared/effects.ts for common effect logic.
Input Patterns
See patterns/input-handling.md for:
- Arrow key navigation
- Action keys (space, etc.)
- ESC/Q handling
- Start screen any-key
Rendering Patterns
See patterns/rendering.md for:
- ANSI escape codes reference
- Border drawing
- Centering content
- Color cycling
- Perspective effects
Game Complexity Guide
Simple (~300-500 lines): Snake, Hangman
- Single file
- Basic collision
- Minimal physics
Medium (~500-800 lines): Tetris, Pong
- Single file with more systems
- AI or grid mechanics
- More effect polish
Complex (~1000+ lines): Chopper
- Multiple files (index.ts, effects.ts)
- Physics, particles, popups
- Level progression
Contributing a Game
- Create your game in
src/games/{name}/index.ts - Register it in
src/games/index.ts - Use shared effects from
../shared/effectsinstead of inlining particle/popup code - Use the shared menu from
../shared/menufor pause/game-over menus - Run
npm run buildandnpm run typecheckto verify - Submit a PR
Template
Use the scaffold template at templates/game-scaffold.ts as a starting point.