claude-arcade
Version:
Add classic arcade games to your Claude Code workflow with Ctrl+G
403 lines (397 loc) • 13.3 kB
JavaScript
"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);
});