UNPKG

claude-arcade

Version:

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

524 lines (523 loc) โ€ข 19.6 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 }); const pty = __importStar(require("node-pty")); class SpaceInvadersWrapper { constructor() { this.inGameMode = false; this.ptyProcess = null; this.gameLoop = null; // Game state this.player = { x: 25, y: 18 }; this.bullets = []; this.invaders = []; this.score = 0; this.playing = false; this.gameOver = false; this.lives = 3; this.invaderDirection = 1; this.invaderSpeed = 0; this.level = 1; this.powerUps = []; this.rapidFireTimer = 0; this.multiShotActive = false; this.playerShield = 0; this.barriers = []; this.hitEffects = []; // Game constants this.WIDTH = 50; this.HEIGHT = 20; this.PLAYER_WIDTH = 3; } start() { this.ptyProcess = pty.spawn('claude', process.argv.slice(2), { name: 'xterm-256color', cols: process.stdout.columns || 80, rows: process.stdout.rows || 24, cwd: process.cwd(), env: process.env }); this.ptyProcess.onData((data) => { if (!this.inGameMode) { process.stdout.write(data); } }); this.ptyProcess.onExit(() => { if (this.inGameMode) { process.stdout.write('\x1b[?1049l'); } process.exit(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) { 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'); process.stdout.write('\x1b[2J\x1b[?25l'); this.inGameMode = true; this.showWelcome(); } else { this.stopGame(); process.stdout.write('\x1b[?25h'); process.stdout.write('\x1b[?1049l'); this.inGameMode = false; setTimeout(() => { if (this.ptyProcess) { this.ptyProcess.write('\x0C'); } }, 100); } } initInvaders() { this.invaders = []; const invaderTypes = [2, 2, 1, 1, 0]; for (let row = 0; row < 5; row++) { for (let col = 0; col < 10; col++) { this.invaders.push({ x: col * 4 + 5, y: row * 2 + 2, active: true, type: invaderTypes[row], }); } } } initBarriers() { this.barriers = []; const barrierPositions = [10, 20, 30, 40]; for (let pos of barrierPositions) { for (let x = 0; x < 4; x++) { for (let y = 0; y < 2; y++) { this.barriers.push({ x: pos + x, y: this.HEIGHT - 6 + y, health: 3 }); } } } } resetGame() { this.player = { x: this.WIDTH / 2 - 1, y: this.HEIGHT - 2 }; this.bullets = []; this.powerUps = []; this.barriers = []; this.hitEffects = []; this.gameOver = false; this.lives = 3; this.level = 1; this.score = 0; this.invaderDirection = 1; this.invaderSpeed = 0; this.rapidFireTimer = 0; this.multiShotActive = false; this.playerShield = 0; this.initInvaders(); this.initBarriers(); } startGame() { this.playing = true; this.resetGame(); if (this.gameLoop) clearInterval(this.gameLoop); this.gameLoop = setInterval(() => this.update(), 80); } stopGame() { if (this.gameLoop) clearInterval(this.gameLoop); this.gameLoop = null; this.playing = false; } update() { if (this.gameOver) return; this.invaderSpeed++; // Update bullets this.bullets = this.bullets.filter(bullet => { bullet.y += bullet.type === 'player' ? -1 : 1; return bullet.y >= 0 && bullet.y < this.HEIGHT; }); // Move invaders (speed increases with level and fewer invaders) const activeCount = this.invaders.filter(i => i.active).length; const baseSpeed = Math.max(3, 8 - this.level); const adjustedSpeed = Math.max(1, baseSpeed - Math.floor((50 - activeCount) / 10)); if (this.invaderSpeed >= adjustedSpeed) { this.invaderSpeed = 0; this.moveInvaders(); } // Invaders shoot randomly with increasing difficulty const shootChance = 0.02 + (this.level - 1) * 0.005; if (Math.random() < shootChance && this.invaders.some(i => i.active)) { const activeInvaders = this.invaders.filter(i => i.active); const randomInvader = activeInvaders[Math.floor(Math.random() * activeInvaders.length)]; this.bullets.push({ x: randomInvader.x + 1, y: randomInvader.y + 1, type: 'invader' }); } // Spawn power-ups occasionally if (Math.random() < 0.001 && this.powerUps.length < 2) { const powerUpTypes = ['rapidFire', 'multiShot', 'shield']; this.powerUps.push({ x: Math.floor(Math.random() * (this.WIDTH - 2)) + 1, y: 1, type: powerUpTypes[Math.floor(Math.random() * powerUpTypes.length)] }); } // Update power-ups this.powerUps = this.powerUps.filter(powerUp => { powerUp.y += 0.3; return powerUp.y < this.HEIGHT; }); // Update power-up timers if (this.rapidFireTimer > 0) this.rapidFireTimer--; if (this.rapidFireTimer === 0) this.multiShotActive = false; if (this.playerShield > 0) this.playerShield--; // Update hit effects this.hitEffects = this.hitEffects.filter(effect => { effect.timer--; return effect.timer > 0; }); // Check collisions this.checkCollisions(); this.checkPowerUpCollisions(); this.checkBarrierCollisions(); // Check win/lose conditions if (this.invaders.every(i => !i.active)) { this.nextLevel(); } else if (this.invaders.some(i => i.active && i.y >= this.HEIGHT - 3)) { this.endGame(); } else if (this.lives <= 0) { this.endGame(); } this.draw(); } moveInvaders() { const activeInvaders = this.invaders.filter(i => i.active); if (activeInvaders.length === 0) return; const leftmost = Math.min(...activeInvaders.map(i => i.x)); const rightmost = Math.max(...activeInvaders.map(i => i.x)); // Increase speed based on level and remaining invaders const speedMultiplier = Math.max(1, this.level) + (50 - activeInvaders.length) / 10; const moveDistance = Math.floor(speedMultiplier); if ((rightmost >= this.WIDTH - 2 && this.invaderDirection > 0) || (leftmost <= 1 && this.invaderDirection < 0)) { this.invaderDirection *= -1; this.invaders.forEach(invader => { if (invader.active) { invader.y += Math.min(2, moveDistance); } }); } else { this.invaders.forEach(invader => { if (invader.active) { invader.x += this.invaderDirection * Math.min(2, moveDistance); } }); } } checkCollisions() { // Bullet vs invader collisions for (let bullet of this.bullets.slice()) { if (bullet.type === 'player') { for (let invader of this.invaders) { if (invader.active && bullet.x >= invader.x && bullet.x <= invader.x + 2 && bullet.y === invader.y) { invader.active = false; this.bullets = this.bullets.filter(b => b !== bullet); const baseScore = (invader.type + 1) * 10; const levelBonus = this.level * 5; this.score += baseScore + levelBonus; // Visual feedback for hit this.hitEffects.push({ x: invader.x, y: invader.y, timer: 10, type: 'explosion' }); break; } } } // Bullet vs player collision (with shield protection) if (bullet.type === 'invader' && bullet.x >= this.player.x && bullet.x <= this.player.x + this.PLAYER_WIDTH - 1 && bullet.y === this.player.y) { this.bullets = this.bullets.filter(b => b !== bullet); if (this.playerShield > 0) { this.playerShield = 0; } else { this.lives--; if (this.lives <= 0) { this.endGame(); } } } } } checkPowerUpCollisions() { for (let powerUp of this.powerUps.slice()) { if (Math.floor(powerUp.y) === this.player.y && powerUp.x >= this.player.x && powerUp.x <= this.player.x + this.PLAYER_WIDTH - 1) { this.powerUps = this.powerUps.filter(p => p !== powerUp); this.activatePowerUp(powerUp.type); } } } activatePowerUp(type) { switch (type) { case 'rapidFire': this.rapidFireTimer = 300; this.multiShotActive = true; break; case 'multiShot': this.rapidFireTimer = 200; this.multiShotActive = true; break; case 'shield': this.playerShield = 150; break; } } checkBarrierCollisions() { for (let bullet of this.bullets.slice()) { for (let barrier of this.barriers.slice()) { if (barrier.health > 0 && bullet.x === barrier.x && bullet.y === barrier.y) { this.bullets = this.bullets.filter(b => b !== bullet); barrier.health--; break; } } } } nextLevel() { this.level++; this.initInvaders(); this.initBarriers(); this.bullets = []; this.powerUps = []; this.invaderSpeed = 0; } draw() { const GREEN = '\x1b[32m', YELLOW = '\x1b[33m', RED = '\x1b[31m'; const CYAN = '\x1b[36m', MAGENTA = '\x1b[35m', RESET = '\x1b[0m'; let output = '\x1b[1;1H' + YELLOW + `Score: ${this.score} Lives: ${this.lives} Level: ${this.level}` + RESET; if (this.rapidFireTimer > 0) output += CYAN + ` โšก${Math.ceil(this.rapidFireTimer / 60)}s` + RESET; if (this.playerShield > 0) output += MAGENTA + ` ๐Ÿ›ก๏ธ${Math.ceil(this.playerShield / 60)}s` + RESET; for (let y = 0; y < this.HEIGHT; y++) { output += `\x1b[${y + 3};1H`; for (let x = 0; x < this.WIDTH; x++) { const invader = this.invaders.find(i => i.active && x >= i.x && x <= i.x + 2 && y === i.y); const bullet = this.bullets.find(b => b.x === x && b.y === y); const powerUp = this.powerUps.find(p => Math.floor(p.x) === x && Math.floor(p.y) === y); const barrier = this.barriers.find(b => b.x === x && b.y === y && b.health > 0); const hitEffect = this.hitEffects.find(h => h.x === x && h.y === y); const isPlayer = y === this.player.y && x >= this.player.x && x <= this.player.x + this.PLAYER_WIDTH - 1; if (invader) { const invaderChars = ['๐Ÿ‘พ', '๐Ÿ›ธ', '๐Ÿ‘ฝ']; const colors = [RED, MAGENTA, CYAN]; if (x === invader.x + 1) { output += colors[invader.type] + invaderChars[invader.type] + RESET; } else { output += ' '; } } else if (bullet) { output += (bullet.type === 'player' ? YELLOW + '|' : RED + '!') + RESET; } else if (powerUp) { const powerUpChars = { rapidFire: 'โšก', multiShot: '๐Ÿ’ฅ', shield: '๐Ÿ›ก๏ธ' }; output += YELLOW + powerUpChars[powerUp.type] + RESET; } else if (barrier) { const barrierChars = ['ยท', 'โ–ช', 'โ– ']; output += GREEN + barrierChars[Math.min(barrier.health - 1, 2)] + RESET; } else if (hitEffect) { output += RED + '๐Ÿ’ฅ' + RESET; } else if (isPlayer) { if (x === this.player.x + 1) { const playerChar = this.playerShield > 0 ? '๐Ÿ›ก๏ธ' : '๐Ÿš€'; output += GREEN + playerChar + RESET; } else { output += ' '; } } else { output += ' '; } } } output += `\x1b[${this.HEIGHT + 4};1H` + GREEN + 'Press P to play | โ†โ†’ to move | SPACE to shoot | Ctrl+G to exit' + RESET; process.stdout.write(output); } endGame() { this.gameOver = true; if (this.gameLoop) clearInterval(this.gameLoop); this.playing = false; const RED = '\x1b[31m', YELLOW = '\x1b[33m', GREEN = '\x1b[32m', RESET = '\x1b[0m'; let output = '\x1b[2J\x1b[10;1H'; output += RED + '๐Ÿ‘พ GAME OVER! ๐Ÿ‘พ\n\n' + RESET; output += YELLOW + `Final Score: ${this.score}\n` + RESET; output += YELLOW + `Level Reached: ${this.level}\n\n` + RESET; output += GREEN + 'Press P to play again\n' + RESET; process.stdout.write(output); } showWelcome() { const GREEN = '\x1b[32m', YELLOW = '\x1b[33m', CYAN = '\x1b[36m', RESET = '\x1b[0m'; const output = '\x1b[2J\x1b[8;1H' + CYAN + '๐Ÿ‘พ SPACE INVADERS ๐Ÿ‘พ\n\n' + RESET + GREEN + 'Defend Earth from alien invasion!\n' + RESET + YELLOW + 'Destroy all invaders to advance levels\n\n' + RESET + GREEN + 'Press P to start!\n' + RESET; process.stdout.write(output); } handleGameInput(key) { const char = key.toString(); if (key[0] === 7) { this.toggleGame(); return; } if (char === 'q' || char === 'Q') { this.toggleGame(); return; } if ((char === 'p' || char === 'P') && !this.playing) { this.startGame(); return; } if (this.playing && !this.gameOver) { // Left arrow or 'a' if (key[0] === 27 && key[1] === 91 && key[2] === 68 || char === 'a' || char === 'A') { this.player.x = Math.max(0, this.player.x - 2); } // Right arrow or 'd' else if (key[0] === 27 && key[1] === 91 && key[2] === 67 || char === 'd' || char === 'D') { this.player.x = Math.min(this.WIDTH - this.PLAYER_WIDTH, this.player.x + 2); } // Space to shoot else if (key[0] === 32) { if (this.multiShotActive) { // Multi-shot: fire 3 bullets this.bullets.push({ x: this.player.x, y: this.player.y - 1, type: 'player' }, { x: this.player.x + 1, y: this.player.y - 1, type: 'player' }, { x: this.player.x + 2, y: this.player.y - 1, type: 'player' }); } else { // Normal shot this.bullets.push({ x: this.player.x + 1, y: this.player.y - 1, type: 'player' }); } } } } } const wrapper = new SpaceInvadersWrapper(); wrapper.start(); 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); });