UNPKG

claude-arcade

Version:

Add classic arcade games to your Claude Code workflow with Ctrl+G

403 lines (397 loc) 13.3 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); // Suppress deprecation warnings from dependencies process.removeAllListeners('warning'); process.on('warning', (warning) => { if (warning.name === 'DeprecationWarning') return; console.warn(warning); }); const pty = __importStar(require("node-pty")); const brick_breaker_1 = require("./games/brick-breaker"); const snake_1 = require("./games/snake"); const dino_1 = require("./games/dino"); const leaderboard_1 = require("./leaderboard"); // ===== CLI ARGUMENT PARSING ===== function parseArgs() { const args = process.argv.slice(2); let help = false; let snake = false; let dino = false; const claudeArgs = []; for (const arg of args) { const cleanArg = arg.replace(/^-+/, ''); if (cleanArg === 'h' || cleanArg === 'help') { help = true; } else if (cleanArg === 'snake') { snake = true; } else if (cleanArg === 'dino' || cleanArg === 'dinosaur') { dino = true; } else { claudeArgs.push(arg); } } return { help, snake, dino, claudeArgs }; } function showHelp() { const CYAN = '\x1b[36m', GREEN = '\x1b[32m', YELLOW = '\x1b[33m', RESET = '\x1b[0m'; console.log(` ${CYAN}Claude Arcade${RESET} - Add games to your Claude Code workflow! ${GREEN}Usage:${RESET} claude-arc Start Claude with Brick Breaker game claude-arc -snake Start Claude with Snake game claude-arc -dino Start Claude with Dino game claude-arc -help Show this help ${GREEN}Controls:${RESET} Ctrl+G Toggle game overlay Q Exit game back to Claude ${GREEN}Games:${RESET} ${YELLOW}•${RESET} Brick Breaker - Break bricks with strength levels (default) ${YELLOW}•${RESET} Snake - Classic snake game ${YELLOW}•${RESET} Dino - Chrome dinosaur runner game ${GREEN}Examples:${RESET} claude-arc # Start with Brick Breaker claude-arc -snake # Start with Snake claude-arc -dino # Start with Dino claude-arc --snake # Also works with double dash claude-arc -snake --model gpt # Pass args to Claude ${GREEN}More info:${RESET} https://github.com/yourusername/claude-arcade `); process.exit(0); } // ===== GAME WRAPPER BASE CLASS ===== class GameWrapper { constructor(game, updateInterval = 100, gameId) { this.inGameMode = false; this.ptyProcess = null; this.gameLoop = null; this.outputBuffer = []; this.awaitingLeaderboardInput = false; this.game = game; this.updateInterval = updateInterval; this.gameId = gameId; } start(claudeArgs = []) { this.ptyProcess = pty.spawn('claude', claudeArgs, { name: 'xterm-256color', cols: process.stdout.columns || 80, rows: process.stdout.rows || 24, cwd: process.cwd(), env: process.env }); let totalBytesReceived = 0; let bufferedBytes = 0; this.ptyProcess.onData((data) => { totalBytesReceived += data.length; if (this.inGameMode) { this.outputBuffer.push(data); bufferedBytes += data.length; } else { process.stdout.write(data); } }); this.ptyProcess.onExit(({ exitCode, signal }) => { if (this.inGameMode) { process.stdout.write('\x1b[?1049l'); } process.exit(exitCode || 0); }); process.stdout.on('resize', () => { if (this.ptyProcess && !this.inGameMode) { this.ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24); } }); this.setupInputHandling(); } setupInputHandling() { if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.resume(); process.stdin.on('data', (key) => { if (key[0] === 7) { // Ctrl+G this.toggleGame(); return; } if (this.inGameMode) { this.handleGameInput(key); } else { if (this.ptyProcess) { this.ptyProcess.write(key); } } }); } toggleGame() { if (!this.inGameMode) { process.stdout.write('\x1b[?1049h\x1b[2J\x1b[?25l'); this.inGameMode = true; process.stdout.write(this.game.drawWelcome()); } else { this.stopGame(); process.stdout.write('\x1b[?25h\x1b[?1049l'); this.inGameMode = false; if (this.outputBuffer.length > 0) { for (const data of this.outputBuffer) { process.stdout.write(data); } this.outputBuffer = []; } setTimeout(() => { if (this.ptyProcess) { this.ptyProcess.write('\x0C'); } }, 100); } } stopGame() { if (this.gameLoop) clearInterval(this.gameLoop); this.gameLoop = null; this.game.stop(); } async handleGameOver(gameOverScreen) { this.stopGame(); process.stdout.write(gameOverScreen); this.awaitingLeaderboardInput = true; (0, leaderboard_1.promptForName)(async (name, action) => { this.awaitingLeaderboardInput = false; if (action === 'submit' && name) { process.stdout.write('\x1b[2J\x1b[10;1H\x1b[33mSubmitting score...\x1b[0m\n'); const success = await (0, leaderboard_1.submitScore)(this.gameId, name, this.game.getScore()); if (success) { process.stdout.write('\x1b[32m✓ Score submitted successfully!\x1b[0m\n\n'); } else { process.stdout.write('\x1b[31m✗ Failed to submit score\x1b[0m\n\n'); } process.stdout.write('\x1b[32mPress P to play again | Press Q to exit\x1b[0m\n'); } else if (action === 'play') { this.startGame(); } else if (action === 'quit') { this.toggleGame(); } }); } handleGameInput(key) { const char = key.toString(); // Ignore input if waiting for leaderboard submission if (this.awaitingLeaderboardInput) { return; } // Ctrl+G, Ctrl+C, or Q to exit game if (key[0] === 7 || key[0] === 3 || char === 'q' || char === 'Q') { this.toggleGame(); return; } if ((char === 'p' || char === 'P') && !this.game.isPlaying()) { this.startGame(); return; } this.handleSpecificGameInput(key); } startGame() { this.game.start(); if (this.gameLoop) clearInterval(this.gameLoop); this.gameLoop = setInterval(() => this.update(), this.updateInterval); } update() { // To be implemented by subclasses } handleSpecificGameInput(key) { // To be implemented by subclasses } } // ===== BRICK BREAKER WRAPPER ===== class BrickBreakerWrapper extends GameWrapper { constructor() { super(new brick_breaker_1.BrickBreakerGame(), 100, 'brick_breaker'); } update() { const result = this.game.update(); if (result.won !== undefined || result.lost !== undefined) { this.handleGameOver(this.game.drawGameOver(result.won || false)); return; } process.stdout.write(this.game.draw()); } handleSpecificGameInput(key) { if (!this.game.isPlaying()) return; const char = key.toString(); if (key[0] === 27 && key[1] === 91 && key[2] === 68 || char === 'a' || char === 'A') { this.game.movePaddleLeft(); process.stdout.write(this.game.draw()); } else if (key[0] === 27 && key[1] === 91 && key[2] === 67 || char === 'd' || char === 'D') { this.game.movePaddleRight(); process.stdout.write(this.game.draw()); } } } // ===== SNAKE WRAPPER ===== class SnakeWrapper extends GameWrapper { constructor() { super(new snake_1.SnakeGame(), 150, 'snake'); } update() { this.game.update(); if (this.game.isGameOver()) { this.handleGameOver(this.game.drawGameOver()); return; } process.stdout.write(this.game.draw()); } handleSpecificGameInput(key) { if (!this.game.isPlaying() || this.game.isGameOver()) return; const char = key.toString(); if (char === 'w' || char === 'W') { this.game.setDirection(0, -1); } else if (char === 's' || char === 'S') { this.game.setDirection(0, 1); } else if (char === 'a' || char === 'A') { this.game.setDirection(-1, 0); } else if (char === 'd' || char === 'D') { this.game.setDirection(1, 0); } else if (key[0] === 27 && key[1] === 91) { if (key[2] === 65) { // Up this.game.setDirection(0, -1); } else if (key[2] === 66) { // Down this.game.setDirection(0, 1); } else if (key[2] === 68) { // Left this.game.setDirection(-1, 0); } else if (key[2] === 67) { // Right this.game.setDirection(1, 0); } } } } // ===== DINO WRAPPER ===== class DinoWrapper extends GameWrapper { constructor() { super(new dino_1.DinoGame(), 50, 'dino'); // 50ms for smoother animation } update() { this.game.update(); if (this.game.isGameOver()) { this.handleGameOver(this.game.drawGameOver()); return; } process.stdout.write(this.game.draw()); } handleSpecificGameInput(key) { if (!this.game.isPlaying() || this.game.isGameOver()) return; const char = key.toString(); // Jump on up arrow or space if (key[0] === 27 && key[1] === 91 && key[2] === 65) { // Up arrow this.game.jump(); } else if (key[0] === 32) { // Space this.game.jump(); } // Duck on down arrow or S else if (key[0] === 27 && key[1] === 91 && key[2] === 66) { // Down arrow this.game.duck(true); } else if (char === 's' || char === 'S') { this.game.duck(true); } } } // ===== STARTUP ===== const { help, snake, dino, claudeArgs } = parseArgs(); if (help) { showHelp(); } let wrapper; if (dino) { wrapper = new DinoWrapper(); } else if (snake) { wrapper = new SnakeWrapper(); } else { wrapper = new BrickBreakerWrapper(); } wrapper.start(claudeArgs); // ===== SIGNAL HANDLING ===== process.on('SIGINT', () => { if (wrapper.ptyProcess) { wrapper.ptyProcess.kill(); } if (process.stdin.isTTY) { process.stdin.setRawMode(false); } process.exit(0); }); process.on('exit', () => { if (wrapper.inGameMode) { process.stdout.write('\x1b[?25h\x1b[?1049l'); } if (process.stdin.isTTY) { process.stdin.setRawMode(false); } }); process.on('uncaughtException', (err) => { if (wrapper.inGameMode) { process.stdout.write('\x1b[?25h\x1b[?1049l'); } if (process.stdin.isTTY) { process.stdin.setRawMode(false); } console.error('Uncaught exception:', err); process.exit(1); });