// ============================================================================= // Name: Pip-Snake // License: CC-BY-NC-4.0 // Repository: https://github.com/CodyTolene/pip-apps // Description: A simple snake game for the Pip-Boy 3000 Mk V. // Version: 1.0.0 // ============================================================================= const SCREEN_WIDTH = g.getWidth(); const SCREEN_HEIGHT = g.getHeight(); const TILE_SIZE = 16; const GRID_WIDTH = Math.floor(SCREEN_WIDTH / TILE_SIZE); const GRID_HEIGHT = Math.floor(SCREEN_HEIGHT / TILE_SIZE); const GAME_SPEED = 200; const PADDING_X = 3; const PADDING_Y = 0; const COLOR_GREEN = '#0F0'; const COLOR_RED = '#F00'; const DIRECTIONS = [ { x: 1, y: 0 }, // Right { x: 0, y: 1 }, // Down { x: -1, y: 0 }, // Left { x: 0, y: -1 }, // Up ]; var snake, directionIndex, food, gameOver, gameLoopInterval, score = 0; function stopGame() { if (gameLoopInterval) { clearInterval(gameLoopInterval); } gameOver = true; g.clear(); g.setColor(COLOR_RED); g.setFont('6x8', 4); g.drawString('GAME HALTED', SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 30); g.setFont('6x8', 2); g.drawString( 'Press torch button again', SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 10, ); g.drawString( 'to restart the device.', SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 30, ); console.log('GAME HALTED'); const waitLoop = setInterval(() => { if (BTN_TORCH.read()) { clearInterval(waitLoop); E.reboot(); } }, 100); } function resetGame() { snake = [{ x: 5, y: 5 }]; directionIndex = 0; // Start as moving right food = { x: 8, y: 5 }; gameOver = false; score = 0; spawnFood(); g.clear(); drawCell(food.x, food.y, COLOR_RED); // Initial food drawScore(); // Initial score snake.forEach( ( segment, // Initial snake ) => drawCell(segment.x, segment.y, COLOR_GREEN), ); if (gameLoopInterval) { clearInterval(gameLoopInterval); } gameLoopInterval = setInterval(gameLoop, GAME_SPEED); } function spawnFood() { var newX, newY, collision; do { newX = Math.floor(Math.random() * (GRID_WIDTH - PADDING_X * 2)) + PADDING_X; newY = Math.floor(Math.random() * (GRID_HEIGHT - PADDING_Y * 2)) + PADDING_Y; collision = snake.some( (segment) => segment.x === newX && segment.y === newY, ); } while (collision); food.x = newX; food.y = newY; } function drawCell(x, y, color) { g.setColor(color); const px = x * TILE_SIZE + PADDING_X; const py = y * TILE_SIZE + PADDING_Y; g.fillRect(px, py, px + TILE_SIZE - 1, py + TILE_SIZE - 1); } function drawScore() { const fixedWidth = 120; const rectX = (SCREEN_WIDTH - fixedWidth) / 2; const rectY = SCREEN_HEIGHT - 26; g.clearRect(rectX, rectY, rectX + fixedWidth, SCREEN_HEIGHT); g.setColor(COLOR_GREEN); g.setFont('6x8', 2); g.drawString('Score: ' + score, SCREEN_WIDTH / 2, SCREEN_HEIGHT - 20); } function updateSnake() { if (gameOver) return; var head = { x: snake[0].x + DIRECTIONS[directionIndex].x, y: snake[0].y + DIRECTIONS[directionIndex].y, }; // Wrap around screen if (head.x < PADDING_X) head.x = GRID_WIDTH - 1 - PADDING_X; if (head.x >= GRID_WIDTH - PADDING_X) head.x = PADDING_X; if (head.y < PADDING_Y) head.y = GRID_HEIGHT - 1 - PADDING_Y; if (head.y >= GRID_HEIGHT - PADDING_Y) head.y = PADDING_Y; const hasCollided = snake.some( (segment) => segment.x === head.x && segment.y === head.y, ); if (hasCollided) { gameOver = true; g.clear(); g.setColor(COLOR_GREEN); g.setFont('6x8', 4); g.drawString('GAME OVER', SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 30); g.setFont('6x8', 2); g.drawString( 'Press tuner-play to restart', SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 10, ); return; } // Move snake snake.unshift(head); const isFoodEaten = head.x === food.x && head.y === food.y; if (isFoodEaten) { score++; spawnFood(); } else { // Clear old tail position const tail = snake.pop(); drawCell(tail.x, tail.y, '#000'); } drawCell(head.x, head.y, COLOR_GREEN); drawCell(food.x, food.y, COLOR_RED); drawScore(); } function handleInput() { if (BTN_TUNEDOWN.read()) { // Turn left directionIndex = (directionIndex + 3) % 4; } else if (BTN_TUNEUP.read()) { // Turn right directionIndex = (directionIndex + 1) % 4; } else if (BTN_TORCH.read()) { stopGame(); } else if (BTN_PLAY.read()) { resetGame(); } } function gameLoop() { handleInput(); updateSnake(); } function initializeGame() { g.clear(); g.setColor(COLOR_GREEN); g.setFont('6x8', 4); g.drawString('PIP-SNAKE', SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 100); g.setFont('6x8', 2); g.drawString( 'Control a Pip-Snake, eat food', SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 60, ); g.drawString( 'Grow as big as you can!', SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 40, ); g.setFont('6x8', 2); g.setColor(COLOR_RED); g.drawString( 'But dont eat yourself!', SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 20, ); g.setColor(COLOR_GREEN); g.drawString( 'Tip: Hitting edges wraps around', SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, ); g.drawString( 'Use tuner-up/down to turn', SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 20, ); g.drawString( 'Torch: Quit | Tuner-play: Restart', SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 40, ); g.drawString( 'Press tuner-play to START', SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 80, ); const waitLoop = setInterval(() => { if (BTN_PLAY.read()) { clearInterval(waitLoop); resetGame(); } }, 100); } initializeGame();