/*
Authors: Jean Achour, Paola Guedes
Project: Chrome Dino Arduino
*/
// Libraries
#include <LiquidCrystal_I2C.h>
#include <math.h>
#include <string.h>
// Definitions
#define JUMPER_PORT (2) // Jumper port on Arduino
#define DISPLAY_WIDTH (16) // Display length
#define DISPLAY_HEIGHT (2) // Display height
#define TOP_ROW (0) // Index of the top row
#define BOTTOM_ROW (1) // Index of the bottom row
#define FIRST_COL (0) // Index of the first column
#define LAST_COL (DISPLAY_WIDTH - 1) // Index of the last column
#define FRAME_RATE (100) // Frame rate of the screen
#define RESET_GAME_STATUS (-2) // Reset game status indicator
#define GAME_OVER_STATUS (-1) // Game over status indicator
#define PLAYING_NO_COLLISION_STATUS (0) // Game is running and there was no collisions status indicator
#define PLAYING_COLLISION_HAPPENED_STATUS (1) // Game is running and there was a collision status indicator
const int EMPTY_TERRAIN = -1; // Matrix value for the empty terrains
const int DINO = 0; // Char id for Dino
const int BIRD = 1; // Char id for Bird
const int CACTUS = 2; // Char id for Cactus
const int SKULL = 3; // Char id for Skull
// Global variables
LiquidCrystal_I2C lcd(0x27, DISPLAY_WIDTH, DISPLAY_HEIGHT); // Physical LCD API definition
int display[DISPLAY_HEIGHT][DISPLAY_WIDTH]; // Screen logical matrix
int isDinoOnAir = 0; // Indicator of how many frames already were updated
int gameStatus = 0; // Status indicator of the state of the game (assumes values -2 [RESET_GAME], -1 [GAME_OVER_IS_SHOWN], 0 [PLAYING_NO_COLLISION], 1 [PLAYING_COLLISION_HAPPENED])
int score = 0; // Holds current score (measured in how many frames the player is alive)
int highScore = 0; // Holds the current High Score
int currentGameOverMessage = 0; // Aux variable for changing the game over screen text
byte dino[8] = { // Dino char definition
B00111,
B00101,
B00111,
B10110,
B11111,
B11110,
B01110,
B00010
};
byte cactus[8] = { // Cactus char definition
B00000,
B00000,
B00000,
B01000,
B01010,
B01110,
B01110,
B00100
};
byte bird[8] = { // Bird char definition
B00000,
B01010,
B11110,
B01000,
B00000,
B00000,
B00000,
B00000
};
byte skull[8] = { // Skull char definition
B00000,
B01110,
B10101,
B11011,
B01110,
B01110,
B00000,
B00000
};
// Functions Prototypes
// --> LCD Aux Functions
void initDisplay();
void updateLCD();
void writeGameOver();
void writeScore(int, const char *);
void resetGame();
void showGameOver();
// --> Chars Aux Functions
void jump();
void fall();
void plotObstacle(int);
void moveObstaclesLeft();
// --> Interrupt Handler Function
void handleInterrupt();
// Initializes the Arduino and setup LCD
void setup() {
pinMode(JUMPER_PORT, INPUT_PULLUP); // Sets the jumper port to listen to INPUT_PULLUP
randomSeed(analogRead(0)); // Sets a random seed to plot obstacles randomly
lcd.init(); // Initializes the LCD
lcd.backlight(); // Turns on the screen
// Creates and sets our custom chars
lcd.createChar(DINO, dino);
lcd.createChar(BIRD, bird);
lcd.createChar(CACTUS, cactus);
lcd.createChar(SKULL, skull);
// Begins LCD
lcd.begin(DISPLAY_WIDTH, DISPLAY_HEIGHT);
// Attachs handleInterrupt function to the CHANGE event at the JUMPER_PORT
attachInterrupt(digitalPinToInterrupt(JUMPER_PORT), handleInterrupt, CHANGE);
// Initializes the display and starts the game
resetGame();
}
void loop() {
if (gameStatus == RESET_GAME_STATUS) { // Should reset the game
resetGame();
return;
}
if (gameStatus == GAME_OVER_STATUS) { // Is on game over screen waiting for restart
showGameOver();
return;
}
if (gameStatus == PLAYING_COLLISION_HAPPENED_STATUS) { // Handle collision and show game over screen
showGameOver();
// Updates the high score if needed
if (score > highScore) {
highScore = score;
}
gameStatus = GAME_OVER_STATUS;
return;
}
if (gameStatus == PLAYING_NO_COLLISION_STATUS) { // Game is running and there was no collision yet
int shouldPlotObstacle = random(1, 11) > 9 ? random(1, 3) : 0;
if (display[TOP_ROW][FIRST_COL] == DINO) { // If Dino is on air
isDinoOnAir = (isDinoOnAir + 1) % 5; // --> Keeps dino on air for 5 frame rates
}
if (!isDinoOnAir) { // If Dino should fall, then fall
fall();
}
moveObstaclesLeft();
if (shouldPlotObstacle) {
plotObstacle(shouldPlotObstacle);
}
updateLCD();
score++;
delay(FRAME_RATE);
}
}
// Initialize the display variable and sets Dino to its initial position
void initDisplay() {
int row, col;
for (row = 0; row < DISPLAY_HEIGHT; row++) {
for (col = 0; col < DISPLAY_WIDTH; col++) {
display[row][col] = EMPTY_TERRAIN;
}
}
display[BOTTOM_ROW][FIRST_COL] = DINO; // Positions Dino to the bottom row at first column
}
// Writes current display values and update the LCD
void updateLCD() {
int row, col;
lcd.clear();
for (row = 0; row < DISPLAY_HEIGHT; row++) {
for (col = 0; col < DISPLAY_WIDTH; col++) {
if (display[row][col] != EMPTY_TERRAIN) { // If position it's not a empty terrain
lcd.setCursor(col, row); // Positions the writing cursor of the LCD to the current position
lcd.write(display[row][col]); // Writes the char in the matrix to the LCD
}
}
}
}
// Clears current Dino position and put it at the top row of the display
void jump() {
display[BOTTOM_ROW][FIRST_COL] = EMPTY_TERRAIN;
display[TOP_ROW][FIRST_COL] = DINO;
}
// Clears current Dino position and put it at the bottom row of the display
void fall() {
display[TOP_ROW][FIRST_COL] = EMPTY_TERRAIN;
display[BOTTOM_ROW][FIRST_COL] = DINO;
}
// Receives and obstacle, verifies where it should be written and then writes it to the display at the last column
void plotObstacle(int obstacle) {
int row = obstacle == BIRD ? TOP_ROW : BOTTOM_ROW;
display[row][LAST_COL] = obstacle;
}
// Verifies if the position passed is an obstacle or not
bool isPositionAnObstacle(int row, int col) {
return display[row][col] != EMPTY_TERRAIN && display[row][col] != DINO;
}
// Move obstacles to the left and verifies for possible collisions
void moveObstaclesLeft() {
int col;
// Verifies from the first col to the last col of the first row
for (col = FIRST_COL; col <= LAST_COL; col++) {
if (isPositionAnObstacle(TOP_ROW, col)) { // If it's an obstacle
if (col == FIRST_COL) { // --> If it's the first column
display[TOP_ROW][col] = EMPTY_TERRAIN; // --> Clears the first column of the first row
} else if (isDinoOnAir && display[TOP_ROW][col - 1] == DINO) { // --> Else if Dino is still in the air and will be hit
gameStatus = 1; // --> Tells the program that a collision happened
} else { // --> Else
display[TOP_ROW][col - 1] = display[TOP_ROW][col]; // --> Move obstacle left
display[TOP_ROW][col] = EMPTY_TERRAIN; // --> Clears old position
}
}
}
// Verifies from the first col to the last col of the bottom row
for (col = FIRST_COL; col <= LAST_COL; col++) {
if (isPositionAnObstacle(BOTTOM_ROW, col)) { // If it's an obstacle
if (col == FIRST_COL) { // --> If it's the first column
display[BOTTOM_ROW][FIRST_COL] = EMPTY_TERRAIN; // --> Clears the first column of the first row
} else if (col == FIRST_COL + 1 && display[BOTTOM_ROW][col - 1] == DINO) { // --> Else if it's the second column and Dino will be hit
gameStatus = 1; // --> Tells the program that a collision happened
} else { // --> Else
display[BOTTOM_ROW][col - 1] = display[BOTTOM_ROW][col]; // --> Move obstacle left
display[BOTTOM_ROW][col] = EMPTY_TERRAIN; // --> Clears old position
}
}
}
}
// Writes the game over screen on top row
void writeGameOver() {
lcd.setCursor(FIRST_COL, TOP_ROW);
lcd.print(" ");
lcd.write(SKULL);
lcd.print(" YOU LOSE ");
lcd.write(SKULL);
lcd.print(" ");
}
// Writes the final game score on bottom row
void writeScore(int s, const char *label) {
const int lengthOfNumber = min(log10(ceil(s)) + 1, 5);
const int howManyLeftSpacesIsNeeded = (DISPLAY_WIDTH - lengthOfNumber - strlen(label)) / 2;
int col;
// Writes the score
lcd.setCursor(FIRST_COL, BOTTOM_ROW);
for (col = 0; col < howManyLeftSpacesIsNeeded; col++) {
lcd.print(" ");
}
lcd.print(label);
lcd.print(s);
}
// Restarts the game
void resetGame() {
// Reset variables
score = 0;
isDinoOnAir = 0;
gameStatus = 0;
currentGameOverMessage = 0;
initDisplay(); // Clears the display variable
updateLCD(); // Updates the LCD
}
// Handle the interruption when the button is clicked
void handleInterrupt() {
int buttonWasClicked = digitalRead(JUMPER_PORT) == 0;
delay(10);
if (gameStatus == 0) { // If it's not a collision
if (buttonWasClicked && display[BOTTOM_ROW][FIRST_COL] == DINO) { // --> If Dino it's on the ground and the button was clicked
jump(); // --> Then jump!
}
} else if (gameStatus == -1) { // Else if is the game over screen
if (buttonWasClicked) { // --> If the button was clicked
gameStatus = -2; // --> Sets the flag to -2, indicating that the game should restart
}
}
}
// Show game over screen
void showGameOver() {
lcd.clear();
if (currentGameOverMessage == 1) {
lcd.print(" PRESS AGAIN");
writeScore(highScore, "HIGH SCORE: ");
currentGameOverMessage = 0; // Toggles to Score Screen
} else {
writeGameOver();
writeScore(score, "SCORE: ");
currentGameOverMessage = 1; // Toggles to High Score Screen
}
delay(2000); // Waits for 2 seconds before changing screens
}