You may have seen people using an Arduino to automate playing the Chrome Dino Game in their browser, I thought we could take it a bit further and see if we could get one Arduino to play the Chrome Dino Game on another Arduino.

I’ve previously set up an Arduino to run a simple version of the game, which produces a continuous stream of randomly spaced cactuses at an increasing speed, and closer together. The game is really easy to get running and only requires an Arduino Uno and an LCD keypad shield. A single button on the keypad shield is then used to make the dinosaur jump over each cactus until the player messes up and hits one. The score is kept based on how long you’re able to avoid running into a cactus.

Once I had the game running on one Arduino, I set up a second one, which uses an LDR to sense each cactus and then moves a servo to press the button on the other Arduino to make the dinosaur jump over each cactus.

Here’s a video of the build and the one Arduino playing the Chrome Dino Game on the other:

We’ll first have a look at running the dino game on an Arduino and then set up a second to play the game on the first. The code would be the same to set the second Arduino up to play on your computer’s browser as well, you’d just need to stick the LDR to the display.

What You Need For This Project

For The Dino Game

For The Dino Game Player

How To Load The Chrome Dino Game

Let me start out by saying – I didn’t code the dino game. It’s a version I found on Hackster.io which was the least complicated layout and ran the smoothest. One downside is that it’s written in AVR C code, so it looks a little different to the generic Arduino language but you should still be able to figure a lot of it out, I’ve also expanded on the description of the game and explained how you can change a few things around in another post – How to Play the Chrome Dino Game onto an Arduino.

Here’s a general overview of the game:

  • You push a button on the keypad shield to make the dinosaur jump over each cactus.
  • The game speed up the longer you play it.
  • Cactuses are initially separated by a minimum of 5 spaces and this goes down to 4 as the game progresses.
  • The current score is displayed throughout the game and is joined by the high score at the end of the game.
  • Cheating by holding down the button or continuously pressing the button is prevented.

I have made a couple of changes to the original version of the code for the timing, cactus spacing and the text layouts to better accommodate the sensor.

Here is the code:

//From Hackster.io
//By BRZI

#include <avr/io.h>
#define  F_CPU 16000000UL //Our CPU speed (16MHz)
#include <util/delay.h> //Libraries for delay and interrupt utilities
#include <avr/interrupt.h>
#define command 0 //explained in dispSend() function
#define write 1

uint8_t upperBuff[16] , downerBuff[16], overMsgUpper[] = "Score:  ", overMsgDowner[] = "Best:   ", scoremsg[] = "Score:" , din[] = {0x0E, 0x17, 0x1E, 0x1F, 0x18, 0x1F, 0x1A, 0x12}, cact[] = {0x04, 0x05, 0x15, 0x15, 0x16, 0x0C, 0x04, 0x04};
        //Buffers for line one and two. Message to display after lost game.                     //Score text during game. //Dinosaur and cactus bitmaps
uint8_t canup = 1, longhold = 0, distance = 6, speed = 200, isup = 0, dontprint = 0; //All of these are explained further
uint16_t aVal = 0, score = 1, bestscore = 0;
int i;

void dispInit();
void dispWrite(uint8_t bits);
void dispSend(uint8_t bits, uint8_t act);
void dispSetLine(uint8_t line);
void dispClear();
void dispHome();
void dispPrintChar(uint8_t chr[], uint8_t size);
uint16_t aRead();

int main(void)
{
  for(i = 0; i < 17; i++) downerBuff[i] = ' '; //Initialize upper and downward buffer
  for(i = 0; i < 17; i++) upperBuff[i] = ' ';
  
  dispInit(); //Initialize the display
  
  TCCR1B |= (1 << WGM12) | (1 << CS11); //Set Timer1 to compare to OCR1A and prescaler of 8
  OCR1AH = (500 >> 8); //This equals to 2000Hz or 500us timing, look for TIMER1_COMPA_vect down below
  OCR1AL = 500;
  TIMSK1 |= (1 << OCIE1A); //Enable Timer1 CompA ISR
  sei(); //Enable global interrupt

  
  ADMUX = (1 << REFS0); //Set AREF to VCC
  ADCSRA = (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0) | (1 << ADEN); //set ADC prescaler to 128 and enable ADC (defaulted to free running mode)
  
  while (1) {   
    
    ADMUX |= (1 << MUX2) | (1 << MUX0); //Set pin from ADMUX to ADC5 (floating)
    srand(aRead()); //Use it as a random seed
    ADMUX &= ~(1 << MUX2) & ~(1 << MUX0); //Revert back to ADC0 to read the button value

    if(aRead() > 900) longhold = 0; //Reads if Up button has been released to prevent cheating. The value is so low because if you hold your fingers beneath one of the buttons the voltage would drop, this prevents the dinosaur from locking up

    for(i = 0; i < 16; i++) downerBuff[i] = downerBuff[i + 1]; //Shifts everything in downward buffer by one place to the left
     if((rand() % 100) > (rand() % 100) && !dontprint){ //This portion decides if it should put a cactus or a blank spot, dontprint is used to prevent cactus grouping
      downerBuff[15] = 0x01; //0x01 represents the cactus (we added cactus and dinosaur to CGRAM when we initialized the display)
      dontprint = 1; //This part acts both as a boolean and a counter to ensure cactus separation
     }
     else downerBuff[15] = ' ';
     char lastchar = downerBuff[3]; //We remember the whats initially added to the downward buffer before replacing it with the dinosaur
     if(!isup){ //If din should be placed down
      downerBuff[3] = 0x00; //Place it down
      dispSetLine(2);
      dispPrintChar(downerBuff, sizeof(downerBuff)); //Draw it
      downerBuff[3] = lastchar; //Place back previous thing to the buffer
      canup = 1;  //This flag is used to disable dinosaur from getting up before it was drawn down, in this case he can go up
    } else { //If din should be placed up
      upperBuff[3] = 0x00; //Place it up in upper buff
      dispSetLine(1);
      dispPrintChar(upperBuff, sizeof(upperBuff));
      dispSetLine(2);
      dispPrintChar(downerBuff, sizeof(downerBuff)); //Draw it
      canup = 0; //In this case he wont go up until rendered on line 2
    }

    if(dontprint) dontprint++;
    if(dontprint > distance) dontprint = 0; //This is the part that ensures cactus separation, it will keep the cactus 3-5 spaces apart minimally (depends on the game progress)
    
    if(isup) isup++; //This part makes sure din is on upper side for 3 loops after he was initially drawn there
    if(isup > 4){
     upperBuff[3] = ' ';
     dispSetLine(1);
     dispPrintChar(upperBuff, sizeof(upperBuff));
     isup = 0;
    }
    for(i = 0; i < sizeof(scoremsg); i++) upperBuff[i + 5] = scoremsg[i]; //This part prints the current score during the game
    uint8_t cnt = 11;
    for(i = 10000; i > 0; i /= 10){
      upperBuff[cnt] = ((score / i) % 10) + '0';
      cnt++;
      dispSetLine(1);
      dispPrintChar(upperBuff, sizeof(upperBuff));
    }

    score++; //Increment the score once on loop
    if(score > bestscore) bestscore = score; //Remember best score
    
    if(lastchar == 0x01 && !isup){ //Check if the dinosaur is downward and hit a cactus
      dispClear(); //Clear the display and buffers
      for(i = 0; i < 17; i++) downerBuff[i] = ' ';
      for(i = 0; i < 17; i++) upperBuff[i] = ' ';
      uint8_t cnt;
      
      dispSetLine(1);
      for(i = 0; i < sizeof(overMsgUpper); i++) upperBuff[i] = overMsgUpper[i]; //Display worst and best score
      cnt = sizeof(overMsgUpper) - 1;
      for(i = 10000; i > 0; i /= 10){
        upperBuff[cnt] = ((score / i) % 10) + '0';
        cnt++;
      }
      dispPrintChar(upperBuff, sizeof(upperBuff));
      
      dispSetLine(2);
      for(i = 0; i < sizeof(overMsgDowner); i++) downerBuff[i] = overMsgDowner[i];
      cnt = sizeof(overMsgDowner) - 1;
      for(i = 10000; i > 0; i /= 10){
        downerBuff[cnt] = ((bestscore / i) % 10) + '0';
        cnt++;
      }
      dispPrintChar(downerBuff, sizeof(downerBuff));
      
      while(1){ //Wait for select button to be pressed
        aVal = aRead();
        if(aVal > 635 && aVal < 645){ //After that clear all the variables
          for(i = 0; i < 17; i++) downerBuff[i] = ' ';
          dispSetLine(1);
          dispPrintChar(downerBuff, sizeof(downerBuff));
          for(i = 0; i < 17; i++) upperBuff[i] = ' ';
          dispSetLine(2);
          dispPrintChar(upperBuff, sizeof(upperBuff));
          dontprint = 0;
          isup = 0;
          score = 1;
          speed = 200;
          longhold = 0;
          distance = 6;
          canup = 1;
          break;
             }
      }
      
    }
    if(score % 5 == 0) speed -=1; //If score is divisible by 5 make game faster by -2ms
    if(speed < 120) speed = 120; //Minimal time in ms (+ ~2ms) that the loop will be halted for (limited by display refreshing, in my testing 11.8Hz was readable enough to be playable)
    if(score % 175 == 0) distance--; //Every time you score a number divisible by 175 minimal cactus distance gets smaller
    if(distance < 4) distance = 4;
    for(i = 0; i < speed; i++) _delay_ms(1); //This is the only way as the compiler expects a const number here
  }
}

void dispInit(){
  _delay_ms(50); //Just in case
  DDRD = 0b11110000; //Set these pins to output. PD4 - PD7 correspond to D4 - D7 on display, we need to configure it to run in 4 bit mode
  DDRB = 0b00000011; //PB0 is tied to RS and PB1 to EN
  dispWrite(0x30);//*This part here is explained in Hitachi HD44780 datasheet on how to initialize the display in 4bit mode
  _delay_us(4500);//*Essentially you send the reset signal 3 times, and then set it to 4 bit mode
  dispWrite(0x30);//*
  _delay_us(4500);//*
  dispWrite(0x30);//*
  _delay_us(4500);//*
  dispWrite(0x28);//*
  dispSend(0x28, command); //Send 4bit mode function set
  dispSend(0x08, command); //Turn the display off
  dispSend(0x01, command); //Clear its RAM (if MCU resets that doesn't mean the display was reset, so we clear everything)
    _delay_ms(50);
  dispSend(0x0C, command); //Turn the display on
  _delay_ms(5);
  dispSend(0x40, command); //Tell the display we want to enter a custom character to its CGRAM (on address 0x00)
  for(i=0; i<8; i++) dispSend(din[i], write);
  dispSend(0x80, command); //Transaction end
  dispSend(0x48, command); //Same thing, but for 0x01
  for(i=0; i<8; i++) dispSend(cact[i], write);
  dispSend(0x80, command);
}

void dispPrintChar(uint8_t chr[], uint8_t size){
  for(uint8_t i = 0; i < size; i++) dispSend(chr[i], write); //Self explanatory 
}

void dispSetLine(uint8_t line){
  if(line == 2) dispSend(0xC0, command); //Sets the line where 0xC0 is line 2 and 0x80 is line 1
  else dispSend(0x80, command);
}

void dispClear(){
  dispSend(0x01, command); //Self explanatory 
  _delay_ms(2); //This command takes longer for the IC to process, this delay is necessary
}

void dispHome(){ //This function isn't used in this application but its there for expandability, it places the cursor on the line 1 column 1
  dispSend(0x02, command); //Self explanatory 
  _delay_ms(2);
}

void dispSend(uint8_t bits, uint8_t act){
  if(act) PORTB |= (1 << DDB0); //Set PB0 if we are writing a character, else pull it low
  else PORTB &= ~(1<<DDB0);
  dispWrite(bits); //Send the bit then shift them 4 bit to the left to work in displays 4bit mode
  dispWrite(bits << 4);
  _delay_us(80);
}

void dispWrite(uint8_t bits){
  PORTD = bits; //This is a dirty way to write it but it's perfect for this application as it's not bulky and PORTD isn't used for anything else anyway
  PORTB |= (1<<DDB1); //Pulse the PB1 to signal the IC to read the data
  _delay_us(1);
  PORTB &= ~(1<<DDB1);
  _delay_us(1);
}

uint16_t aRead(){
  ADCSRA |= (1 << ADSC); //This signal the avr to read the ADC value
  while  (ADCSRA & (1 << ADSC)); //Wait until it's finished
  return ADCL | (ADCH << 8); //Send it back stitched together
}

ISR (TIMER1_COMPA_vect){ //Timer ISR we set up earlier
  if(!longhold){ //Return if the Up button was still held
    aVal = aRead(); //Read from ADC0
    if(aVal > 450 && aVal < 600 && canup){ //Check if Up is pressed and that din was rendered down
     isup = 1;
     longhold++;
     }
    }
}

Download The Code – DinoGame

The game is quite easy to play on the LCD keypad shield, although the buttons are not the best for quick presses, and the LCD is quite slow, so it starts suffering from ghosting and brightness issues once the cactuses start moving quickly.

Playing The Chrome Dino Game On An Arduino

Setting Up An Arduino To Play The Chrome Dino Game

Now that we’ve got the game running on our first Arduino, let’s try and get the second one playing the Chrome Dino Game on the first.

The components and wiring are quite simple, they involve basic starter circuits for the LDR, LED and servo. I initially included the LED to light up when each cactus was detected but the light started interfering with the LDR, so I turned it off. You can leave this LED out if you want to or cover it up slightly with tape so that it doesn’t interfere with the surrounding components.

Schematic - Arduino Chrome Dino Game Player

I connected the components together using a few strips of ribbon cable and some header pins to make it easier to plug into the Arduino and the servo.

Soldering Components

Ribbon Cable Connector

You’ll need to glue the servo onto a box or stand just above the left button on the keypad shield so that the servo arm pushes the button down when it rotates. You’ll then need to glue the LDR onto the edge of the LCD screen as close to the screen as possible. It needs to be positioned over the 7th character from the left of the display in the second row.

Arduino Playing The Chrome Dino Game On Another Arduino - Side

Make sure that the LED is covered, turned off or pointed away from the display so that the light from the LED doesn’t interfere with the LDR sensing the cactuses. The first few runs I had were interrupted by the LED causing false cactus readings by the LDR, so it turned it off.

Servo Pressing Jump Button

Now let’s have a look at the player code.

//Michael Klements
//The DIY Life
//8 May 2020

#include <Servo.h>

Servo player;         //Create a servo object to control the button servo
int cactus = 0;       //Variable to read in LDR value
int cactusVal = 770;  //LDR Sensor value when detecting a cactus

void setup()
{
  //Serial.begin(9600);
  player.attach(6);     //Set the player servo pin to pin 6
  player.write(75);     //Set the servo to an initial position just above the button
}

void loop()
{
  cactus = analogRead(A0);          //Read the output from the LDR to detect a cactus
  //Serial.println(cactus);         //Used for calibration
  if (cactus > cactusVal)           //If a cactus is detected
  {
    player.write(60);               //Move the servo to push the jump button
    delay(300);                     //Wait 300 milliseconds
    player.write(75);               //Move the servo back to the initial position
    delay(100);
  }
}

Download The Code – DinoGamePlayer

We start by including the servo library to control the servo.

We then create a servo object called player to control the servo and then create two variables, one to store the value read from the LDR and a second to store the light level set point when a cactus passes in front of the LDR.

In the setup function we set the servo pin number and set the servo position slightly above the jump button. You’ll also need the Serial monitor for calibration, this is detailed further on.

In the loop function, we read in the LDR sensor level, then compare it to the cactus set point. If the measured level is greater than the cactus set point, indicating a cactus is passing the sensor, then we move the servo downwards to push the button, wait 300 milliseconds for the servo to move and for the game to register the input and then move the servo back up for the next push. The second delay is to avoid repeated servo movements, this could be reduced to 50ms.

I’ve also included a serial monitor printout of the sensor value which is used initially to set the cactus value set point. You’ll need to run the game and display the sensor values and then see what value is measured when a cactus runs past the sensor and update this value in the code accordingly. This step is detailed in the video at the beginning of the project.

One Arduino Playing The Chrome Dino Game On Another

Upload the code and calibrate the sensor and you’re ready to try it out. Startup the player Arduino first and then start the game on the other. Press the reset button on the LCD shield to start a new game.

Arduino Playing The Chrome Dino Game On Another Arduino

It initially looks like the sensor is making the dinosaur jump a little too early, but it doesn’t hit the cactuses and you need to it respond quickly later on in the game. It starts to look better as the game progresses.

LDR Detecting Cactuses

You may need to make a few adjustments to the servo travel limits and to the cactus detection set points in your code to get it to work correctly.

Arduino Playing The Chrome Dino Game On Another Arduino - Side

Also, position the LDR as close to the LCD as possible. These LDRs are really sensitive, I noticed a significant fluctuation in the measured values with me moving around the room or at different times of the day, so It would be a good addition to add a pot to adjust the set point at any time. This way you could get it to work in any light conditions and use it on the Arduino game or your computer without having to change the code.

Let me know in the comments if you’ve tried playing the Chrome Dino Game on an Arduino or tried getting an Arduino to play the Chrome Dino Game.

Share This Guide

An Arduino Playing The Chrome Dino Game On Another Arduino Social

Leave a Reply

Your email address will not be published.