Posts etiquetados ‘proyectos arduino lcd’

Veinte años han transcurrido desde aquel 19 de Junio de 1999 cuando fue lanzada aquella primera beta de un mod para Half-Life. Un mod que llegó a ser mas popular que el propio juego para el que fue creado y tiempo después se convirtió en el juego de disparos en primera persona en línea de referencia. Es aún, veinte años después de su salida, el juego más jugado en LAN partys e Internet. La última versión del juego es el Counter-Strike: Global Offensive, una de las versiones del juego más jugado en el mundo, ante juegos más recientes, como la versión Counter-Strike: Source, basado en el motor Source, desarrollado para el juego Half-Life 2.

Llevo muchos años jugando al CS, desde que apareció como mod del HL1. Durante todos estos años, como todo jugador he ido evolucionando, al principio era increíble la diversión que podía crear este juego, me lo pasaba realmente bien sin importarme si jugaba bien o no, simplemente era divertido. Pero supongo que como todo jugador, he ido evolucionando y ahora disto mucho de ser el que era cuando empecé en esto, ahora más que divertirme me desespera. Ya no juego por diversión sino para comprobar que sigo teniendo el mismo nivel que tuve en su día (aunque ya no sea lo que fui…se me “pianta” un lagrimón como diría Carlos Gardel). Como maker, y ya que este juego (entre otros) marcó con fuego una etapa de mi vida, decidí crear una réplica completamente funcional de la bomba C4 que como terroristas debemos colocar en una de las dos zonas definidas (zona A y zona B).

Al momento de crear una réplica cualquiera (funcional o no) debemos investigar, examinar y reunir la mayor cantidad de información posible del objeto a replicar. Como siempre, el secreto está en los detalles. Tanto el diseño estético como funcional podemos tomarlo del mismo juego ya que en algún momento, como terroristas, hemos tenido la bomba en nuestras manos para "plantarla". A continuación analizaremos la parte electrónica y funcional de la bomba para recrearla en nuestro código fuente. Las características que presenta la bomba son las siguientes:

  • Teclado numérico de 4 filas, 3 columnas con dígitos 0-9, * y #. Marco color negro, teclas color blanco y serigrafía numérica color negro. Utilizaremos un teclado idéntico.
  • Display LCD, fondo amarillo con letras negras, retroiluminado. Utilizaremos un display LCD 16 columnas, 2 filas con las mismas características e interfaz I2C para simplificar las conexiones.
  • LED de 5mm, bicolor (rojo/verde), alto brillo. Utilizaremos un LED idéntico.
  • Buzzer pasivo. Utilizaremos un buzzer pasivo de 5V.
  • Batería 9V.

En cuanto a la parte funcional, la bomba presenta las siguientes características:

  • Cuando comenzamos la ronda, la bomba se encuentra armada y lista para ser "plantada". El LED emite un destello verde cada 1500 milisegundos. A esto podemos apreciarlo si dejamos caer la bomba al piso.
  • Posee un código numérico de activación: 7355608. Una vez introducida esta secuencia el display muestra una serie de 7 * (siete asteriscos) ocultando el código de activación.
  • Una vez "plantada" la bomba comienza una cuenta regresiva de 40 segundos y el buzzer comienza a emitir un "beep" continuo. El LED cambia de color verde a rojo y comienza a destellar al mismo intervalo del "beep", inicialmente cada 1000 milisegundos.
  • Para desactivar la bomba es necesario introducir el mismo código de activación (7355608) antes de que termine la cuenta regresiva de 40 segundos.
  • A medida que disminuye la cuenta regresiva, el LED y el buzzer emiten destellos y "beeps" respectivamente con menor intervalo de tiempo. Cuando la cuenta regresiva llega a 25 segundos, el intervalo se reduce a 800 milisegundos, a los 15 segundos el intervalo se reduce a 600 milisegundos, a los 9 segundos se reduce a 400 milisegundos, a los 6 segundos se reduce a 200 milisegundos y a los 3 segundos se reduce a 150 milisegundos.
  • Una vez finalizada la cuenta regresiva la detonación es inevitable. El LED cambia de color rojo a verde por 400 milisegundos mientras el buzzer emite 15 "beeps" continuos (con un intervalo de 80 milisegundos entre cada "beep") antes de la detonación y finaliza la ronda dando por ganadora a la facción terrorista.
  • En caso de que la facción antiterrorista logre desactivar la bomba antes de finalizar la cuenta regresiva de 40 segundos, el LED se apaga y el buzzer deja de emitir sonido cuando la bomba es desactivada. La ronda finaliza dando por ganadora a la facción antiterrorista.

Con la información reunida hasta este punto estamos en condiciones de recrear perfectamente la bomba de Counter-Strike: Global Offensive, ¿pero que sucedería si decidimos utilizar la bomba para algún otro fin que no sea una mera réplica funcional de este inefable juego? Por ejemplo, para Airsoft o jugar una broma en la escuela? Podemos realizar dos modificaciones muy sencillas sin afectar de ninguna manera la parte estética ni funcional mencionadas anteriormente con lujo de detalle. Estas modificaciones consisten en lo siguiente:

  • Modificar el tiempo de detonación predeterminado. Las secuencias finales de efectos (luz y sonido) no son afectadas. El tiempo puede establecerse entre 1 y 99999 segundos. Si se presiona la tecla # sin ingresar ningún valor establece la cuenta regresiva en 40 segundos.
  • Modificar el código de activación/desactivación por defecto. El código puede contener entre 1 y 16 dígitos. Si se presiona la tecla # sin ingresar ningún valor establece el código en 7355608.

Como vemos, con estas modificaciones tenemos una bomba completamente personalizable para otros fines además de ser una réplica de la utilizada en Counter-Strike: Global Offensive. Para emular el funcionamiento del C4 de CS: GO solo debemos presionar dos veces la tecla #.

Hasta aquí no solo tenemos una réplica funcional, sino que también le hemos agregado personalización sin afectar el funcionamiento original de nuestra querida bomba. La parte estética queda a criterio del diseñador, no es estrictamente obligatorio diseñar el explosivo idéntico al del juego. Es posible simular dinamita o cualquier otro explosivo, en fin, la imaginación es el límite. Por esta razón he armado la parte electrónica sobre un protoboard a modo genérico.

El diagrama esquemático de la bomba se observa en la siguiente imagen:

(click para ampliar)

Lista de Componentes
1 Display LCD con interfaz I2C (fondo amarillo, letras negras) 16×2 con controlador Hitachi HD44780 o compatible.
1 Teclado Numérico 4×3 (marco color negro, teclas color blanco y serigrafía numérica color negro).
1 Transistor 2SC1815
1 Resistencia 10KΩ 0.25W
1 Resistencia 270Ω 0.25W
1 Resistencia 330Ω 0.25W
1 LED 5mm bicolor rojo/verde
1 Buzzer Pasivo 5V
1 Batería 9V

Antes de compilar y cargar el código en nuestra en nuestra placa Arduino, es necesario descargar e instalar las siguientes librerías:
Librería Keypad de Mark Stanley, Alexander Brevig.
Librería NewTone de Tim Eckel.
Librería NewLiquidCrystal de Francisco Malpartida. Esta librería es un reemplazo de la librería LiquidCrystal incluida por defecto en el IDE Arduino, por lo tanto es necesario eliminar la librería original.

#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
#include <NewTone.h>

#define DEFAULT_PASSWORD "7355608" // default password (must not exceed 16 chars)
#define TIME_BOMB 40 // default countdown time


#define MAX_PASSWORD_LENGTH 17 // max password length (16 chars) + ('\0'). PLEASE NOT MODIFY!!!

#define GREEN_LED_PIN 12
// RED LED PIN -> LED_BUILTIN (13)

#define TONE_PIN 9
#define TONE_FREQ 2000
#define TONE_DUR 120
#define KEYTONE_FREQ 3000

#define I2C_ADDR 0x27

char password[MAX_PASSWORD_LENGTH] = {'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'};
char data[MAX_PASSWORD_LENGTH] = {'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'};

unsigned long cuMillis, preMillis = 0, tempo = 0;

byte index = 0, codepos = 0, pwdLength;

char customKey;

const byte rows = 4;
const byte cols = 3;

char keyMap[rows][cols] = {
    {'1', '2', '3'},
    {'4', '5', '6'},
    {'7', '8', '9'},
    {'*', '0', '#'}
};

byte rowPins[rows] = {5, 4, 3, 2};
byte colPins[cols] = {8, 7, 6};

Keypad myKeypad = Keypad(makeKeymap(keyMap), rowPins, colPins, rows, cols);

LiquidCrystal_I2C lcd(I2C_ADDR, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);

char keyget() {

    char whichKey = myKeypad.getKey(); //define which key is pressed with getKey

    if (whichKey != NO_KEY) {

        if (whichKey == '*' || whichKey == '#') {
            index = 0;
            lcd.home();
            lcd.print(F("****************"));
            codepos = 0;
            lcd.home();
            whichKey = NO_KEY;
        }
        else {
            codepos++;
            lcd.setCursor((codepos - 1), 0);
            lcd.print(whichKey);
            NewTone(TONE_PIN, KEYTONE_FREQ, TONE_DUR);
        }

        if (codepos == pwdLength) {
            lcd.home();
            lcd.print(F("****************"));
            codepos = 0;
            lcd.home();
        }
    }

    return whichKey;
}

byte setPassword() {
    char key;
    byte count = 0;

    lcd.clear();
    lcd.print(F("SET PASSWORD"));
    lcd.setCursor(0, 1);

    while (count < (MAX_PASSWORD_LENGTH - 1)) {

        key = myKeypad.getKey();

        if (key != NO_KEY) {

            if (key == '#') {

                if (count == 0) {
                    strncpy(password, DEFAULT_PASSWORD, sizeof(DEFAULT_PASSWORD));
                }

                break;
            }

            if (key == '*') {
                lcd.setCursor(0, 1);
                lcd.print(F("                "));

                for (byte i = 0; i < MAX_PASSWORD_LENGTH; i++) {
                    password[i] = '\0';
                }

                count = 0;
            }
            else {
                lcd.setCursor(count, 1);
                lcd.print(key);
                password[count] = key;
                NewTone(TONE_PIN, KEYTONE_FREQ, TONE_DUR);
                count++;
            }
        }

        while (count == (MAX_PASSWORD_LENGTH - 1)) {

            key = myKeypad.getKey();

            if (key != NO_KEY) {

                if (key == '#') {
                    break;
                }

                if (key == '*') {
                    lcd.setCursor(0, 1);
                    lcd.print(F("                "));

                    for (byte i = 0; i < MAX_PASSWORD_LENGTH; i++) {
                        password[i] = '\0';
                    }

                    count = 0;
                }
            }
        }
    }

    return (strlen(password));
}

void setTime() {
    char key;
    byte count = 0;

    lcd.clear();
    lcd.print(F("SET TIME (secs)"));
    lcd.setCursor(0, 1);

    while (count < 5) {

        key = myKeypad.getKey();

        if (key != NO_KEY) {

            if (key == '#') {

                if (count == 0) {
                    tempo = TIME_BOMB;
                }

                break;
            }

            if (key == '*') {
                lcd.setCursor(0, 1);
                lcd.print(F("     "));

                tempo = 0;
                count = 0;
            }
            else {
                lcd.setCursor(count, 1);
                lcd.print(key);
                NewTone(TONE_PIN, KEYTONE_FREQ, TONE_DUR);
                tempo = (tempo * 10) + key - '0';
                count++;
            }
        }

        while (count == 5) {

            key = myKeypad.getKey();

            if (key != NO_KEY) {

                if (key == '#') {
                    break;
                }

                if (key == '*') {
                    lcd.setCursor(0, 1);
                    lcd.print(F("     "));

                    tempo = 0;
                    count = 0;
                }
            }
        }
    }
}

void countdown(unsigned int timesincestart) {

    unsigned long timeleft, currentMillis, previousMillis = 0;
    unsigned int count = TIME_BOMB, interval = 1000;
    char whichKey;

    digitalWrite(GREEN_LED_PIN, LOW);

    lcd.clear();
    lcd.print(F("****************"));
    lcd.setCursor(0, 1);
    lcd.print(F("BOMB ARMED "));

    while (count > 0) {

        whichKey = keyget();

        if (whichKey != NO_KEY) {

            data[index] = whichKey;
            index++;

            if (index == pwdLength) {
                index = 0;
                if(strcmp(data, password) == 0) {
                    lcd.print(F(" BOMB DISARMED  "));
                    lcd.setCursor(0, 1);
                    lcd.print(F("    CT'S WIN    "));
                    digitalWrite(LED_BUILTIN, LOW);
                    while (1);
                }
            }
        }

        lcd.setCursor(11, 1);
        timeleft = tempo - (millis() / 1000 - timesincestart);
        lcd.print(timeleft);

        switch (timeleft) {
            case 9999:
                lcd.setCursor(15, 1);
                lcd.print(" ");
                break;

            case 999:
                lcd.setCursor(14, 1);
                lcd.print(" ");
                break;

            case 99:
                lcd.setCursor(13, 1);
                lcd.print(" ");
                break;

            case 25:
                interval = 800;
                break;

            case 15:
                interval = 600;
                break;

            case 9:
                lcd.setCursor(12, 1);
                lcd.print(" ");
                interval = 400;
                break;

            case 6:
                interval = 200;
                break;

            case 3:
                interval = 150;
                break;

            default:
                break;
        }

        currentMillis = millis();

        if (currentMillis - previousMillis >= interval) {

            previousMillis = currentMillis;

            digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
            NewTone(TONE_PIN, TONE_FREQ, TONE_DUR);
        }

        if (timeleft <= 0) {
            digitalWrite(LED_BUILTIN, LOW);
            digitalWrite(GREEN_LED_PIN, HIGH);
            delay(400);
            for (byte i = 0; i < 15; i++) {
                NewTone(TONE_PIN, TONE_FREQ, (TONE_DUR / 2));
                delay((TONE_DUR / 2) * 1.30);
            }
            lcd.home();
            lcd.print(F("TERRORISTS WIN  "));
            lcd.setCursor(0, 1);
            lcd.print(F("BOMB DETONATED  "));
            digitalWrite(GREEN_LED_PIN, LOW);
            digitalWrite(LED_BUILTIN, HIGH);
            while (1);
        }
    }

    return;
}

void setup() {

    pinMode(GREEN_LED_PIN, OUTPUT);
    pinMode(LED_BUILTIN, OUTPUT);
    digitalWrite(GREEN_LED_PIN, LOW);
    digitalWrite(LED_BUILTIN, LOW);

    lcd.begin(16, 2);
    lcd.setBacklight(HIGH);

    setTime();
    pwdLength = setPassword();

    lcd.clear();
    lcd.print(F("****************"));
}

void loop() {

    customKey = keyget();

    if (customKey != NO_KEY) {
        data[index] = customKey;
        index++;

        if (index == pwdLength) {
            index = 0;
            if (strcmp(data, password) == 0) {

                countdown(millis() / 1000);
            }
        }
    }

    cuMillis = millis();

    if (cuMillis - preMillis >= 1500) {
        preMillis = cuMillis;
        digitalWrite(GREEN_LED_PIN, !digitalRead(GREEN_LED_PIN));
    }
}

Anuncios

Arduino es una plataforma prácticamente infinita para todo tipo de aplicaciones como lo hemos visto en este humilde blog de entre millones que existen en la web. En este espacio comenzamos con aplicaciones de Hacking, luego incursionamos en la fascinante disciplina de la Robótica y últimamente avanzamos sobre el increíble universo de los Videojuegos.

Anteriormente publicamos juegos (basados en la librería VGAx desarrollada por Sandro Maffiodo) como, por ejemplo:

También construimos juegos más sencillos basados simplemente en LEDs y pulsadores como

Y no hace mucho tiempo atrás comenzamos con la construcción de videojuegos utilizando matrices de LEDs

En esta ocasión construiremos un juego muy sencillo orientado a quienes recién se inician en este maravilloso mundo de los microcontroladores, particularmente en Arduino, y desean obtener resultados satisfactorios a corto plazo ya que no creo que haya alguna emoción más intensa para un inventor, que ver alguna de sus creaciones funcionando. Utilizaremos solamente un display LCD de 16 columnas 2 filas, y un pulsador.

El juego que crearemos pertenece al género “Endless Runner“, del inglés corredor infinito cuya definición es la siguiente:

Género donde el jugador debe avanzar de manera irremediable en una misma dirección, generalmente escapando de algún enemigo o peligro, y cuyo objetivo es avanzar lo máximo posible antes de morir.

Generalmente las acciones principales son saltar y esquivar obstáculos, pero muchos juegos de este tipo incluyen también algún tipo de ataque, tanto cuerpo a cuerpo como con armas.

Desde la decáda de los 80’s con juegos como “Jump Bug” o “Moon Patrol” hasta el día de hoy se encuentran de manera casi omnipresente en nuestras vidas. Como ejemplo inmediato tenemos el mundialmente conocido navegador web del gigante Google, “Chrome“, que a partir de “Chrome Canary” incluye un divertido juego “endless runner” en el cual controlamos a un simpático T-Rex. Aunque parezca increíble existen en la web competencias de mayor puntaje obtenido en el juego. Meses atrás un gigante del entretenimiento, Netflix, ha incluido un juego “endless runner” en el cual podemos seleccionar personajes como Pablo Escobar o Marco Polo.

El diagrama esquemático de nuestro divertido y sencillo juego se observa en la siguiente imagen:

(click para ampliar)

Lista de Componentes
1 Display LCD 16×2 con controlador Hitachi HD44780 o compatible
1 Resistencia 3K3Ω 0.25W
1 Resistencia 220Ω 0.25W
1 Pulsador N.A. o Push-Button N.A.

#include <LiquidCrystal.h>

#define PIN_BUTTON 2

#define SPRITE_RUN1 1
#define SPRITE_RUN2 2
#define SPRITE_JUMP 3
#define SPRITE_JUMP_UPPER '.'         // Use the '.' character for the head
#define SPRITE_JUMP_LOWER 4
#define SPRITE_TERRAIN_EMPTY ' '      // User the ' ' character
#define SPRITE_TERRAIN_SOLID 5
#define SPRITE_TERRAIN_SOLID_RIGHT 6
#define SPRITE_TERRAIN_SOLID_LEFT 7

#define HERO_HORIZONTAL_POSITION 1    // Horizontal position of hero on screen

#define TERRAIN_WIDTH 16
#define TERRAIN_EMPTY 0
#define TERRAIN_LOWER_BLOCK 1
#define TERRAIN_UPPER_BLOCK 2

#define HERO_POSITION_OFF 0          // Hero is invisible
#define HERO_POSITION_RUN_LOWER_1 1  // Hero is running on lower row (pose 1)
#define HERO_POSITION_RUN_LOWER_2 2  //                              (pose 2)

#define HERO_POSITION_JUMP_1 3       // Starting a jump
#define HERO_POSITION_JUMP_2 4       // Half-way up
#define HERO_POSITION_JUMP_3 5       // Jump is on upper row
#define HERO_POSITION_JUMP_4 6       // Jump is on upper row
#define HERO_POSITION_JUMP_5 7       // Jump is on upper row
#define HERO_POSITION_JUMP_6 8       // Jump is on upper row
#define HERO_POSITION_JUMP_7 9       // Half-way down
#define HERO_POSITION_JUMP_8 10      // About to land

#define HERO_POSITION_RUN_UPPER_1 11 // Hero is running on upper row (pose 1)
#define HERO_POSITION_RUN_UPPER_2 12 //                              (pose 2)

LiquidCrystal lcd(7,  //RS
                  8,  //E
                  3,  //DB4
                  4,  //DB5
                  5,  //DB6
                  6   //DB7
                  );

char terrainUpper[TERRAIN_WIDTH + 1];
char terrainLower[TERRAIN_WIDTH + 1];
volatile boolean buttonPushed = false;

void initializeGraphics() {
	byte graphics[] = {
        // Run position 1
        B01100,
        B01100,
        B00000,
        B01110,
        B11100,
        B01100,
        B11010,
        B10011,
        // Run position 2
        B01100,
        B01100,
        B00000,
        B01100,
        B01100,
        B01100,
        B01100,
        B01110,
        // Jump
        B01100,
        B01100,
        B00000,
        B11110,
        B01101,
        B11111,
        B10000,
        B00000,
        // Jump lower
        B11110,
        B01101,
        B11111,
        B10000,
        B00000,
        B00000,
        B00000,
        B00000,
        // Ground
        B11111,
        B11111,
        B11111,
        B11111,
        B11111,
        B11111,
        B11111,
        B11111,
        // Ground right
        B00011,
        B00011,
        B00011,
        B00011,
        B00011,
        B00011,
        B00011,
        B00011,
        // Ground left
        B11000,
        B11000,
        B11000,
        B11000,
        B11000,
        B11000,
        B11000,
        B11000,
    };

    int i;

    // Skip using character 0, this allows lcd.print() to be used to
    // quickly draw multiple characters
    for (i = 0; i < 7; ++i) {
        lcd.createChar(i + 1, &graphics[i * 8]);
    }

    for (i = 0; i < TERRAIN_WIDTH; ++i) {
        terrainUpper[i] = SPRITE_TERRAIN_EMPTY;
        terrainLower[i] = SPRITE_TERRAIN_EMPTY;
    }
}

// Slide the terrain to the left in half-character increments
void advanceTerrain(char* terrain, byte newTerrain) {
    for (int i = 0; i < TERRAIN_WIDTH; ++i) {
        char current = terrain[i];
        char next = (i == TERRAIN_WIDTH-1) ? newTerrain : terrain[i+1];

        switch (current) {
            case SPRITE_TERRAIN_EMPTY:
                terrain[i] = (next == SPRITE_TERRAIN_SOLID) ? SPRITE_TERRAIN_SOLID_RIGHT : SPRITE_TERRAIN_EMPTY;
            break;

            case SPRITE_TERRAIN_SOLID:
                terrain[i] = (next == SPRITE_TERRAIN_EMPTY) ? SPRITE_TERRAIN_SOLID_LEFT : SPRITE_TERRAIN_SOLID;
            break;

            case SPRITE_TERRAIN_SOLID_RIGHT:
                terrain[i] = SPRITE_TERRAIN_SOLID;
            break;

            case SPRITE_TERRAIN_SOLID_LEFT:
                terrain[i] = SPRITE_TERRAIN_EMPTY;
            break;
        }
    }
}

boolean drawHero(byte position, char* terrainUpper, char* terrainLower, unsigned int score) {
    boolean collide = false;
    char upperSave = terrainUpper[HERO_HORIZONTAL_POSITION];
    char lowerSave = terrainLower[HERO_HORIZONTAL_POSITION];
    byte upper, lower;

    switch (position) {
        case HERO_POSITION_OFF:
            upper = lower = SPRITE_TERRAIN_EMPTY;
        break;

        case HERO_POSITION_RUN_LOWER_1:
            upper = SPRITE_TERRAIN_EMPTY;
            lower = SPRITE_RUN1;
        break;

        case HERO_POSITION_RUN_LOWER_2:
            upper = SPRITE_TERRAIN_EMPTY;
            lower = SPRITE_RUN2;
        break;

        case HERO_POSITION_JUMP_1:

        case HERO_POSITION_JUMP_8:
            upper = SPRITE_TERRAIN_EMPTY;
            lower = SPRITE_JUMP;
        break;

        case HERO_POSITION_JUMP_2:

        case HERO_POSITION_JUMP_7:
            upper = SPRITE_JUMP_UPPER;
            lower = SPRITE_JUMP_LOWER;
        break;

        case HERO_POSITION_JUMP_3:

        case HERO_POSITION_JUMP_4:

        case HERO_POSITION_JUMP_5:

        case HERO_POSITION_JUMP_6:
            upper = SPRITE_JUMP;
            lower = SPRITE_TERRAIN_EMPTY;
        break;

        case HERO_POSITION_RUN_UPPER_1:
            upper = SPRITE_RUN1;
            lower = SPRITE_TERRAIN_EMPTY;
        break;

        case HERO_POSITION_RUN_UPPER_2:
            upper = SPRITE_RUN2;
            lower = SPRITE_TERRAIN_EMPTY;
        break;
    }

    if (upper != ' ') {
        terrainUpper[HERO_HORIZONTAL_POSITION] = upper;
        collide = (upperSave == SPRITE_TERRAIN_EMPTY) ? false : true;
    }

    if (lower != ' ') {
        terrainLower[HERO_HORIZONTAL_POSITION] = lower;
        collide |= (lowerSave == SPRITE_TERRAIN_EMPTY) ? false : true;
    }

    byte digits = (score > 9999) ? 5 : (score > 999) ? 4 : (score > 99) ? 3 : (score > 9) ? 2 : 1;

    // Draw the scene
    terrainUpper[TERRAIN_WIDTH] = '\0';
    terrainLower[TERRAIN_WIDTH] = '\0';
    char temp = terrainUpper[16-digits];
    terrainUpper[16-digits] = '\0';
    lcd.setCursor(0,0);
    lcd.print(terrainUpper);
    terrainUpper[16-digits] = temp;
    lcd.setCursor(0,1);
    lcd.print(terrainLower);

    lcd.setCursor(16 - digits,0);
    lcd.print(score);

    terrainUpper[HERO_HORIZONTAL_POSITION] = upperSave;
    terrainLower[HERO_HORIZONTAL_POSITION] = lowerSave;

    return collide;
}

// Handle the button push as an interrupt
void buttonPush() {
    buttonPushed = true;
}

void setup() {
    pinMode(PIN_BUTTON, INPUT_PULLUP);

    // Digital pin 2 maps to interrupt 0
    attachInterrupt(0, buttonPush, FALLING);

    initializeGraphics();

    lcd.begin(16, 2);
}

void loop(){
    static byte heroPos = HERO_POSITION_RUN_LOWER_1;
    static byte newTerrainType = TERRAIN_EMPTY;
    static byte newTerrainDuration = 1;
    static boolean playing = false;
    static boolean blink = false;
    static unsigned int distance = 0;

    if (!playing) {
        drawHero((blink) ? HERO_POSITION_OFF : heroPos, terrainUpper, terrainLower, distance >> 3);

        if (blink) {
        lcd.setCursor(0,0);
        lcd.print("Press Start");
        }

        delay(250);
        blink = !blink;

        if (buttonPushed) {
            initializeGraphics();
            heroPos = HERO_POSITION_RUN_LOWER_1;
            playing = true;
            buttonPushed = false;
            distance = 0;
        }

        return;
    }

    // Shift the terrain to the left
    advanceTerrain(terrainLower, newTerrainType == TERRAIN_LOWER_BLOCK ? SPRITE_TERRAIN_SOLID : SPRITE_TERRAIN_EMPTY);
    advanceTerrain(terrainUpper, newTerrainType == TERRAIN_UPPER_BLOCK ? SPRITE_TERRAIN_SOLID : SPRITE_TERRAIN_EMPTY);

    // Make new terrain to enter on the right
    if (--newTerrainDuration == 0) {
        if (newTerrainType == TERRAIN_EMPTY) {
            newTerrainType = (random(3) == 0) ? TERRAIN_UPPER_BLOCK : TERRAIN_LOWER_BLOCK;
            newTerrainDuration = 2 + random(10);
        } else {
            newTerrainType = TERRAIN_EMPTY;
            newTerrainDuration = 10 + random(10);
        }
    }

    if (buttonPushed) {
        if (heroPos <= HERO_POSITION_RUN_LOWER_2)
            heroPos = HERO_POSITION_JUMP_1;

        buttonPushed = false;
    }

    if (drawHero(heroPos, terrainUpper, terrainLower, distance >> 3)) {
        playing = false; // The hero collided with something. Too bad.
    } else {
        if (heroPos == HERO_POSITION_RUN_LOWER_2 || heroPos == HERO_POSITION_JUMP_8) {
            heroPos = HERO_POSITION_RUN_LOWER_1;
        } else if ((heroPos >= HERO_POSITION_JUMP_3 && heroPos <= HERO_POSITION_JUMP_5) && terrainLower[HERO_HORIZONTAL_POSITION] != SPRITE_TERRAIN_EMPTY) {
            heroPos = HERO_POSITION_RUN_UPPER_1;
        } else if (heroPos >= HERO_POSITION_RUN_UPPER_1 && terrainLower[HERO_HORIZONTAL_POSITION] == SPRITE_TERRAIN_EMPTY) {
            heroPos = HERO_POSITION_JUMP_5;
        } else if (heroPos == HERO_POSITION_RUN_UPPER_2) {
            heroPos = HERO_POSITION_RUN_UPPER_1;
        } else {
            ++heroPos;
        }

        ++distance;
    }

    delay(100);
}