game-dev

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Game Dev Skill

游戏开发技能

This skill enables rapid creation of terminal-based games for
@hypersocial/cli-games
.
本技能可用于为
@hypersocial/cli-games
快速创建基于终端的游戏。

Quick Start

快速开始

  1. Get game concept and name from user
  2. Create game file at
    src/games/{name}/index.ts
  3. Register in
    src/games/index.ts
  4. Follow patterns below for consistent look and feel
  1. 从用户处获取游戏概念和名称
  2. src/games/{name}/index.ts
    路径下创建游戏文件
  3. src/games/index.ts
    中注册游戏
  4. 遵循下方的模式保证统一的外观和使用体验

Architecture Overview

架构总览

All games share these characteristics:
  • GameController interface:
    stop()
    method and
    isRunning
    getter
  • State machine:
    gameStarted
    ,
    gameOver
    ,
    paused
    ,
    won
    flags
  • 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
所有游戏都具备以下特性:
  • GameController 接口:包含
    stop()
    方法和
    isRunning
    getter
  • 状态机:包含
    gameStarted
    gameOver
    paused
    won
    标志位
  • 共享暂停菜单:从
    ../shared/menu
    导入
  • 共享效果:从
    ../shared/effects
    导入
  • 主题感知:使用
    ../utils
    提供的
    getCurrentThemeColor()
    方法
  • 终端渲染:使用ANSI转义码、备用缓冲区模式

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
src/games/
├── {gamename}/
│   ├── index.ts          # 主游戏文件(必填)
│   └── effects.ts        # 复杂游戏:单独存放效果逻辑
├── shared/
│   ├── menu.ts           # 共享暂停菜单系统
│   ├── effects.ts        # 共享粒子、弹窗、震动、闪屏工具
│   └── index.ts          # 重新导出
├── gameTransitions.ts    # 退出/切换游戏辅助工具
├── utils.ts              # 主题颜色、工具函数
└── index.ts              # 游戏注册表

Required Imports

必须导入的模块

typescript
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:
typescript
import {
  type Particle,
  type ScorePopup,
  spawnParticles,
  updateParticles,
  addScorePopup,
  updatePopups,
  triggerShake,
  applyShake,
  createShakeState,
  MAX_PARTICLES,
  PARTICLE_CHARS,
} from '../shared/effects';
typescript
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';
如果需要使用效果,从共享模块导入:
typescript
import {
  type Particle,
  type ScorePopup,
  spawnParticles,
  updateParticles,
  addScorePopup,
  updatePopups,
  triggerShake,
  applyShake,
  createShakeState,
  MAX_PARTICLES,
  PARTICLE_CHARS,
} from '../shared/effects';

GameController Interface

GameController 接口

Every game exports a controller with this shape:
typescript
export interface {Name}Controller {
  stop: () => void;
  isRunning: boolean;
}
每个游戏都要导出符合以下结构的控制器:
typescript
export interface {Name}Controller {
  stop: () => void;
  isRunning: boolean;
}

State Machine Pattern

状态机模式

typescript
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
    , reset
    pauseMenuSelection = 0
  • Game Over: collision/loss ->
    gameOver = true
    , optionally
    won = true
  • Quit: Q key or menu selection ->
    controller.stop()
    +
    dispatchGameQuit()
typescript
let running = true;
let gameStarted = false;
let gameOver = false;
let paused = false;
let pauseMenuSelection = 0;
let won = false;
状态流转:
  • 启动:
    !gameStarted && !paused
    -> 按下任意键 ->
    gameStarted = true
  • 暂停:ESC键切换
    paused
    状态,重置
    pauseMenuSelection = 0
  • 游戏结束:碰撞/失败 ->
    gameOver = true
    ,胜利时可额外设置
    won = true
  • 退出:按下Q键或选择对应菜单选项 ->
    controller.stop()
    +
    dispatchGameQuit()

Pause Menu Integration

暂停菜单集成

CRITICAL: Always use the shared menu system:
typescript
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
    }
  }
}
重要提示:必须使用共享菜单系统:
typescript
import { PAUSE_MENU_ITEMS, renderSimpleMenu, navigateMenu } from '../shared/menu';

// 在render()方法中:
if (paused) {
  output += renderSimpleMenu(PAUSE_MENU_ITEMS, pauseMenuSelection, {
    centerX: Math.floor(cols / 2),
    startY: pauseY + 2,
    showShortcuts: false,
  });
}

// 在按键处理逻辑中:
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;  // 继续游戏
      case 1: initGame(); gameStarted = true; paused = false; break;  // 重新开始
      case 2: controller.stop(); dispatchGameQuit(terminal); break;  // 退出
      case 3: dispatchGamesMenu(terminal); break;  // 游戏列表
      case 4: dispatchGameSwitch(terminal); break;  // 下一个游戏
    }
  }
}

Terminal Size Handling

终端尺寸处理

Always check minimum size and show helpful resize message:
typescript
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;
}
必须检查最小尺寸并展示友好的调整提示:
typescript
const MIN_COLS = 40;
const MIN_ROWS = 20;

// 在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}`;
  // 居中并渲染提示信息...
  return;
}

Glitch Title Effect

故障风标题效果

Every game has a glitchy ASCII title:
typescript
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`;
}
每个游戏都需要配备故障风ASCII标题:
typescript
const title = [
  '{TITLE_LINE_1}',
  '{TITLE_LINE_2}',
];

let glitchFrame = 0;

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

游戏循环设置

typescript
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);
typescript
setTimeout(() => {
  if (!running) return;

  // 进入备用缓冲区,隐藏光标
  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();
    // 处理输入...
  });

  // 重写stop方法用于清理资源
  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:
typescript
// 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,
};
src/games/index.ts
中添加直接导入和注册条目:
typescript
// 1. 在顶部现有导入区域添加导入语句:
import { run{Name}Game } from './{name}';

// 2. 添加到games数组中:
export const games: GameInfo[] = [
  // ... 现有游戏
  { id: '{name}', name: '{Name}', description: 'Game description', run: run{Name}Game },
];

// 3. 添加到单独的游戏运行器导出列表中:
export {
  // ... 现有导出
  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.
查看
patterns/effects.md
了解以下内容:
  • 粒子系统(爆发、拖尾、烟花)
  • 屏幕震动
  • 得分弹窗
  • 闪屏效果
  • 连杀和连击
使用
src/games/shared/effects.ts
的共享效果模块实现通用效果逻辑。

Input Patterns

输入模式

See
patterns/input-handling.md
for:
  • Arrow key navigation
  • Action keys (space, etc.)
  • ESC/Q handling
  • Start screen any-key
查看
patterns/input-handling.md
了解以下内容:
  • 方向键导航
  • 动作按键(空格等)
  • ESC/Q按键处理
  • 开始界面任意键触发

Rendering Patterns

渲染模式

See
patterns/rendering.md
for:
  • ANSI escape codes reference
  • Border drawing
  • Centering content
  • Color cycling
  • Perspective effects
查看
patterns/rendering.md
了解以下内容:
  • ANSI转义码参考
  • 边框绘制
  • 内容居中
  • 颜色循环
  • 透视效果

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
简单(约300-500行代码): 贪吃蛇、猜词游戏
  • 单文件
  • 基础碰撞检测
  • 极简物理逻辑
中等(约500-800行代码): 俄罗斯方块、乒乓球
  • 单文件包含更多系统
  • AI或网格机制
  • 更丰富的效果打磨
复杂(约1000行以上代码): 直升机游戏
  • 多文件(index.ts、effects.ts)
  • 物理、粒子、弹窗效果
  • 关卡进度系统

Contributing a Game

贡献游戏

  1. Create your game in
    src/games/{name}/index.ts
  2. Register it in
    src/games/index.ts
  3. Use shared effects from
    ../shared/effects
    instead of inlining particle/popup code
  4. Use the shared menu from
    ../shared/menu
    for pause/game-over menus
  5. Run
    npm run build
    and
    npm run typecheck
    to verify
  6. Submit a PR
  1. src/games/{name}/index.ts
    中创建你的游戏
  2. src/games/index.ts
    中注册游戏
  3. 使用
    ../shared/effects
    的共享效果,不要内联粒子/弹窗代码
  4. 暂停/游戏结束菜单使用
    ../shared/menu
    的共享菜单
  5. 运行
    npm run build
    npm run typecheck
    验证代码
  6. 提交PR

Template

模板

Use the scaffold template at
templates/game-scaffold.ts
as a starting point.
可以使用
templates/game-scaffold.ts
的脚手架模板作为开发起点。