UNPKG

jumpurr

Version:

jumpurr is a terminal-based game where you control a cat running through a retro-style environment, avoiding obstacles and collecting items.

522 lines (440 loc) โ€ข 14.3 kB
#!/usr/bin/env node const blessed = require('blessed'); const colors = require('ansi-colors'); // Create screen const screen = blessed.screen({ smartCSR: true, title: '๐Ÿพ Jumpurr DX' }); // Game constants const gameWidth = 60; const gameHeight = 20; const groundY = gameHeight - 1; const catX = 5; const catHeight = 3; const catWidth = 7; // Game state let score = 0; let speed = 1; let gameState = 'title'; // 'title', 'running', 'paused', 'gameover' let isJumping = false; let jumpFrame = 0; const maxJumpFrame = 16; const jumpStrength = 8; let obstacles = []; let frameCount = 0; let highScore = 0; let obstacleTimer = 0; // ASCII art for title and game over screens const titleArt = [ " __ _____ ", " | | | __ \\ ", " | |_ _ _ __ ___ | |_ ) |_ _ _ __ _ __ ", " _ | | | | | '_ ` _ \\| ___/| | | | '__| '__|", " | |__| | |_| | | | | | | | | |_| | | || | ", " \\____/ \\__,_|_| |_| |_|_| \\__,_|_| |_| " ] const gameOverArt = [ " _____ ____ ", " / ____| / __ \\ ", "| | __ __ _ _ __ ___ ___ | | | |_ _____ _ __ ", "| | |_ |/ _` | '_ ` _ \\ / _ \\| | | \\ \\ / / _ \\ '__|", "| |__| | (_| | | | | | | __/| |__| |\\ V / __/ | ", " \\_____|\\__,_|_| |_| |_|\\___| \\____/ \\_/ \\___|_| " ]; // Cat sprites const catSprites = { run1: [' /\\_/\\ ', '( o.o )', ' > ^ < '], run2: [' /\\_/\\ ', '( -.- )', ' > ^ < '], jump: [' /\\_/\\ ', '( ^.^ )', ' \\_/ '] }; // Obstacle types const obstacleTypes = [ { char: 'โ–ˆ', height: 2, width: 1 }, { char: 'โ–“', height: 3, width: 1 }, { char: 'โ–’', height: 1, width: 1 } ]; // Create UI container with retro border const container = blessed.box({ top: 'center', left: 'center', width: gameWidth + 6, height: gameHeight + 8, border: { type: 'line', fg: 'cyan' }, label: ' Jumpurr DX ', style: { border: { fg: 'cyan' }, label: { fg: 'yellow', bold: true }, bg: 'black' }, parent: screen }); // Game display area const gameBox = blessed.box({ top: 1, left: 1, width: gameWidth + 2, height: gameHeight + 2, tags: true, style: { bg: 'black' }, parent: container }); // Stats display const statsBox = blessed.box({ bottom: 2, left: 'center', width: gameWidth, height: 1, tags: true, content: '', style: { fg: 'green' }, parent: container }); // Controls hint const controlsBox = blessed.box({ bottom: 0, left: 'center', width: gameWidth, height: 1, tags: true, content: '{bold}[SPACE]{/bold} Jump | {bold}[P]{/bold} Pause | {bold}[Q]{/bold} Quit', style: { fg: 'gray' }, parent: container }); // Register key events screen.key(['space'], () => { if (gameState === 'title') { startGame(); } else if (!isJumping && gameState === 'running') { isJumping = true; jumpFrame = 0; } else if (gameState === 'gameover') { startGame(); } }); screen.key(['p'], () => { if (gameState === 'running') { gameState = 'paused'; renderPauseScreen(); } else if (gameState === 'paused') { gameState = 'running'; } }); screen.key(['q', 'C-c'], () => process.exit(0)); // Helper functions function getCatY() { if (!isJumping) return groundY - catHeight + 1; const phase = Math.sin((Math.PI * jumpFrame) / maxJumpFrame); const jumpOffset = Math.round(jumpStrength * phase); return groundY - catHeight + 1 - jumpOffset; } function generateObstacle() { const type = obstacleTypes[Math.floor(Math.random() * obstacleTypes.length)]; return { x: gameWidth, width: type.width || 1, height: type.height, char: type.char, passed: false }; } // Function to generate obstacle patterns function generateObstaclePattern() { const level = Math.floor(score / 300) + 1; const patternType = Math.random(); // Single obstacle if (patternType < Math.max(0.2, 0.7 - level * 0.1)) { obstacles.push(generateObstacle()); return; } // Double obstacles if (patternType < 0.7) { const obs1 = generateObstacle(); obstacles.push(obs1); // Second obstacle close to the first const obs2 = generateObstacle(); obs2.x = gameWidth + Math.floor(Math.random() * 5) + 3; obstacles.push(obs2); return; } // Triple obstacles const obs1 = generateObstacle(); const obs2 = generateObstacle(); const obs3 = generateObstacle(); obs1.x = gameWidth; obs2.x = gameWidth + Math.floor(Math.random() * 4) + 4; obs3.x = gameWidth + Math.floor(Math.random() * 4) + 10; obstacles.push(obs1); obstacles.push(obs2); obstacles.push(obs3); // Occasionally add a fourth obstacle at higher levels if (level > 3 && Math.random() < 0.3) { const obs4 = generateObstacle(); obs4.x = gameWidth + Math.floor(Math.random() * 5) + 16; obstacles.push(obs4); } } function colorText(text, colorFn) { return text.split('').map(char => colorFn(char)).join(''); } function renderTitleScreen() { let frame = Array.from({ length: gameHeight }, () => Array(gameWidth).fill(' ')); const offsetY = 3; // Draw title art for (let i = 0; i < titleArt.length; i++) { for (let j = 0; j < titleArt[i].length && j < gameWidth; j++) { if (offsetY + i < gameHeight) { const char = titleArt[i][j]; if (char !== ' ') { // Alternate colors for title const colorFn = (i % 2 === 0) ? colors.cyan : colors.blue; frame[offsetY + i][Math.floor((gameWidth - titleArt[i].length) / 2) + j] = colorFn(char); } else { frame[offsetY + i][Math.floor((gameWidth - titleArt[i].length) / 2) + j] = ' '; } } } } // Draw blinking press start const startText = "PRESS SPACE TO START"; const startY = offsetY + titleArt.length + 3; const blinkOn = Math.floor(Date.now() / 500) % 2 === 0; if (blinkOn && startY < gameHeight) { for (let i = 0; i < startText.length; i++) { const x = Math.floor((gameWidth - startText.length) / 2) + i; frame[startY][x] = colors.yellow(startText[i]); } } // Draw cat const sprite = Math.floor(Date.now() / 200) % 2 === 0 ? catSprites.run1 : catSprites.run2; const catPosX = Math.floor((gameWidth - catWidth) / 2); const catPosY = startY + 3; for (let i = 0; i < sprite.length && catPosY + i < gameHeight; i++) { for (let j = 0; j < sprite[i].length; j++) { if (catPosY + i >= 0 && catPosX + j < gameWidth) { frame[catPosY + i][catPosX + j] = colors.cyan(sprite[i][j]); } } } // Draw ground for (let x = 0; x < gameWidth; x++) { const groundPattern = (x % 4 === 0) ? 'โ–€' : (x % 4 === 2) ? 'โ–„' : 'โ•'; frame[groundY][x] = colors.green(groundPattern); } // Render frame gameBox.setContent(frame.map(row => row.join('')).join('\n')); statsBox.setContent(`{bold}HIGH SCORE: ${highScore}{/bold}`); screen.render(); } function renderPauseScreen() { const pauseText = "GAME PAUSED"; const continueText = "Press P to continue"; gameBox.setContent( `\n\n\n\n\n{center}${colors.yellow(pauseText)}\n\n${continueText}{/center}` ); screen.render(); } function renderGameOverScreen() { let frame = Array.from({ length: gameHeight }, () => Array(gameWidth).fill(' ')); const offsetY = 3; // Draw game over art for (let i = 0; i < gameOverArt.length; i++) { for (let j = 0; j < gameOverArt[i].length && j < gameWidth; j++) { if (offsetY + i < gameHeight) { const char = gameOverArt[i][j]; if (char !== ' ') { frame[offsetY + i][Math.floor((gameWidth - gameOverArt[i].length) / 2) + j] = colors.red(char); } else { frame[offsetY + i][Math.floor((gameWidth - gameOverArt[i].length) / 2) + j] = ' '; } } } } // Show score const scoreText = `SCORE: ${score}`; const highScoreText = `HIGH SCORE: ${highScore}`; const restartText = "PRESS SPACE TO RESTART"; const scoreY = offsetY + gameOverArt.length + 2; const highScoreY = scoreY + 1; const restartY = highScoreY + 2; if (scoreY < gameHeight) { for (let i = 0; i < scoreText.length; i++) { const x = Math.floor((gameWidth - scoreText.length) / 2) + i; frame[scoreY][x] = colors.yellow(scoreText[i]); } } if (highScoreY < gameHeight) { for (let i = 0; i < highScoreText.length; i++) { const x = Math.floor((gameWidth - highScoreText.length) / 2) + i; frame[highScoreY][x] = colors.green(highScoreText[i]); } } const blinkOn = Math.floor(Date.now() / 500) % 2 === 0; if (blinkOn && restartY < gameHeight) { for (let i = 0; i < restartText.length; i++) { const x = Math.floor((gameWidth - restartText.length) / 2) + i; frame[restartY][x] = colors.cyan(restartText[i]); } } // Render frame gameBox.setContent(frame.map(row => row.join('')).join('\n')); screen.render(); } const starCount = 30; const stars = Array.from({ length: starCount }, () => ({ x: Math.floor(Math.random() * gameWidth), y: Math.floor(Math.random() * (groundY - 1)), char: Math.random() > 0.8 ? 'โ˜…' : 'ยท' })); function renderGame() { frameCount++; let frame = Array.from({ length: gameHeight }, () => Array(gameWidth).fill(' ')); for (const star of stars) { if (star.y >= 0 && star.y < groundY && star.x >= 0 && star.x < gameWidth) { frame[star.y][star.x] = colors.gray(star.char); } } // Draw ground with retro pattern for (let x = 0; x < gameWidth; x++) { const groundPattern = ((x + frameCount) % 4 === 0) ? 'โ–€' : ((x + frameCount) % 4 === 2) ? 'โ–„' : 'โ•'; frame[groundY][x] = colors.green(groundPattern); } // Draw obstacles with retro colors for (const obs of obstacles) { for (let w = 0; w < obs.width; w++) { for (let h = 0; h < obs.height; h++) { const y = groundY - h; const x = Math.floor(obs.x) + w; if (y >= 0 && y < gameHeight && x >= 0 && x < gameWidth) { const colorFn = (h % 2 === 0) ? colors.red : colors.yellow; frame[y][x] = colorFn(obs.char); } } } } // Draw cat with animation const sprite = isJumping ? catSprites.jump : (Math.floor(frameCount / 3) % 2 === 0 ? catSprites.run1 : catSprites.run2); const catY = getCatY(); for (let i = 0; i < sprite.length; i++) { for (let j = 0; j < sprite[i].length; j++) { const y = catY + i; const x = catX + j; if (y >= 0 && y < gameHeight && x >= 0 && x < gameWidth) { // Add color effects to cat while jumping let colorFn = colors.cyan; if (isJumping) { // Simple color cycling for jumping cat const jumpColors = [colors.cyan, colors.magenta, colors.blue, colors.yellow]; colorFn = jumpColors[Math.floor(frameCount / 3) % jumpColors.length]; } frame[y][x] = colorFn(sprite[i][j]); } } } gameBox.setContent(frame.map(row => row.join('')).join('\n')); // Update stats with retro styling const levelText = Math.floor(score / 300) + 1; const paddedScore = score.toString().padStart(6, '0'); statsBox.setContent( `{bold}SCORE: ${colors.yellow(paddedScore)} | ` + `LEVEL: ${colors.magenta(levelText)} | ` + `SPEED: ${colors.green((speed).toFixed(1))} | ` + `OBSTACLES: ${colors.red(obstacles.length)}{/bold}` ); screen.render(); } function detectCollision() { const catY = getCatY(); const catX2 = catX + catWidth - 2; // Adjust collision box to be more forgiving for (const obs of obstacles) { const obsX1 = Math.floor(obs.x); const obsX2 = obsX1 + obs.width; // Check for score counting if (!obs.passed && obsX2 < catX) { obs.passed = true; // Add point bonus for each obstacle passed score += 5; } // Collision detection with hitbox adjustment if (obsX2 > catX + 1 && obsX1 < catX2) { const catY2 = catY + catHeight - 1; const obsY1 = groundY - obs.height + 1; if (catY2 > obsY1) { return true; } } } return false; } function startGame() { gameState = 'running'; score = 0; speed = 1; obstacles = []; isJumping = false; frameCount = 0; obstacleTimer = 0; } function gameLoop() { if (gameState === 'title') { renderTitleScreen(); return; } if (gameState === 'paused') return; if (gameState !== 'running') return; // Move obstacles for (const obs of obstacles) { obs.x -= speed; } obstacles = obstacles.filter(o => o.x + o.width > 0); obstacleTimer++; // Base interval that decreases with level and speed const level = Math.floor(score / 200) + 1; let spawnInterval = Math.max(20, 40 - level * 3 - speed * 5); // Make spawning somewhat unpredictable spawnInterval += Math.floor(Math.random() * 10) - 5; if (obstacleTimer >= spawnInterval) { generateObstaclePattern(); obstacleTimer = 0; } // Jump logic if (isJumping) { jumpFrame++; if (jumpFrame >= maxJumpFrame) isJumping = false; } // Collision detection if (detectCollision()) { gameState = 'gameover'; if (score > highScore) highScore = score; renderGameOverScreen(); return; } // Update score and speed score++; if (score % 100 === 0) { speed = Math.min(speed + 0.1, 3.0); } // Render frame renderGame(); } // Start loop setInterval(gameLoop, 100); // Show initial title screen renderTitleScreen(); function startGame() { score = 0; speed = 1; obstacles = []; isJumping = false; jumpFrame = 0; frameCount = 0; obstacleTimer = 0; gameState = 'running'; }