diff --git a/dinorunner/dino_runner.ino b/dinorunner/dino_runner.ino new file mode 100644 index 0000000..dc83cc3 --- /dev/null +++ b/dinorunner/dino_runner.ino @@ -0,0 +1,304 @@ +#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; + } + } +} \ No newline at end of file