Making the T-Rex Smarter
Google Chrome is an awesome web browser that I absolutely love to use. I was working on a web project a few days ago when my internet connection went down and I got this screen on my browser.
You probably already know that there is a hidden game on this screen which can be activated by pressing the Space, Up or Down keys on your keyboard.
The game is really simple but fun to play. Basically, it is about a T-Rex dinosaur that keeps running endlessly and should avoid hitting obstacles by jumping or ducking.
I haven’t played this game for a while, so instead of fixing my internet connection I decided to give it a shot and try to break my best record which was around 5500. I got to nearly 4800 points when I was distracted by my ringing phone and then I hit a large cactus tree and lost.
I got frustrated but then I decided to re-try and break my own record … not by playing again, but by writing JavaScript code that plays the game. In this article, I will explain how I did it.
The Plan
Basically, we need to figure out how to:
1. Simulate keyboard events, specifically Up and Down key press events.
2. Detect obstacles coming ahead.
3. Jump/Duck when the dinosaur is about to hit an obstacle.
4. Get the dinosaur to score over 6000 points.
Simulating Keyboard Events
This part is easy, the game listens to keyboard events on the document object so we need to dispatch an event that simulates an Arrow Up key press to make the dinosaur jump and an event that simulates an Arrow Down key press to make the dinosaur duck.
function jump() {
var e = new Event('keydown');
e.keyCode = 38; // keyCode for the Arrow Up key is 38
document.dispatchEvent(e);
}function duck() {
var e = new Event('keydown');
e.keyCode = 40; // keyCode for the Arrow Down key is 40
document.dispatchEvent(e);
}
Both functions are identical except for the keyCode part so we can refactor the code as follows:
const dispatchKeyEvent = function(keyCode) {
var e = new Event('keydown');
e.keyCode = keyCode;
document.dispatchEvent(e);
}const jump = function() {
dispatchKeyEvent(38); // keyCode for the Arrow Up key is 38
};const duck = function() {
dispatchKeyEvent(40); // keyCode for the Arrow Down key is 40
};
Now that we are able to simulate keyboard events, we need to detect upcoming obstacles.
Detecting obstacles
This part is also easy as we can simply access the obstacles array from the game singleton object:
const obstacles = Runner.instance_.horizon.obstacles;
The first item in this array is the upcoming obstacle. The following code logs its position to the console:
(function tick() {
const obstacles = Runner.instance_.horizon.obstacles;if (obstacles.length) {
console.log(obstacles[0].xPos);
}
requestAnimationFrame(tick);
}());
The window.requestAnimationFrame()
method tells the browser that you wish to perform an animation and requests that the browser call a specified function to update an animation before the next repaint. The method takes as an argument a callback to be invoked before the repaint.
If you want to learn more about requestAnimationFrame
, click here.
We will use requestAnimationFrame
to execute our code on each browser repaint but we do not want our code to be executed if the game is paused or if the game is over, so we will check for that:
const game = Runner.instance_;(function tick() {
/* do not do anything if the game is not running */
if (game.crashed || game.paused) {
return requestAnimationFrame(tick);
}const obstacles = game.horizon.obstacles;if (obstacles.length) {
console.log(obstacles[0].xPos);
}
requestAnimationFrame(tick);
}());
Jump, Duck … Duck, Jump!
Let’s modify the code to make a jump if the distance between the dinosaur and the next obstacle is less than 25 pixels.
Note: The number 25 is just an arbitrary value, we will use a more accurate number later, but for now let’s just use 25.
The horizontal origin for each object is on the left side of the object so we will need to subtract the dinosaur width to get the correct value.
(function tick() {
/* do not do anything if the game is not running */
if (game.crashed || game.paused) {
return requestAnimationFrame(tick);
}const obstacles = game.horizon.obstacles;if (obstacles.length) {
const tRex = game.tRex;
const tRexWidth = tRex.config.WIDTH;
const obstacle = obstacles[0];if (obstacle.xPos - tRex.xPos - tRexWidth <= 25) {
if (!tRex.jumping) {
jump();
}
}
}
requestAnimationFrame(tick);
}());
Now, the T-Rex only jumps if there is an obstacle ahead but it doesn’t know what to do with pterodactyls.
The problem with pterodactyls is that they are not all flying on the same level, some of them fly very low so the dinosaur needs to jump, others fly mid-level so the dinosaur needs to duck (while it’s ok to jump but it’s safer to duck) and others fly too high that the dinosaur can go right below them.
Let’s fix that by detecting the vertical position of the obstacle and based on that decide what action should the dinosaur take: jump, duck or do nothing.
By exploring in the console, we can find out that the vertical position of the mid-level flying pterodactyl is 75 pixels from the top of the canvas element.
If the yPos of the obstacle is 75 pixels, then it’s a mid-level flying pterodactyl and the dinosaur should duck. For obstacles on the ground, the yPos would be more than 75 pixels and then the dinosaur should jump. Otherwise, the dinosaur should ignore the obstacle and keep running.
(function tick() {
/* do not do anything if the game is not running */
if (game.crashed || game.paused) {
return requestAnimationFrame(tick);
}const obstacles = game.horizon.obstacles;if (obstacles.length) {
const tRex = game.tRex;
const tRexWidth = tRex.config.WIDTH;
const obstacle = obstacles[0];if (obstacle.xPos - tRex.xPos - tRexWidth <= 25) {
if (obstacle.yPos > 75) {
if (!tRex.jumping) {
jump();
}
} else
if (obstacle.yPos === 75) {
if (!tRex.ducking) {
duck();
}
}
}
}
requestAnimationFrame(tick);
}());
Sweet! Now the T-Rex knows when to jump, when to duck and when to do nothing, but there is a problem … once it ducks, it does not stand straight again.
With our current implementation, the jump and duck functions trigger a keydown event but they never trigger a following keyup event. This is like pressing and holding the Arrow Up or Arrow Down keys. We need to rectify this by triggering a delayed keyup event after each keydown event.
Let’s modify our functions to trigger a keyup event after 300 milliseconds from dispatching the keydown event.
const dispatchKeyEvent = function(eventName, keyCode) {
var e = new Event(eventName);
e.keyCode = keyCode;
document.dispatchEvent(e);
}const simulateKeyPress = function(keyCode) {
dispatchKeyEvent('keydown', keyCode);setTimeout(function() {
dispatchKeyEvent('keyup', keyCode);
}, 300);
}const jump = function() {
simulateKeyPress(38); // keyCode for the Arrow Up key is 38
};const duck = function() {
simulateKeyPress(40); // keyCode for the Arrow Down key is 40
};
The dinosaur is now able to avoid hitting all obstacles properly, however, things get really messy when the speed increases. The T-Rex can barely score 1000 points without losing and we need to score more than 6000 points.
It is now time we resort to the laws of physics.
The Perfect Score
For each obstacle, we will use the Projectile Motion Equations to find out the velocity (Vy) at which the dinosaur needs to jump in order to avoid hitting the obstacle. According to the projectile motion equations:
So let’s translate that to JavaScript:
(function tick() {
/* do not do anything if the game is not running */
if (game.crashed || game.paused) {
return requestAnimationFrame(tick);
}const obstacles = game.horizon.obstacles;if (obstacles.length) {
const tRex = game.tRex;
const tRexWidth = tRex.config.WIDTH;
const obstacle = obstacles[0];
const obstacleHeight = obstacles[0].typeConfig.height;
const gravity = game.config.GRAVITY;
const jumpVelocityY = Math.sqrt(2 * gravity * obstacleHeight);if (obstacle.xPos - tRex.xPos - tRexWidth <= 25) {
if (obstacle.yPos > 75) {
if (!tRex.jumping) {
jump();
tRex.jumpVelocity = -jumpVelocityY;
}
} else
if (obstacle.yPos === 75) {
if (!tRex.ducking) {
duck();
}
}
}
}
requestAnimationFrame(tick);
}());
This way the dinosaur will make a jump that is just about enough to get past the obstacle.
Jumping to the exact height of the obstacle is not safe enough though, we need to add more pixels to be on the safe side.
const safety = 15;
const obstacleHeight = obstacles[0].typeConfig.height;
const jumpHeight = obstacleHeight + safety;
The safety margin will be useless once the speed goes up, so let’s make it proportional to the game speed:
const safety = 2 * game.currentSpeed;
Our code is still not complete yet, we did figure out the required jump velocity but we don’t know exactly when to jump … remember those arbitrary 25 pixels?
Back to the projectile motion equations, the distance travelled by the dinosaur while flying in the air can be calculated as follows:
We will assume that the dinosaur will jump at an angle of 60 degrees, so in JavaScript, the equations above would be:
const jumpAngle = 60 * Math.PI / 180;
const jumpVelocity = jumpVelocityY / Math.sin(jumpAngle);
const jumpDistance = Math.pow(jumpVelocity, 2) * Math.sin(2 * jumpAngle) / gravity;
We will tweak how we calculate jumpDistance
to include our safety
margin, so the final and complete code would look like this:
const dispatchKeyEvent = function(eventName, keyCode) {
var e = new Event(eventName);
e.keyCode = keyCode;
document.dispatchEvent(e);
}const simulateKeyPress = function(keyCode) {
dispatchKeyEvent('keydown', keyCode);setTimeout(function() {
dispatchKeyEvent('keyup', keyCode);
}, 300);
}const jump = function() {
simulateKeyPress(38); // keyCode for the Arrow Up key is 38
};const duck = function() {
simulateKeyPress(40); // keyCode for the Arrow Down key is 40
};const game = Runner.instance_;(function tick() {
/* do not do anything if the game is not running */
if (game.crashed || game.paused) {
return requestAnimationFrame(tick);
}const obstacles = game.horizon.obstacles;if (obstacles.length) {
const tRex = game.tRex;
const tRexWidth = tRex.config.WIDTH;
const obstacle = obstacles[0];
const obstacleWidth = obstacles[0].width;
const obstacleHeight = obstacles[0].typeConfig.height;
const gravity = game.config.GRAVITY;
const safety = 2 * game.currentSpeed;
const jumpHeight = obstacleHeight + safety;
const jumpVelocityY = Math.sqrt(2 * gravity * jumpHeight);
const jumpAngle = 60 * Math.PI / 180;
const jumpVelocity = jumpVelocityY / Math.sin(jumpAngle);
const jumpDistance = safety + Math.pow(jumpVelocity, 2) * Math.sin(2 * jumpAngle) / gravity;if (obstacle.xPos - tRex.xPos - tRexWidth <= 0.5 * (jumpDistance - obstacleWidth)) {
if (obstacle.yPos > 75) {
if (!tRex.jumping) {
jump();
tRex.jumpVelocity = -jumpVelocityY;
}
} else
if (obstacle.yPos === 75) {
if (!tRex.ducking) {
duck();
}
}
}
}
requestAnimationFrame(tick);
}());
Open your Chrome Dev Tools and paste the code in the console then start the game and enjoy watching the T-Rex play like a pro!
The Challenge
I ran this script to see if the dinosaur will be able to break my score of 5500 points but it actually scored over 30,000 points. I tried breaking this score but I couldn’t … can you?
Disclaimer
I am not saying that the dinosaur will never lose. It will try its best not to, but eventually it will hit an obstacle and lose. Never-loses was not the goal anyway.