Master the Dino Game on Google Chrome (by cheating)

I read a great article recently on Hacking the Dino Game from Google Chrome, where we're shown that the JavaScript state for the Dino game is not only accessible from the console, but everything is un-minified, meaning we can play around with it.

If you haven't seen the dino game, it's built into Chrome and appears when your internet is dead. It's best enjoyed when your at work and the internet dies, leaving you to compete with your colleagues to get the high score before it comes back on. After all, we can't be expected to code without StackOverflow, right?

I wanted to extend on the article above by giving the illusion of the user playing the game expertly, whilst the little dinosaur is jumping over and ducking under objects on his own. Then everyone can gather around my PC and marvel as I go over the 99999 limit.

To get started, let's first get to the game without killing our internet, by going here:

chrome://dino

(Sorry, you'll have to copy and paste it in! Chrome blocks the link).

Dino game

Let's have a look at the code. Hit F12 to bring up the DevTools window, and click on the Console tab.

Once we're in the console window, let's see if we can get the little guy to jump/duck by itself. We'll keep it stupid for the time-being, and let it intelligently jump/duck obstacles later.

Jump and Duck

Right, we should try and make the dino jump every second by typing the following into the console:

setInterval(() => { 
    Runner.instance_.tRex.startJump(Runner.instance_.currentSpeed) // Jump, based on the current speed 
}, 1000) // Every 1000 ms, or 1 second

Once you hit play, you'll see that it jumps every second, and very quickly fails...

Dino jump

Now let's see if we can make it duck every second. Let's refresh the page to stop it from jumping, and then add this code:

setInterval(() => { 
    Runner.instance_.tRex.setDuck(true); // Dino ducks

	setTimeout(() => {  
        Runner.instance_.tRex.setDuck(false) // Dino stops ducking...
    }, (3000 / Runner.instance_.currentSpeed)); // ...after waiting a little bit for the obstacle to pass
}, 1000) // Every 1000 ms, or 1 second

Dino duck

Clearly we're going to need to add some form of obstacle detection, because both of those attempts we're awful. Fortunately, the game as done the job for us, since it needs to detect when you've hit an obstacle and bring up the "GAME OVER" screen.

Collision Detection

Let's take a look at the code by typing the following into the console:

checkForCollision

We can then inspect this code by right-clicking on the output, and clicking "Show function definition":

Dino game console

There's a couple things we want to do with this method:

  • At the top there is a tRexBox, that acts as the collision box. When this overlaps an obstacle collision box, the game ends. We should move this in front of the dino so that we can use the collision to make it jump/duck instead!
  • At the bottom, there is a check to see if the dino has crashed. Let's keep the check, but replace the contents so that it jumps/ducks instead of returning true.

Let's refresh again, and replace the logic of this method by overriding it with our (very similar) version. I've marked the changes with comments stating "OUR CHANGE":

function checkForCollision(obstacle, tRex, opt_canvasCtx) {
  const obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
  
  // Adjustments are made to the bounding box as there is a 1 pixel white
  // border around the t-rex and obstacles.
  const tRexBox = new CollisionBox(
      tRex.xPos + Runner.instance_.currentSpeed * 3, // OUR CHANGE: Move the box forwards, relative to the current speed
      tRex.yPos + 1,
      tRex.config.WIDTH - 2,
      tRex.config.HEIGHT - 2);

  const obstacleBox = new CollisionBox(
      obstacle.xPos + 1,
      obstacle.yPos + 1,
      obstacle.typeConfig.width * obstacle.size - 2,
      obstacle.typeConfig.height - 2);

  // Debug outer box
  if (opt_canvasCtx) {
    drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
  }

  // Simple outer bounds check.
  if (boxCompare(tRexBox, obstacleBox)) {
    const collisionBoxes = obstacle.collisionBoxes;
    const tRexCollisionBoxes = tRex.ducking ?
        Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;

    // Detailed axis aligned box check.
    for (let t = 0; t < tRexCollisionBoxes.length; t++) {
      for (let i = 0; i < collisionBoxes.length; i++) {
        // Adjust the box to actual positions.
        const adjTrexBox =
            createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
        const adjObstacleBox =
            createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
        const crashed = boxCompare(adjTrexBox, adjObstacleBox);

        // Draw boxes for debug.
        if (opt_canvasCtx) {
          drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
        }

        if (crashed) {
            // OUR OTHER CHANGE: Get dino to duck and jump
			if(obstacle.typeConfig.type == "PTERODACTYL" // We only jump for Pterodactyl's
                && obstacle.yPos < 100) { // And only if they're in the air!
				tRex.setDuck(true); // Dino starts ducking
				setTimeout(() => { 
                    tRex.setDuck(false); 
                }, (3000 / Runner.instance_.currentSpeed)); // Dino stops ducking
			} else {
			  tRex.startJump(Runner.instance_.currentSpeed); // Dino jumps 
			  setTimeout(() => { 
                  tRex.setSpeedDrop() 
              }, (4000 / Runner.instance_.currentSpeed)); // And slams to the floor after passing the obstacle!
			}
        }
      }
    }
  }
}

I added a slight change at the end, where the dino will "SpeedDrop". It's the equivalent of pressing down when the dino is in the air, making it fall back to the ground quicker. You may want to tweak the numbers a little, but these seem to do the job.

Let's take a look at the results:

Dino duck bug

Ah, that is not ideal. Every time dino speed drops to the ground, it goes into a "Duck" state. We'll fix that in the next step.

Update changes

You can find the duck on speed drop logic in the update method:

Runner.instance_.tRex.update

Right at the bottom of that method, we can see this:

// Speed drop becomes duck if the down key is still being pressed.
if (this.speedDrop && this.yPos === this.groundYPos) {
    this.speedDrop = false;
    this.setDuck(true);
}

This is useful as a player, since you wouldn't want to slam to the floor and then press again to crouch quickly. Since the computer is playing itself however, we can just remove this by replacing the method:

Runner.instance_.tRex.update = function(deltaTime, opt_status) {
    this.timer += deltaTime;

    // Update the status.
    if (opt_status) {
      this.status = opt_status;
      this.currentFrame = 0;
      this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
      this.currentAnimFrames = Trex.animFrames[opt_status].frames;

      if (opt_status === Trex.status.WAITING) {
        this.animStartTime = getTimeStamp();
        this.setBlinkDelay();
      }
    }

    // Game intro animation, T-rex moves in from the left.
    if (this.playingIntro && this.xPos < this.config.START_X_POS) {
      this.xPos += Math.round((this.config.START_X_POS /
          this.config.INTRO_DURATION) * deltaTime);
      this.xInitialPos = this.xPos;
    }

    if (this.status === Trex.status.WAITING) {
      this.blink(getTimeStamp());
    } else {
      this.draw(this.currentAnimFrames[this.currentFrame], 0);
    }

    // Update the frame position.
    if (this.timer >= this.msPerFrame) {
      this.currentFrame = this.currentFrame ==
          this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
      this.timer = 0;
    }

    // ...and goodbye duck logic
  }

Now let's see what we have:

Dino done

Not bad at all!

Disable user input

Finally, if you want to make it look like you're playing, you'll probably want to be hitting the up and down keys whilst not actually controlling the dino. You can do this by replacing the keycodes:

Runner.keycodes = {
  JUMP: {'32': 1}, // Spacebar
  RESTART: {'13': 1}  // Enter
};

Now you just have to hit spacebar to start, and (realistically) spam up and down keys until your heart is content!

Future improvements

There are still a couple modifications you can make:

  • Dual collision detection logic, one for jumping and the other for "Game Over", since the dino will just keep going forever at the moment.
  • Make jumping/ducking more human-like, by adding some variability in the timing.

TL;DR

If you'd prefer to just copy and paste all of the code in at once and hit space, here you go:

function checkForCollision(obstacle, tRex, opt_canvasCtx) {
  const obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
  
  // Adjustments are made to the bounding box as there is a 1 pixel white
  // border around the t-rex and obstacles.
  const tRexBox = new CollisionBox(
      tRex.xPos + Runner.instance_.currentSpeed * 3, // OUR CHANGE: Move the box forwards
      tRex.yPos + 1,
      tRex.config.WIDTH - 2,
      tRex.config.HEIGHT - 2);

  const obstacleBox = new CollisionBox(
      obstacle.xPos + 1,
      obstacle.yPos + 1,
      obstacle.typeConfig.width * obstacle.size - 2,
      obstacle.typeConfig.height - 2);

  // Debug outer box
  if (opt_canvasCtx) {
    drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
  }

  // Simple outer bounds check.
  if (boxCompare(tRexBox, obstacleBox)) {
    const collisionBoxes = obstacle.collisionBoxes;
    const tRexCollisionBoxes = tRex.ducking ?
        Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;

    // Detailed axis aligned box check.
    for (let t = 0; t < tRexCollisionBoxes.length; t++) {
      for (let i = 0; i < collisionBoxes.length; i++) {
        // Adjust the box to actual positions.
        const adjTrexBox =
            createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
        const adjObstacleBox =
            createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
        const crashed = boxCompare(adjTrexBox, adjObstacleBox);

        // Draw boxes for debug.
        if (opt_canvasCtx) {
          drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
        }

        if (crashed) {
            // OUR OTHER CHANGE: Get dino to duck and jump
			if(obstacle.typeConfig.type == "PTERODACTYL" // We only jump for Pterodactyl's
                && obstacle.yPos < 100) { // And only if they're in the air!
				tRex.setDuck(true); // Dino starts ducking
				setTimeout(() => { 
                    tRex.setDuck(false); 
                }, (3000 / Runner.instance_.currentSpeed)); // Dino stops ducking
			} else {
			  tRex.startJump(Runner.instance_.currentSpeed); // Dino jumps 
			  setTimeout(() => { 
                  tRex.setSpeedDrop() 
              }, (4000 / Runner.instance_.currentSpeed)); // And slams to the floor after passing the obstacle!
			}
        }
      }
    }
  }
}

Runner.instance_.tRex.update = function(deltaTime, opt_status) {
	this.timer += deltaTime;

	// Update the status.
	if (opt_status) {
	  this.status = opt_status;
	  this.currentFrame = 0;
	  this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
	  this.currentAnimFrames = Trex.animFrames[opt_status].frames;

	  if (opt_status === Trex.status.WAITING) {
		this.animStartTime = getTimeStamp();
		this.setBlinkDelay();
	  }
	}

	// Game intro animation, T-rex moves in from the left.
	if (this.playingIntro && this.xPos < this.config.START_X_POS) {
	  this.xPos += Math.round((this.config.START_X_POS /
		  this.config.INTRO_DURATION) * deltaTime);
	  this.xInitialPos = this.xPos;
	}

	if (this.status === Trex.status.WAITING) {
	  this.blink(getTimeStamp());
	} else {
	  this.draw(this.currentAnimFrames[this.currentFrame], 0);
	}

	// Update the frame position.
	if (this.timer >= this.msPerFrame) {
	  this.currentFrame = this.currentFrame ==
		  this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
	  this.timer = 0;
	}
}

Runner.keycodes = {
  JUMP: {'32': 1}, // Spacebar
  RESTART: {'13': 1}  // Enter
};