#!/usr/bin/env node 'use strict'; const readline = require('readline'); // --- Config --- const W = 40, H = 20; const TICK = 100; // ms per frame // --- ANSI helpers --- const ESC = '\x1b['; const cls = () => process.stdout.write('\x1bc'); const hide = () => process.stdout.write(ESC + '?25l'); const show = () => process.stdout.write(ESC + '?25h'); const move = (x, y) => process.stdout.write(ESC + `${y};${x}H`); const color = (code, s) => `\x1b[${code}m${s}\x1b[0m`; const green = s => color('32;1', s); const yellow = s => color('33;1', s); const red = s => color('31;1', s); const cyan = s => color('36;1', s); const white = s => color('37;1', s); const dim = s => color('2', s); // --- State --- let snake, dir, nextDir, food, score, highScore = 0, running, timer; function randPos() { return { x: 1 + Math.floor(Math.random() * (W - 2)), y: 1 + Math.floor(Math.random() * (H - 2)) }; } function spawnFood() { let pos; do { pos = randPos(); } while (snake.some(s => s.x === pos.x && s.y === pos.y)); food = pos; } function init() { const mid = { x: Math.floor(W / 2), y: Math.floor(H / 2) }; snake = [mid, { x: mid.x - 1, y: mid.y }, { x: mid.x - 2, y: mid.y }]; dir = { x: 1, y: 0 }; nextDir = { x: 1, y: 0 }; score = 0; spawnFood(); running = true; } function drawBorder() { const top = cyan('╔' + '═'.repeat(W - 2) + '╗'); const bottom = cyan('╚' + '═'.repeat(W - 2) + '╝'); move(1, 1); process.stdout.write(top); move(1, H); process.stdout.write(bottom); for (let row = 2; row < H; row++) { move(1, row); process.stdout.write(cyan('║')); move(W, row); process.stdout.write(cyan('║')); } } function drawStatus() { move(1, H + 1); process.stdout.write( white(' Score: ') + yellow(String(score).padEnd(6)) + white(' Best: ') + yellow(String(highScore).padEnd(6)) + dim(' [←↑→↓] move [q] quit') ); } function drawGame() { // Clear interior for (let row = 2; row < H; row++) { move(2, row); process.stdout.write(' '.repeat(W - 2)); } // Food move(food.x, food.y); process.stdout.write(red('◆')); // Snake snake.forEach((seg, i) => { move(seg.x, seg.y); process.stdout.write(i === 0 ? green('█') : green('▓')); }); drawStatus(); } function tick() { dir = nextDir; const head = { x: snake[0].x + dir.x, y: snake[0].y + dir.y }; // Wall collision if (head.x <= 0 || head.x >= W || head.y <= 0 || head.y >= H) { return gameOver(); } // Self collision if (snake.some(s => s.x === head.x && s.y === head.y)) { return gameOver(); } snake.unshift(head); if (head.x === food.x && head.y === food.y) { score++; if (score > highScore) highScore = score; spawnFood(); } else { snake.pop(); } drawGame(); } function gameOver() { running = false; clearInterval(timer); const msg = red(' GAME OVER '); const sub = ` Score: ${yellow(String(score))} `; const reLine = ` Press ${green('r')} to restart or ${red('q')} to quit `; const cx = Math.floor(W / 2) - 12; const cy = Math.floor(H / 2) - 1; move(cx, cy); process.stdout.write('┌' + '─'.repeat(28) + '┐'); move(cx, cy + 1); process.stdout.write('│' + msg.padEnd(28 + msg.length - 13) + '│'); move(cx, cy + 2); process.stdout.write('│' + sub.padEnd(28 + sub.length - 14) + '│'); move(cx, cy + 3); process.stdout.write('│' + reLine.padEnd(28 + reLine.length - 27) + '│'); move(cx, cy + 4); process.stdout.write('└' + '─'.repeat(28) + '┘'); } function startGame() { cls(); hide(); init(); drawBorder(); drawGame(); timer = setInterval(tick, TICK); } // --- Input --- readline.emitKeypressEvents(process.stdin); if (process.stdin.isTTY) process.stdin.setRawMode(true); process.stdin.on('keypress', (_, key) => { if (!key) return; if (key.name === 'q' || (key.ctrl && key.name === 'c')) { clearInterval(timer); show(); cls(); move(1, 1); console.log(white('Thanks for playing! Final score: ') + yellow(String(score))); process.exit(0); } if (!running) { if (key.name === 'r') { startGame(); } return; } const moves = { up: { x: 0, y: -1 }, down: { x: 0, y: 1 }, left: { x: -1, y: 0 }, right: { x: 1, y: 0 }, }; const next = moves[key.name]; if (!next) return; // Prevent 180° reversal if (next.x === -dir.x && next.y === -dir.y) return; nextDir = next; }); // --- Title screen --- cls(); move(1, Math.floor(H / 2) - 3); const logo = [ ' ███████╗███╗ ██╗ █████╗ ██╗ ██╗███████╗', ' ██╔════╝████╗ ██║██╔══██╗██║ ██╔╝██╔════╝', ' ███████╗██╔██╗██║███████║█████╔╝ █████╗ ', ' ╚════██║██║╚████║██╔══██║██╔═██╗ ██╔══╝ ', ' ███████║██║ ╚███║██║ ██║██║ ██╗███████╗', ' ╚══════╝╚═╝ ╚══╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝', ]; logo.forEach((line, i) => { move(1, Math.floor(H / 2) - 3 + i); process.stdout.write(green(line)); }); move(1, Math.floor(H / 2) + 4); process.stdout.write(white(' Use ') + cyan('arrow keys') + white(' to move · ') + yellow('eat ◆ to grow') + white(' · ') + red('q') + white(' to quit')); move(1, Math.floor(H / 2) + 6); process.stdout.write(white(' Press ') + green('any arrow key') + white(' to start...')); // Start on first keypress process.stdin.once('keypress', (_, key) => { if (key && (key.ctrl && key.name === 'c')) process.exit(0); startGame(); });