claude-arcade
Version:
Add classic arcade games to your Claude Code workflow with Ctrl+G
524 lines (523 loc) โข 19.6 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 });
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);
});