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).
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...
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
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":
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:
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:
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
};