Loading...
Loading...
Create terminal games for cli-games. Use when asked to "create a game", "make a terminal game", "add a game", "vibe code a game", build arcade/puzzle/action games for the terminal, or contribute a new game.
npx skill4agent add hypersocialinc/cli-games game-dev@hypersocial/cli-gamessrc/games/{name}/index.tssrc/games/index.tsstop()isRunninggameStartedgameOverpausedwon../shared/menu../shared/effectsgetCurrentThemeColor()../utilssrc/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 registryimport type { Terminal } from '@xterm/xterm';
import { getCurrentThemeColor } from '../utils';
import { dispatchGameQuit, dispatchGameSwitch, dispatchGamesMenu } from '../gameTransitions';
import { PAUSE_MENU_ITEMS, renderSimpleMenu, navigateMenu } from '../shared/menu';import {
type Particle,
type ScorePopup,
spawnParticles,
updateParticles,
addScorePopup,
updatePopups,
triggerShake,
applyShake,
createShakeState,
MAX_PARTICLES,
PARTICLE_CHARS,
} from '../shared/effects';export interface {Name}Controller {
stop: () => void;
isRunning: boolean;
}let running = true;
let gameStarted = false;
let gameOver = false;
let paused = false;
let pauseMenuSelection = 0;
let won = false;!gameStarted && !pausedgameStarted = truepausedpauseMenuSelection = 0gameOver = truewon = truecontroller.stop()dispatchGameQuit()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
}
}
}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;
}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`;
}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);src/games/index.ts// 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,
};patterns/effects.mdsrc/games/shared/effects.tspatterns/input-handling.mdpatterns/rendering.mdsrc/games/{name}/index.tssrc/games/index.ts../shared/effects../shared/menunpm run buildnpm run typechecktemplates/game-scaffold.ts