#include #include // Standard pinout for 16x2 LCD Keypad Shield LiquidCrystal lcd(8, 9, 4, 5, 6, 7); // Optional audio output pin (Pin 3 is unused by standard LCD shields) const int BUZZER_PIN = 3; // Button definitions on analog pin A0 #define BTN_RIGHT 0 #define BTN_UP 1 #define BTN_DOWN 2 #define BTN_LEFT 3 #define BTN_SELECT 4 #define BTN_NONE 5 // Custom 5x8 pixel bitmaps byte runnerStanding[8] = { B01110, B01101, B01111, B00110, B01111, B11101, B01010, B10001 }; byte runnerDucking[8] = { B00000, B00000, B00000, B01110, B11111, B01111, B10101, B00000 }; byte obstacleGround[8] = { B00100, B00101, B10101, B10111, B11101, B00100, B00100, B00100 }; // Redesigned airborne hazard: Sharp flying pterodactyl / bird facing left byte obstacleAir[8] = { B00000, B10001, // Wings up B11011, // Wings + body B01110, // Torso B11100, // Beak pointing left B00100, // Tail / claw B00000, B00000 }; const byte CHAR_RUNNER = 0; const byte CHAR_DUCK = 1; const byte CHAR_GROUND = 2; const byte CHAR_AIR = 3; // Player & Game parameters const int PLAYER_COL = 1; enum PlayerPose { RUNNING, JUMPING, DUCKING }; PlayerPose playerState = RUNNING; int jumpTimer = 0; const int JUMP_DURATION = 3; byte terrain[16]; unsigned long lastTick = 0; int gameSpeed = 220; unsigned int score = 0; unsigned int highScore = 0; bool isNewHigh = false; bool gameOver = false; // EEPROM addresses & signature const int EEPROM_INIT_ADDR = 0; const int EEPROM_HS_ADDR = 1; const byte EEPROM_MAGIC = 0x4D; // Read buttons from resistor ladder on A0 int readButtons() { int adc = analogRead(A0); if (adc > 1000) return BTN_NONE; if (adc < 50) return BTN_RIGHT; if (adc < 200) return BTN_UP; if (adc < 400) return BTN_DOWN; if (adc < 600) return BTN_LEFT; if (adc < 850) return BTN_SELECT; return BTN_NONE; } // Sound effects (harmless if no buzzer is connected to pin 3) void playJumpSound() { tone(BUZZER_PIN, 600, 40); } void playDuckSound() { tone(BUZZER_PIN, 300, 30); } void playCrashSound() { tone(BUZZER_PIN, 150, 250); } void playNewHighSound() { tone(BUZZER_PIN, 523, 80); delay(90); tone(BUZZER_PIN, 659, 80); delay(90); tone(BUZZER_PIN, 784, 120); } void resetHighScoreInEEPROM() { highScore = 0; EEPROM.write(EEPROM_INIT_ADDR, EEPROM_MAGIC); EEPROM.put(EEPROM_HS_ADDR, highScore); } void loadHighScore() { // If LEFT is held during power-on or board reset, force clear EEPROM if (readButtons() == BTN_LEFT) { resetHighScoreInEEPROM(); lcd.clear(); lcd.setCursor(0, 0); lcd.print("High Score Reset"); delay(1000); return; } // Check magic byte signature if (EEPROM.read(EEPROM_INIT_ADDR) != EEPROM_MAGIC) { resetHighScoreInEEPROM(); } else { EEPROM.get(EEPROM_HS_ADDR, highScore); // Sanity check to clear uninitialized residual bytes if (highScore > 10000) { resetHighScoreInEEPROM(); } } } void saveHighScore(unsigned int newScore) { highScore = newScore; EEPROM.put(EEPROM_HS_ADDR, highScore); } void resetGame() { for (int i = 0; i < 16; i++) { terrain[i] = 0; } score = 0; gameSpeed = 220; jumpTimer = 0; playerState = RUNNING; gameOver = false; isNewHigh = false; lcd.clear(); } void setup() { pinMode(BUZZER_PIN, OUTPUT); lcd.begin(16, 2); lcd.createChar(CHAR_RUNNER, runnerStanding); lcd.createChar(CHAR_DUCK, runnerDucking); lcd.createChar(CHAR_GROUND, obstacleGround); lcd.createChar(CHAR_AIR, obstacleAir); loadHighScore(); randomSeed(analogRead(A1)); resetGame(); } void loop() { int btn = readButtons(); if (gameOver) { // Press any button to restart if (btn != BTN_NONE) { delay(250); // Debounce resetGame(); } return; } // Player controls if ((btn == BTN_UP || btn == BTN_SELECT) && playerState == RUNNING) { playerState = JUMPING; jumpTimer = JUMP_DURATION; playJumpSound(); } else if (btn == BTN_DOWN && playerState == RUNNING) { playerState = DUCKING; playDuckSound(); } else if (btn != BTN_DOWN && playerState == DUCKING) { playerState = RUNNING; } // Game clock tick if (millis() - lastTick >= (unsigned long)gameSpeed) { lastTick = millis(); // Jump duration counter if (playerState == JUMPING) { jumpTimer--; if (jumpTimer <= 0) { playerState = RUNNING; } } // Shift terrain left for (int i = 0; i < 15; i++) { terrain[i] = terrain[i + 1]; } // Spawn obstacles at column 15 if (terrain[14] == 0 && terrain[13] == 0 && random(0, 100) < 38) { terrain[15] = (random(0, 100) < 65) ? 1 : 2; // 1 = Ground, 2 = Air } else { terrain[15] = 0; } // Collision detection byte currentObstacle = terrain[PLAYER_COL]; if (currentObstacle == 1 && playerState != JUMPING) { gameOver = true; } else if (currentObstacle == 2 && playerState != DUCKING) { gameOver = true; } if (gameOver) { playCrashSound(); if (score > highScore) { saveHighScore(score); isNewHigh = true; playNewHighSound(); } lcd.clear(); lcd.setCursor(0, 0); if (isNewHigh) { lcd.print("NEW RECORD! "); lcd.print(score); } else { lcd.print("OVER! Score:"); lcd.print(score); } lcd.setCursor(0, 1); lcd.print("Best: "); lcd.print(highScore); lcd.print(" [Press]"); return; } // Draw frame for (int r = 0; r < 2; r++) { lcd.setCursor(0, r); for (int c = 0; c < 16; c++) { if (c == PLAYER_COL) { if (r == 0) { lcd.write(playerState == JUMPING ? CHAR_RUNNER : ' '); } else { if (playerState == RUNNING) lcd.write(CHAR_RUNNER); else if (playerState == DUCKING) lcd.write(CHAR_DUCK); else lcd.print(" "); } } else { if (r == 1 && terrain[c] == 1) { lcd.write(CHAR_GROUND); } else if (r == 0 && terrain[c] == 2) { lcd.write(CHAR_AIR); } else { lcd.print(" "); } } } } // Score display (top-right corner) lcd.setCursor(12, 0); if (score < 10) lcd.print(" "); else if (score < 100) lcd.print(" "); else if (score < 1000) lcd.print(" "); lcd.print(score); score++; // Accelerate gameplay every 25 points down to 90ms/frame if (score % 25 == 0 && gameSpeed > 90) { gameSpeed -= 10; } } }