blecsd-game
3
总安装量
3
周安装量
#56766
全站排名
安装命令
npx skills add https://github.com/kadajett/blecsd-skill --skill blecsd-game
Agent 安装分布
amp
3
github-copilot
3
codex
3
kimi-cli
3
gemini-cli
3
opencode
3
Skill 文档
@blecsd/game Package Skill
The @blecsd/game package provides a high-level API for building terminal games with blECSd. It wraps the core ECS world, input system, and game loop into a simple createGame() factory. All blECSd functional programming rules apply.
Install: pnpm add @blecsd/game
Peer dependency: blecsd >= 0.6.0
Import: import { createGame } from '@blecsd/game'
Quick Start
import { createGame } from '@blecsd/game';
const game = createGame({
title: 'My Game',
width: 80,
height: 24,
targetFPS: 30,
mouse: true,
alternateScreen: true,
hideCursor: true,
});
// Create UI
const player = game.createBox({
position: { x: 10, y: 10 },
dimensions: { width: 3, height: 1 },
content: '@',
});
// Input
game.onKey('q', () => game.quit());
// Update loop
game.onUpdate((dt) => {
if (game.isKeyDown('up')) moveUp(dt);
if (game.isKeyDown('down')) moveDown(dt);
});
// Start
game.start();
API Reference
createGame(config?)
Factory function that returns a Game interface. Config is optional:
interface GameConfig {
title?: string; // Window title
width?: number; // Terminal width
height?: number; // Terminal height
targetFPS?: number; // Target frame rate (default: 60)
mouse?: boolean; // Enable mouse input
alternateScreen?: boolean; // Use alternate screen buffer
hideCursor?: boolean; // Hide terminal cursor
fixedTimestep?: {
tickRate: number; // Physics tick rate (e.g., 60)
maxUpdatesPerFrame: number; // Prevent spiral of death
interpolate: boolean; // Interpolate between ticks
};
}
Widget Creation
All widget factories take a config object and return an entity ID:
const box = game.createBox(config);
const text = game.createText(config);
const button = game.createButton(config);
const input = game.createInput(config);
const textarea = game.createTextarea(config);
const textbox = game.createTextbox(config);
const checkbox = game.createCheckbox(config);
const radio = game.createRadioButton(config);
const radioSet = game.createRadioSet(config);
const select = game.createSelect(config);
const slider = game.createSlider(config);
const progress = game.createProgressBar(config);
const list = game.createList(config);
const form = game.createForm(config);
Input System
Key Handlers
// Single key handler
game.onKey('space', () => shoot());
game.onKey('escape', () => pause());
// Any key handler
game.onAnyKey((keyName) => {
console.log(`Key pressed: ${keyName}`);
});
// Check if key is currently held
if (game.isKeyDown('left')) moveLeft();
if (game.isKeyDown('right')) moveRight();
Mouse Handler
game.onMouse((event) => {
// event: { x, y, button, action }
if (event.action === 'click') {
handleClick(event.x, event.y);
}
});
Action System
Map multiple keys/buttons to named actions:
game.defineActions([
{ action: 'move_up', keys: ['up', 'w', 'k'] },
{ action: 'move_down', keys: ['down', 's', 'j'] },
{ action: 'move_left', keys: ['left', 'a', 'h'] },
{ action: 'move_right', keys: ['right', 'd', 'l'] },
{ action: 'shoot', keys: ['space'], mouseButtons: [0] },
{ action: 'interact', keys: ['e', 'enter'] },
]);
// Check action state
game.onUpdate((dt) => {
if (game.isActionActive('move_up')) player.y -= speed * dt;
if (game.isActionActive('move_down')) player.y += speed * dt;
if (game.isActionActive('shoot')) fireBullet();
});
Game Loop
Variable Timestep (UI, movement)
game.onUpdate((deltaTime) => {
// deltaTime is seconds since last frame
playerX += velocityX * deltaTime;
playerY += velocityY * deltaTime;
updateAnimations(deltaTime);
});
Fixed Timestep (Physics, deterministic logic)
const game = createGame({
fixedTimestep: {
tickRate: 60, // 60 physics ticks per second
maxUpdatesPerFrame: 5, // Prevent spiral of death
interpolate: true, // Smooth between ticks
},
});
game.onFixedUpdate((fixedDt, tick) => {
// fixedDt is always 1/60 (or 1/tickRate)
// tick is the current tick number
applyPhysics(fixedDt);
checkCollisions();
});
Render Callback
game.onRender((alpha) => {
// alpha is interpolation factor (0-1) between physics ticks
// Use for smooth rendering when using fixed timestep
const renderX = prevX + (currX - prevX) * alpha;
const renderY = prevY + (currY - prevY) * alpha;
drawPlayer(renderX, renderY);
});
Lifecycle
game.start(); // Start the game loop
game.stop(); // Stop the game loop
game.pause(); // Pause (loop still runs, logic skipped)
game.resume(); // Resume from pause
game.quit(); // Full cleanup and exit
// State queries
game.isRunning(); // Is the loop active?
game.isPaused(); // Is the game paused?
// Performance stats
const stats = game.getStats();
// { fps, frameTime, updateTime, renderTime, entityCount }
ECS World Access
For advanced use, access the underlying blECSd world:
import { addEntity, addComponent, hasComponent } from 'blecsd';
import { Position, Velocity } from 'blecsd/components';
// Access world directly
const eid = addEntity(game.world);
addComponent(game.world, eid, Position);
addComponent(game.world, eid, Velocity);
Position.x[eid] = 40;
Velocity.x[eid] = 2;
Common Game Patterns
Roguelike Movement
const game = createGame({ title: 'Roguelike', alternateScreen: true });
let playerX = 40, playerY = 12;
game.defineActions([
{ action: 'up', keys: ['up', 'k'] },
{ action: 'down', keys: ['down', 'j'] },
{ action: 'left', keys: ['left', 'h'] },
{ action: 'right', keys: ['right', 'l'] },
]);
game.onUpdate(() => {
if (game.isActionActive('up')) playerY--;
if (game.isActionActive('down')) playerY++;
if (game.isActionActive('left')) playerX--;
if (game.isActionActive('right')) playerX++;
});
game.onKey('q', () => game.quit());
game.start();
Simple Physics Game
const game = createGame({
targetFPS: 60,
fixedTimestep: { tickRate: 60, maxUpdatesPerFrame: 3, interpolate: true },
});
let ballX = 40, ballY = 12, velX = 10, velY = 5;
game.onFixedUpdate((dt) => {
ballX += velX * dt;
ballY += velY * dt;
// Bounce off walls
if (ballX <= 0 || ballX >= 79) velX *= -1;
if (ballY <= 0 || ballY >= 23) velY *= -1;
});
game.start();
Dashboard with Stats
const game = createGame({ title: 'Dashboard' });
const fpsText = game.createText({
position: { x: 0, y: 0 },
dimensions: { width: 20, height: 1 },
});
game.onUpdate(() => {
const stats = game.getStats();
setText(game.world, fpsText, `FPS: ${stats.fps.toFixed(1)}`);
});
game.start();
Best Practices
- Use
defineActionsfor game input. Map multiple keys to actions instead of checking individual keys everywhere. - Use fixed timestep for physics. Variable timestep causes non-deterministic physics. Use
fixedTimestepconfig andonFixedUpdate. - Access
game.worldfor advanced ECS. The game API is a convenience wrapper. For full power, use the blECSd core APIs ongame.world. - Set
maxUpdatesPerFrameto prevent spiral of death when the game can’t keep up. - Use
interpolate: truewith fixed timestep for smooth rendering between physics ticks. - Clean up with
game.quit(). This restores the terminal state (cursor, alternate screen, mouse). - All blECSd rules apply. No classes, no
this, pure functions, early returns, Zod validation at boundaries.