├── README.md ├── part1 ├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .prettierrc ├── README.md ├── ScreenFlow.gif ├── index.html ├── package.json ├── src │ ├── game │ │ ├── Cloud.js │ │ ├── CollisionBox.js │ │ ├── DistanceMeter.js │ │ ├── Horizon.js │ │ ├── HorizonLine.js │ │ ├── ImageSprite.js │ │ ├── Obstacle.js │ │ ├── Runner.js │ │ ├── RuntimeConfig.js │ │ ├── Trex.js │ │ ├── TrexGroup.js │ │ ├── constants.js │ │ ├── images │ │ │ ├── offline-sprite.png │ │ │ └── splash.png │ │ ├── index.js │ │ ├── index.less │ │ └── utils.js │ └── nn.js └── webpack.config.js └── part2 ├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .prettierrc ├── README.md ├── ScreenFlow.gif ├── index.html ├── package.json ├── src ├── ai │ └── models │ │ ├── Model.js │ │ ├── genetic │ │ └── GeneticModel.js │ │ └── random │ │ └── RandomModel.js ├── apps │ └── genetic.js └── game │ ├── Cloud.js │ ├── CollisionBox.js │ ├── DistanceMeter.js │ ├── Horizon.js │ ├── HorizonLine.js │ ├── ImageSprite.js │ ├── Obstacle.js │ ├── Runner.js │ ├── RuntimeConfig.js │ ├── Trex.js │ ├── TrexGroup.js │ ├── constants.js │ ├── images │ ├── offline-sprite.png │ └── splash.png │ ├── index.js │ ├── index.less │ └── utils.js └── webpack.config.js /README.md: -------------------------------------------------------------------------------- 1 | # GeneticAlgorithms 2 | 3 | ## Part 1 4 | Automating The Chrome Dino Game using Neural Networks and Tensorflow.js 5 | * To Run clone this repo and follow the commands 6 | 7 | ```sh 8 | cd part1 9 | npm install 10 | npm start 11 | ``` 12 | 13 | ## Part 2 14 | Automating The Chrome Dino Game using Genetic Algorithms 15 | * To Run clone this repo and follow the commands 16 | 17 | ```sh 18 | cd part2 19 | npm install 20 | npm start 21 | ``` 22 | -------------------------------------------------------------------------------- /part1/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "stage-1"] 3 | } 4 | -------------------------------------------------------------------------------- /part1/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /part1/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "airbnb-base", 5 | "airbnb-base/rules/strict", 6 | "prettier" 7 | ], 8 | "env": { 9 | "browser": true 10 | }, 11 | "rules": { 12 | "class-methods-use-this": "off", 13 | "comma-dangle": "off", 14 | "function-paren-newline": "off", 15 | "global-require": "off", 16 | "import/prefer-default-export": "off", 17 | "max-len": "warn", 18 | "no-loop-func": "off", 19 | "no-mixed-operators": "off", 20 | "no-param-reassign": "off", 21 | "no-unused-vars": "off", 22 | "no-use-before-define": "off", 23 | "no-console": [ 24 | "warn", 25 | { 26 | "allow": [ 27 | "info", 28 | "warn", 29 | "error" 30 | ] 31 | } 32 | ], 33 | "no-underscore-dangle": "off", 34 | "prefer-destructuring": "off" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /part1/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Compiled assets 9 | build 10 | 11 | # Dependency directories 12 | node_modules/ 13 | 14 | # Optional npm cache directory 15 | .npm 16 | 17 | # Optional eslint cache 18 | .eslintcache 19 | 20 | # VSCode 21 | .vscode 22 | 23 | # Package 24 | package-lock.json 25 | -------------------------------------------------------------------------------- /part1/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "arrowParens": "always" 5 | } 6 | -------------------------------------------------------------------------------- /part1/README.md: -------------------------------------------------------------------------------- 1 | # Automating the Chrome Dinosaur game 2 | 3 | A TensorFlow.js based AI for playing chrome dino game. 4 | 5 | 6 | ## About 7 | 8 | ![](https://9to5google.files.wordpress.com/2015/06/pterodactyl.png?w=1600&h=1000) 9 | 10 | [Chrome dino game](http://www.omgchrome.com/chrome-easter-egg-trex-game-offline/) is [originally](https://cs.chromium.org/chromium/src/components/neterror/resources/offline.js?q=t-rex+package:%5Echromium$&dr=C&l=7) an easter egg game inside chrome's offline error page. 11 | 12 | 13 | ## About TensorFlow.js 14 | 15 | The official version of TensorFlow in JavaScript. It is A WebGL accelerated, browser based JavaScript library for training and deploying ML models. 16 | Visit the [official website](https://js.tensorflow.org/) to discover more. 17 | 18 | 19 | ## About This Project 20 | This is the code for part 1 of the blog series Automating the Chrome Dinosaur game, in the project we use the simple Tensorflow.js API to make a neural network which plays the chrome dino game ,while learning from its mistakes just like a human brain. 21 | 22 | ## How to Install 23 | 24 | ```sh 25 | npm install 26 | ``` 27 | 28 | 29 | ## How to Run 30 | 31 | 32 | ```sh 33 | npm start 34 | ``` 35 | 36 | Visit http://localhost:8080 37 | -------------------------------------------------------------------------------- /part1/ScreenFlow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aayusharora/GeneticAlgorithms/82c137a0c01770ef7e859a2d6cdadb7885a50047/part1/ScreenFlow.gif -------------------------------------------------------------------------------- /part1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Neural Network - T-Rex Runner 8 | 9 | 10 | 11 |

Simple Neural Network Model

12 |
13 |
14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /part1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-dino-game", 3 | "version": "1.0.0", 4 | "description": "A TensorFlow.js based AI player platform for chrome dino game.The dinosaur game is originally an easter egg game inside chrome.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --mode production", 8 | "start": "webpack-dev-server --mode development" 9 | }, 10 | "keywords": [ 11 | "tensorflow", 12 | "tensorflow.js", 13 | "neural network", 14 | "ai", 15 | "t-rex", 16 | "runner", 17 | "chrome", 18 | "easter", 19 | "egg", 20 | "multiplayer", 21 | "game" 22 | ], 23 | "author": "pratyush goel", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "babel-core": "^6.26.0", 27 | "babel-eslint": "^8.2.2", 28 | "babel-loader": "^7.1.4", 29 | "babel-preset-env": "^1.6.1", 30 | "babel-preset-stage-1": "^6.24.1", 31 | "clean-webpack-plugin": "^0.1.19", 32 | "css-loader": "^0.28.11", 33 | "eslint": "^4.19.1", 34 | "eslint-config-airbnb-base": "^12.1.0", 35 | "eslint-config-prettier": "^2.9.0", 36 | "eslint-loader": "^2.0.0", 37 | "eslint-plugin-import": "^2.10.0", 38 | "file-loader": "^1.1.11", 39 | "less": "^3.0.1", 40 | "less-loader": "^4.1.0", 41 | "style-loader": "^0.20.3", 42 | "url-loader": "^1.0.1", 43 | "webpack": "^4.5.0", 44 | "webpack-cli": "^3.1.1", 45 | "webpack-dev-server": "^3.1.3" 46 | }, 47 | "dependencies": { 48 | "@tensorflow/tfjs": "^0.9.0", 49 | "babel-polyfill": "^6.26.0", 50 | "dev": "^0.1.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /part1/src/game/Cloud.js: -------------------------------------------------------------------------------- 1 | import { getImageSprite } from './ImageSprite'; 2 | import { getRandomNum } from './utils'; 3 | 4 | /** 5 | * Cloud background item. 6 | * Similar to an obstacle object but without collision boxes. 7 | * @param {HTMLCanvasElement} canvas Canvas element. 8 | * @param {Object} spritePos Position of image in sprite. 9 | * @param {number} containerWidth 10 | */ 11 | export default class Cloud { 12 | /** 13 | * Cloud object config. 14 | * @enum {number} 15 | */ 16 | static config = { 17 | HEIGHT: 14, 18 | MAX_CLOUD_GAP: 400, 19 | MAX_SKY_LEVEL: 30, 20 | MIN_CLOUD_GAP: 100, 21 | MIN_SKY_LEVEL: 71, 22 | WIDTH: 46 23 | }; 24 | 25 | constructor(canvas, spritePos, containerWidth) { 26 | this.canvas = canvas; 27 | this.canvasCtx = this.canvas.getContext('2d'); 28 | this.spritePos = spritePos; 29 | this.containerWidth = containerWidth; 30 | this.xPos = containerWidth; 31 | this.yPos = 0; 32 | this.remove = false; 33 | this.cloudGap = getRandomNum( 34 | Cloud.config.MIN_CLOUD_GAP, 35 | Cloud.config.MAX_CLOUD_GAP 36 | ); 37 | 38 | this.init(); 39 | } 40 | 41 | /** 42 | * Initialise the cloud. Sets the Cloud height. 43 | */ 44 | init() { 45 | this.yPos = getRandomNum( 46 | Cloud.config.MAX_SKY_LEVEL, 47 | Cloud.config.MIN_SKY_LEVEL 48 | ); 49 | this.draw(); 50 | } 51 | 52 | /** 53 | * Draw the cloud. 54 | */ 55 | draw() { 56 | this.canvasCtx.save(); 57 | const sourceWidth = Cloud.config.WIDTH; 58 | const sourceHeight = Cloud.config.HEIGHT; 59 | 60 | this.canvasCtx.drawImage( 61 | getImageSprite(), 62 | this.spritePos.x, 63 | this.spritePos.y, 64 | sourceWidth, 65 | sourceHeight, 66 | this.xPos, 67 | this.yPos, 68 | Cloud.config.WIDTH, 69 | Cloud.config.HEIGHT 70 | ); 71 | 72 | this.canvasCtx.restore(); 73 | } 74 | 75 | /** 76 | * Update the cloud position. 77 | * @param {number} speed 78 | */ 79 | update(speed) { 80 | if (!this.remove) { 81 | this.xPos -= Math.ceil(speed); 82 | this.draw(); 83 | 84 | // Mark as removeable if no longer in the canvas. 85 | if (!this.isVisible()) { 86 | this.remove = true; 87 | } 88 | } 89 | } 90 | 91 | /** 92 | * Check if the cloud is visible on the stage. 93 | * @return {boolean} 94 | */ 95 | isVisible() { 96 | return this.xPos + Cloud.config.WIDTH > 0; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /part1/src/game/CollisionBox.js: -------------------------------------------------------------------------------- 1 | import { CANVAS_WIDTH } from './constants'; 2 | import { getImageSprite } from './ImageSprite'; 3 | 4 | /** 5 | * Collision box object. 6 | * @param {number} x X position. 7 | * @param {number} y Y Position. 8 | * @param {number} w Width. 9 | * @param {number} h Height. 10 | */ 11 | export default class CollisionBox { 12 | constructor(x, y, w, h) { 13 | this.x = x; 14 | this.y = y; 15 | this.width = w; 16 | this.height = h; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /part1/src/game/DistanceMeter.js: -------------------------------------------------------------------------------- 1 | import { getImageSprite } from './ImageSprite'; 2 | 3 | /** 4 | * Handles displaying the distance meter. 5 | * @param {!HTMLCanvasElement} canvas 6 | * @param {Object} spritePos Image position in sprite. 7 | * @param {number} canvasWidth 8 | * @constructor 9 | */ 10 | export default class DistanceMeter { 11 | static dimensions = { 12 | WIDTH: 10, 13 | HEIGHT: 13, 14 | DEST_WIDTH: 11 15 | }; 16 | 17 | static yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120]; 18 | 19 | static config = { 20 | // Number of digits. 21 | MAX_DISTANCE_UNITS: 5, 22 | 23 | // Distance that causes achievement animation. 24 | ACHIEVEMENT_DISTANCE: 100, 25 | 26 | // Used for conversion from pixel distance to a scaled unit. 27 | COEFFICIENT: 0.025, 28 | 29 | // Flash duration in milliseconds. 30 | FLASH_DURATION: 1000 / 4, 31 | 32 | // Flash iterations for achievement animation. 33 | FLASH_ITERATIONS: 3 34 | }; 35 | 36 | constructor(canvas, spritePos, canvasWidth) { 37 | this.canvas = canvas; 38 | this.canvasCtx = canvas.getContext('2d'); 39 | this.image = getImageSprite(); 40 | this.spritePos = spritePos; 41 | this.x = 0; 42 | this.y = 5; 43 | 44 | this.currentDistance = 0; 45 | this.maxScore = 0; 46 | this.highScore = 0; 47 | this.container = null; 48 | 49 | this.digits = []; 50 | this.acheivement = false; 51 | this.defaultString = ''; 52 | this.flashTimer = 0; 53 | this.flashIterations = 0; 54 | this.invertTrigger = false; 55 | 56 | this.config = DistanceMeter.config; 57 | this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS; 58 | this.init(canvasWidth); 59 | } 60 | 61 | /** 62 | * Initialise the distance meter to '00000'. 63 | * @param {number} width Canvas width in px. 64 | */ 65 | init(width) { 66 | let maxDistanceStr = ''; 67 | 68 | this.calcXPos(width); 69 | this.maxScore = this.maxScoreUnits; 70 | for (let i = 0; i < this.maxScoreUnits; i += 1) { 71 | this.draw(i, 0); 72 | this.defaultString += '0'; 73 | maxDistanceStr += '9'; 74 | } 75 | 76 | this.maxScore = parseInt(maxDistanceStr, 0); 77 | } 78 | 79 | /** 80 | * Calculate the xPos in the canvas. 81 | * @param {number} canvasWidth 82 | */ 83 | calcXPos(canvasWidth) { 84 | this.x = 85 | canvasWidth - 86 | DistanceMeter.dimensions.DEST_WIDTH * (this.maxScoreUnits + 1); 87 | } 88 | 89 | /** 90 | * Draw a digit to canvas. 91 | * @param {number} digitPos Position of the digit. 92 | * @param {number} value Digit value 0-9. 93 | * @param {boolean} highScore Whether drawing the high score. 94 | */ 95 | draw(digitPos, value, highScore) { 96 | const sourceWidth = DistanceMeter.dimensions.WIDTH; 97 | const sourceHeight = DistanceMeter.dimensions.HEIGHT; 98 | let sourceX = DistanceMeter.dimensions.WIDTH * value; 99 | let sourceY = 0; 100 | 101 | const targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH; 102 | const targetY = this.y; 103 | const targetWidth = DistanceMeter.dimensions.WIDTH; 104 | const targetHeight = DistanceMeter.dimensions.HEIGHT; 105 | 106 | sourceX += this.spritePos.x; 107 | sourceY += this.spritePos.y; 108 | 109 | this.canvasCtx.save(); 110 | 111 | if (highScore) { 112 | // Left of the current score. 113 | const highScoreX = 114 | this.x - this.maxScoreUnits * 2 * DistanceMeter.dimensions.WIDTH; 115 | this.canvasCtx.translate(highScoreX, this.y); 116 | } else { 117 | this.canvasCtx.translate(this.x, this.y); 118 | } 119 | 120 | this.canvasCtx.drawImage( 121 | this.image, 122 | sourceX, 123 | sourceY, 124 | sourceWidth, 125 | sourceHeight, 126 | targetX, 127 | targetY, 128 | targetWidth, 129 | targetHeight 130 | ); 131 | 132 | this.canvasCtx.restore(); 133 | } 134 | 135 | /** 136 | * Covert pixel distance to a 'real' distance. 137 | * @param {number} distance Pixel distance ran. 138 | * @return {number} The 'real' distance ran. 139 | */ 140 | getActualDistance(distance) { 141 | return distance ? Math.round(distance * this.config.COEFFICIENT) : 0; 142 | } 143 | 144 | /** 145 | * Update the distance meter. 146 | * @param {number} distance 147 | * @param {number} deltaTime 148 | */ 149 | update(deltaTime, distance) { 150 | let paint = true; 151 | 152 | if (!this.acheivement) { 153 | distance = this.getActualDistance(distance); 154 | // Score has gone beyond the initial digit count. 155 | if ( 156 | distance > this.maxScore && 157 | this.maxScoreUnits === this.config.MAX_DISTANCE_UNITS 158 | ) { 159 | this.maxScoreUnits += 1; 160 | this.maxScore = parseInt(`${this.maxScore}9`, 1); 161 | } else { 162 | this.distance = 0; 163 | } 164 | 165 | if (distance > 0) { 166 | // Acheivement unlocked 167 | if (distance % this.config.ACHIEVEMENT_DISTANCE === 0) { 168 | this.acheivement = true; 169 | this.flashTimer = 0; 170 | } 171 | 172 | // Create a string representation of the distance with leading 0. 173 | const distanceStr = (this.defaultString + distance).substr( 174 | -this.maxScoreUnits 175 | ); 176 | this.digits = distanceStr.split(''); 177 | } else { 178 | this.digits = this.defaultString.split(''); 179 | } 180 | } else if (this.flashIterations <= this.config.FLASH_ITERATIONS) { 181 | this.flashTimer += deltaTime; 182 | 183 | if (this.flashTimer < this.config.FLASH_DURATION) { 184 | paint = false; 185 | } else if (this.flashTimer > this.config.FLASH_DURATION * 2) { 186 | this.flashTimer = 0; 187 | this.flashIterations += 1; 188 | } 189 | } else { 190 | this.acheivement = false; 191 | this.flashIterations = 0; 192 | this.flashTimer = 0; 193 | } 194 | 195 | // Draw the digits if not flashing. 196 | if (paint) { 197 | for (let i = this.digits.length - 1; i >= 0; i -= 1) { 198 | this.draw(i, parseInt(this.digits[i], 0)); 199 | } 200 | } 201 | 202 | this.drawHighScore(); 203 | } 204 | /** 205 | * Draw the high score. 206 | */ 207 | drawHighScore() { 208 | this.canvasCtx.save(); 209 | this.canvasCtx.globalAlpha = 0.8; 210 | for (let i = this.highScore.length - 1; i >= 0; i -= 1) { 211 | this.draw(i, parseInt(this.highScore[i], 10), true); 212 | } 213 | this.canvasCtx.restore(); 214 | } 215 | 216 | /** 217 | * Set the highscore as a array string. 218 | * Position of char in the sprite: H - 10, I - 11. 219 | * @param {number} distance Distance ran in pixels. 220 | */ 221 | setHighScore(distance) { 222 | distance = this.getActualDistance(distance); 223 | const highScoreStr = (this.defaultString + distance).substr( 224 | -this.maxScoreUnits 225 | ); 226 | 227 | this.highScore = ['10', '11', ''].concat(highScoreStr.split('')); 228 | } 229 | 230 | /** 231 | * Reset the distance meter back to '00000'. 232 | */ 233 | reset() { 234 | this.update(0); 235 | this.acheivement = false; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /part1/src/game/Horizon.js: -------------------------------------------------------------------------------- 1 | import { getRandomNum } from './utils'; 2 | import { RUNNER_MAX_OBSTACLE_DUPLICATION } from './constants'; 3 | import Cloud from './Cloud'; 4 | import HorizonLine from './HorizonLine'; 5 | import Obstacle from './Obstacle'; 6 | 7 | /** 8 | * Horizon background class. 9 | * @param {HTMLCanvasElement} canvas 10 | * @param {Object} spritePos Sprite positioning. 11 | * @param {Object} dimensions Canvas dimensions. 12 | * @param {number} gapCoefficient 13 | * @constructor 14 | */ 15 | export default class Horizon { 16 | /** 17 | * Horizon config. 18 | * @enum {number} 19 | */ 20 | static config = { 21 | BG_CLOUD_SPEED: 0.2, 22 | BUMPY_THRESHOLD: 0.3, 23 | CLOUD_FREQUENCY: 0.5, 24 | HORIZON_HEIGHT: 16, 25 | MAX_CLOUDS: 6 26 | }; 27 | 28 | constructor(canvas, spritePos, dimensions, gapCoefficient) { 29 | this.canvas = canvas; 30 | this.canvasCtx = this.canvas.getContext('2d'); 31 | this.config = Horizon.config; 32 | this.dimensions = dimensions; 33 | this.gapCoefficient = gapCoefficient; 34 | this.obstacles = []; 35 | this.obstacleHistory = []; 36 | this.horizonOffsets = [0, 0]; 37 | this.cloudFrequency = this.config.CLOUD_FREQUENCY; 38 | this.spritePos = spritePos; 39 | 40 | // Cloud 41 | this.clouds = []; 42 | this.cloudSpeed = this.config.BG_CLOUD_SPEED; 43 | 44 | // Horizon 45 | this.horizonLine = null; 46 | this.init(); 47 | } 48 | 49 | /** 50 | * Initialise the horizon. Just add the line and a cloud. No obstacles. 51 | */ 52 | init() { 53 | this.addCloud(); 54 | this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON); 55 | } 56 | 57 | /** 58 | * @param {number} deltaTime 59 | * @param {number} currentSpeed 60 | * @param {boolean} updateObstacles Used as an override to prevent 61 | * the obstacles from being updated / added. This happens in the 62 | * ease in section. 63 | */ 64 | update(deltaTime, currentSpeed, updateObstacles) { 65 | this.runningTime += deltaTime; 66 | this.horizonLine.update(deltaTime, currentSpeed); 67 | this.updateClouds(deltaTime, currentSpeed); 68 | 69 | if (updateObstacles) { 70 | this.updateObstacles(deltaTime, currentSpeed); 71 | } 72 | } 73 | 74 | /** 75 | * Update the cloud positions. 76 | * @param {number} deltaTime 77 | * @param {number} currentSpeed 78 | */ 79 | updateClouds(deltaTime, speed) { 80 | const cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed; 81 | const numClouds = this.clouds.length; 82 | 83 | if (numClouds) { 84 | for (let i = numClouds - 1; i >= 0; i -= 1) { 85 | this.clouds[i].update(cloudSpeed); 86 | } 87 | 88 | const lastCloud = this.clouds[numClouds - 1]; 89 | 90 | // Check for adding a new cloud. 91 | if ( 92 | numClouds < this.config.MAX_CLOUDS && 93 | this.dimensions.WIDTH - lastCloud.xPos > lastCloud.cloudGap && 94 | this.cloudFrequency > Math.random() 95 | ) { 96 | this.addCloud(); 97 | } 98 | 99 | // Remove expired clouds. 100 | this.clouds = this.clouds.filter(obj => !obj.remove); 101 | } else { 102 | this.addCloud(); 103 | } 104 | } 105 | 106 | /** 107 | * Update the obstacle positions. 108 | * @param {number} deltaTime 109 | * @param {number} currentSpeed 110 | */ 111 | updateObstacles(deltaTime, currentSpeed) { 112 | // Obstacles, move to Horizon layer. 113 | const updatedObstacles = this.obstacles.slice(0); 114 | 115 | for (let i = 0; i < this.obstacles.length; i += 1) { 116 | const obstacle = this.obstacles[i]; 117 | obstacle.update(deltaTime, currentSpeed); 118 | 119 | // Clean up existing obstacles. 120 | if (obstacle.remove) { 121 | updatedObstacles.shift(); 122 | } 123 | } 124 | this.obstacles = updatedObstacles; 125 | 126 | if (this.obstacles.length > 0) { 127 | const lastObstacle = this.obstacles[this.obstacles.length - 1]; 128 | 129 | if ( 130 | lastObstacle && 131 | !lastObstacle.followingObstacleCreated && 132 | lastObstacle.isVisible() && 133 | lastObstacle.xPos + lastObstacle.width + lastObstacle.gap < 134 | this.dimensions.WIDTH 135 | ) { 136 | this.addNewObstacle(currentSpeed); 137 | lastObstacle.followingObstacleCreated = true; 138 | } 139 | } else { 140 | // Create new obstacles. 141 | this.addNewObstacle(currentSpeed); 142 | } 143 | } 144 | 145 | removeFirstObstacle() { 146 | this.obstacles.shift(); 147 | } 148 | 149 | /** 150 | * Add a new obstacle. 151 | * @param {number} currentSpeed 152 | */ 153 | addNewObstacle(currentSpeed) { 154 | const obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1); 155 | const obstacleType = Obstacle.types[obstacleTypeIndex]; 156 | 157 | // Check for multiples of the same type of obstacle. 158 | // Also check obstacle is available at current speed. 159 | if ( 160 | this.duplicateObstacleCheck(obstacleType.type) || 161 | currentSpeed < obstacleType.minSpeed 162 | ) { 163 | this.addNewObstacle(currentSpeed); 164 | } else { 165 | const obstacleSpritePos = this.spritePos[obstacleType.type]; 166 | 167 | this.obstacles.push( 168 | new Obstacle( 169 | this.canvasCtx, 170 | obstacleType, 171 | obstacleSpritePos, 172 | this.dimensions, 173 | this.gapCoefficient, 174 | currentSpeed, 175 | obstacleType.width 176 | ) 177 | ); 178 | 179 | this.obstacleHistory.unshift(obstacleType.type); 180 | 181 | if (this.obstacleHistory.length > 1) { 182 | this.obstacleHistory.splice(RUNNER_MAX_OBSTACLE_DUPLICATION); 183 | } 184 | } 185 | } 186 | 187 | /** 188 | * Returns whether the previous two obstacles are the same as the next one. 189 | * Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION. 190 | * @return {boolean} 191 | */ 192 | duplicateObstacleCheck(nextObstacleType) { 193 | let duplicateCount = 0; 194 | 195 | for (let i = 0; i < this.obstacleHistory.length; i += 1) { 196 | duplicateCount = 197 | this.obstacleHistory[i] === nextObstacleType ? duplicateCount + 1 : 0; 198 | } 199 | return duplicateCount >= RUNNER_MAX_OBSTACLE_DUPLICATION; 200 | } 201 | 202 | /** 203 | * Reset the horizon layer. 204 | * Remove existing obstacles and reposition the horizon line. 205 | */ 206 | reset() { 207 | this.obstacles = []; 208 | this.horizonLine.reset(); 209 | } 210 | 211 | /** 212 | * Update the canvas width and scaling. 213 | * @param {number} width Canvas width. 214 | * @param {number} height Canvas height. 215 | */ 216 | resize(width, height) { 217 | this.canvas.width = width; 218 | this.canvas.height = height; 219 | } 220 | 221 | /** 222 | * Add a new cloud to the horizon. 223 | */ 224 | addCloud() { 225 | this.clouds.push( 226 | new Cloud(this.canvas, this.spritePos.CLOUD, this.dimensions.WIDTH) 227 | ); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /part1/src/game/HorizonLine.js: -------------------------------------------------------------------------------- 1 | import { getFPS } from './RuntimeConfig'; 2 | import { getImageSprite } from './ImageSprite'; 3 | 4 | /** 5 | * Horizon Line. 6 | * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon. 7 | * @param {HTMLCanvasElement} canvas 8 | * @param {Object} spritePos Horizon position in sprite. 9 | * @constructor 10 | */ 11 | export default class HorizonLine { 12 | /** 13 | * Horizon line dimensions. 14 | * @enum {number} 15 | */ 16 | static dimensions = { 17 | WIDTH: 600, 18 | HEIGHT: 12, 19 | YPOS: 127 20 | }; 21 | 22 | constructor(canvas, spritePos) { 23 | this.spritePos = spritePos; 24 | this.canvas = canvas; 25 | this.canvasCtx = canvas.getContext('2d'); 26 | this.sourceDimensions = {}; 27 | this.dimensions = HorizonLine.dimensions; 28 | this.sourceXPos = [ 29 | this.spritePos.x, 30 | this.spritePos.x + this.dimensions.WIDTH 31 | ]; 32 | this.xPos = []; 33 | this.yPos = 0; 34 | this.bumpThreshold = 0.5; 35 | 36 | this.setSourceDimensions(); 37 | this.draw(); 38 | } 39 | 40 | /** 41 | * Set the source dimensions of the horizon line. 42 | */ 43 | setSourceDimensions() { 44 | /* eslint-disable-next-line */ 45 | for (const dimension in HorizonLine.dimensions) { 46 | this.sourceDimensions[dimension] = HorizonLine.dimensions[dimension]; 47 | this.dimensions[dimension] = HorizonLine.dimensions[dimension]; 48 | } 49 | 50 | this.xPos = [0, HorizonLine.dimensions.WIDTH]; 51 | this.yPos = HorizonLine.dimensions.YPOS; 52 | } 53 | 54 | /** 55 | * Return the crop x position of a type. 56 | */ 57 | getRandomType() { 58 | return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0; 59 | } 60 | 61 | /** 62 | * Draw the horizon line. 63 | */ 64 | draw() { 65 | this.canvasCtx.drawImage( 66 | getImageSprite(), 67 | this.sourceXPos[0], 68 | this.spritePos.y, 69 | this.sourceDimensions.WIDTH, 70 | this.sourceDimensions.HEIGHT, 71 | this.xPos[0], 72 | this.yPos, 73 | this.dimensions.WIDTH, 74 | this.dimensions.HEIGHT 75 | ); 76 | 77 | this.canvasCtx.drawImage( 78 | getImageSprite(), 79 | this.sourceXPos[1], 80 | this.spritePos.y, 81 | this.sourceDimensions.WIDTH, 82 | this.sourceDimensions.HEIGHT, 83 | this.xPos[1], 84 | this.yPos, 85 | this.dimensions.WIDTH, 86 | this.dimensions.HEIGHT 87 | ); 88 | } 89 | 90 | /** 91 | * Update the x position of an indivdual piece of the line. 92 | * @param {number} pos Line position. 93 | * @param {number} increment 94 | */ 95 | updateXPos(pos, increment) { 96 | const line1 = pos; 97 | const line2 = pos === 0 ? 1 : 0; 98 | 99 | this.xPos[line1] -= increment; 100 | this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH; 101 | 102 | if (this.xPos[line1] <= -this.dimensions.WIDTH) { 103 | this.xPos[line1] += this.dimensions.WIDTH * 2; 104 | this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH; 105 | this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x; 106 | } 107 | } 108 | 109 | /** 110 | * Update the horizon line. 111 | * @param {number} deltaTime 112 | * @param {number} speed 113 | */ 114 | update(deltaTime, speed) { 115 | const increment = Math.floor(speed * (getFPS() / 1000) * deltaTime); 116 | 117 | if (this.xPos[0] <= 0) { 118 | this.updateXPos(0, increment); 119 | } else { 120 | this.updateXPos(1, increment); 121 | } 122 | this.draw(); 123 | } 124 | 125 | /** 126 | * Reset horizon to the starting position. 127 | */ 128 | reset() { 129 | this.xPos[0] = 0; 130 | this.xPos[1] = HorizonLine.dimensions.WIDTH; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /part1/src/game/ImageSprite.js: -------------------------------------------------------------------------------- 1 | let _imageSprite = null; 2 | 3 | export function getImageSprite() { 4 | return _imageSprite; 5 | } 6 | 7 | export function loadImageSprite() { 8 | return new Promise((resolve) => { 9 | const imageSprite = document.createElement('img'); 10 | imageSprite.src = require('./images/offline-sprite.png'); 11 | imageSprite.addEventListener('load', () => { 12 | _imageSprite = imageSprite; 13 | resolve(); 14 | }); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /part1/src/game/Obstacle.js: -------------------------------------------------------------------------------- 1 | import { getFPS } from './RuntimeConfig'; 2 | import { getImageSprite } from './ImageSprite'; 3 | import { getRandomNum } from './utils'; 4 | import CollisionBox from './CollisionBox'; 5 | 6 | /** 7 | * Obstacle. 8 | * @param {HTMLCanvasCtx} canvasCtx 9 | * @param {Obstacle.type} type 10 | * @param {Object} spritePos Obstacle position in sprite. 11 | * @param {Object} dimensions 12 | * @param {number} gapCoefficient Mutipler in determining the gap. 13 | * @param {number} speed 14 | * @param {number} offset 15 | */ 16 | export default class Obstacle { 17 | /** 18 | * Coefficient for calculating the maximum gap. 19 | * @const 20 | */ 21 | static MAX_GAP_COEFFICIENT = 1.5; 22 | 23 | /** 24 | * Maximum obstacle grouping count. 25 | * @const 26 | */ 27 | static MAX_OBSTACLE_LENGTH = 3; 28 | 29 | static types = [ 30 | { 31 | type: 'CACTUS_SMALL', 32 | width: 17, 33 | height: 35, 34 | yPos: 105, 35 | multipleSpeed: 4, 36 | minGap: 120, 37 | minSpeed: 0, 38 | collisionBoxes: [ 39 | new CollisionBox(0, 7, 5, 27), 40 | new CollisionBox(4, 0, 6, 34), 41 | new CollisionBox(10, 4, 7, 14) 42 | ] 43 | }, 44 | { 45 | type: 'CACTUS_LARGE', 46 | width: 25, 47 | height: 50, 48 | yPos: 90, 49 | multipleSpeed: 7, 50 | minGap: 120, 51 | minSpeed: 0, 52 | collisionBoxes: [ 53 | new CollisionBox(0, 12, 7, 38), 54 | new CollisionBox(8, 0, 7, 49), 55 | new CollisionBox(13, 10, 10, 38) 56 | ] 57 | }, 58 | { 59 | type: 'PTERODACTYL', 60 | width: 46, 61 | height: 40, 62 | yPos: [100, 75, 50], // Variable height. 63 | yPosMobile: [100, 50], // Variable height mobile. 64 | multipleSpeed: 999, 65 | minSpeed: 8.5, 66 | minGap: 150, 67 | collisionBoxes: [ 68 | new CollisionBox(15, 15, 16, 5), 69 | new CollisionBox(18, 21, 24, 6), 70 | new CollisionBox(2, 14, 4, 3), 71 | new CollisionBox(6, 10, 4, 7), 72 | new CollisionBox(10, 8, 6, 9) 73 | ], 74 | numFrames: 2, 75 | frameRate: 1000 / 6, 76 | speedOffset: 0.8 77 | } 78 | ]; 79 | 80 | constructor( 81 | canvasCtx, 82 | type, 83 | spriteImgPos, 84 | dimensions, 85 | gapCoefficient, 86 | speed, 87 | offset 88 | ) { 89 | this.canvasCtx = canvasCtx; 90 | this.spritePos = spriteImgPos; 91 | this.typeConfig = type; 92 | this.gapCoefficient = gapCoefficient; 93 | this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH); 94 | this.dimensions = dimensions; 95 | this.remove = false; 96 | this.xPos = dimensions.WIDTH + (offset || 0); 97 | this.yPos = 0; 98 | this.width = 0; 99 | this.collisionBoxes = []; 100 | this.gap = 0; 101 | this.speedOffset = 0; 102 | 103 | // For animated obstacles. 104 | this.currentFrame = 0; 105 | this.timer = 0; 106 | 107 | this.init(speed); 108 | } 109 | 110 | init(speed) { 111 | this.cloneCollisionBoxes(); 112 | 113 | // Only allow sizing if we're at the right speed. 114 | if (this.size > 1 && this.typeConfig.multipleSpeed > speed) { 115 | this.size = 1; 116 | } 117 | 118 | this.width = this.typeConfig.width * this.size; 119 | 120 | // Check if obstacle can be positioned at various heights. 121 | if (Array.isArray(this.typeConfig.yPos)) { 122 | const yPosConfig = this.typeConfig.yPos; 123 | this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)]; 124 | } else { 125 | this.yPos = this.typeConfig.yPos; 126 | } 127 | 128 | this.draw(); 129 | 130 | // Make collision box adjustments, 131 | // Central box is adjusted to the size as one box. 132 | // ____ ______ ________ 133 | // _| |-| _| |-| _| |-| 134 | // | |<->| | | |<--->| | | |<----->| | 135 | // | | 1 | | | | 2 | | | | 3 | | 136 | // |_|___|_| |_|_____|_| |_|_______|_| 137 | // 138 | if (this.size > 1) { 139 | this.collisionBoxes[1].width = 140 | this.width - 141 | this.collisionBoxes[0].width - 142 | this.collisionBoxes[2].width; 143 | this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width; 144 | } 145 | 146 | // For obstacles that go at a different speed from the horizon. 147 | if (this.typeConfig.speedOffset) { 148 | this.speedOffset = 149 | Math.random() > 0.5 150 | ? this.typeConfig.speedOffset 151 | : -this.typeConfig.speedOffset; 152 | } 153 | 154 | this.gap = this.getGap(this.gapCoefficient, speed); 155 | } 156 | 157 | /** 158 | * Draw and crop based on size. 159 | */ 160 | draw() { 161 | const sourceWidth = this.typeConfig.width; 162 | const sourceHeight = this.typeConfig.height; 163 | 164 | // X position in sprite. 165 | let sourceX = 166 | sourceWidth * this.size * (0.5 * (this.size - 1)) + this.spritePos.x; 167 | 168 | // Animation frames. 169 | if (this.currentFrame > 0) { 170 | sourceX += sourceWidth * this.currentFrame; 171 | } 172 | 173 | this.canvasCtx.drawImage( 174 | getImageSprite(), 175 | sourceX, 176 | this.spritePos.y, 177 | sourceWidth * this.size, 178 | sourceHeight, 179 | this.xPos, 180 | this.yPos, 181 | this.typeConfig.width * this.size, 182 | this.typeConfig.height 183 | ); 184 | } 185 | 186 | /** 187 | * Obstacle frame update. 188 | * @param {number} deltaTime 189 | * @param {number} speed 190 | */ 191 | update(deltaTime, speed) { 192 | if (!this.remove) { 193 | if (this.typeConfig.speedOffset) { 194 | speed += this.speedOffset; 195 | } 196 | this.xPos -= Math.floor(speed * getFPS() / 1000 * deltaTime); 197 | 198 | // Update frame 199 | if (this.typeConfig.numFrames) { 200 | this.timer += deltaTime; 201 | if (this.timer >= this.typeConfig.frameRate) { 202 | this.currentFrame = 203 | this.currentFrame === this.typeConfig.numFrames - 1 204 | ? 0 205 | : this.currentFrame + 1; 206 | this.timer = 0; 207 | } 208 | } 209 | this.draw(); 210 | 211 | if (!this.isVisible()) { 212 | this.remove = true; 213 | } 214 | } 215 | } 216 | 217 | /** 218 | * Calculate a random gap size. 219 | * - Minimum gap gets wider as speed increses 220 | * @param {number} gapCoefficient 221 | * @param {number} speed 222 | * @return {number} The gap size. 223 | */ 224 | getGap(gapCoefficient, speed) { 225 | const minGap = Math.round( 226 | this.width * speed + this.typeConfig.minGap * gapCoefficient 227 | ); 228 | const maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT); 229 | return getRandomNum(minGap, maxGap); 230 | } 231 | 232 | /** 233 | * Check if obstacle is visible. 234 | * @return {boolean} Whether the obstacle is in the game area. 235 | */ 236 | isVisible() { 237 | return this.xPos + this.width > 0; 238 | } 239 | 240 | /** 241 | * Make a copy of the collision boxes, since these will change based on 242 | * obstacle type and size. 243 | */ 244 | cloneCollisionBoxes() { 245 | const { collisionBoxes } = this.typeConfig; 246 | 247 | for (let i = collisionBoxes.length - 1; i >= 0; i -= 1) { 248 | this.collisionBoxes[i] = new CollisionBox( 249 | collisionBoxes[i].x, 250 | collisionBoxes[i].y, 251 | collisionBoxes[i].width, 252 | collisionBoxes[i].height 253 | ); 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /part1/src/game/Runner.js: -------------------------------------------------------------------------------- 1 | import { CANVAS_WIDTH, CANVAS_HEIGHT } from './constants'; 2 | import { getFPS } from './RuntimeConfig'; 3 | import { getImageSprite, setImageSprite, loadImageSprite } from './ImageSprite'; 4 | import { getTimeStamp } from './utils'; 5 | import DistanceMeter from './DistanceMeter'; 6 | import Horizon from './Horizon'; 7 | import Trex, { checkForCollision } from './Trex'; 8 | import TrexGroup from './TrexGroup'; 9 | 10 | /** 11 | * T-Rex runner. 12 | * @param {string} outerContainerId Outer containing element id. 13 | * @param {Object} options 14 | * @constructor 15 | * @export 16 | */ 17 | export default class Runner { 18 | static generation = 0; 19 | 20 | static config = { 21 | ACCELERATION: 0.001, 22 | BG_CLOUD_SPEED: 0.2, 23 | CLEAR_TIME: 0, 24 | CLOUD_FREQUENCY: 0.5, 25 | GAP_COEFFICIENT: 0.6, 26 | GRAVITY: 0.6, 27 | INITIAL_JUMP_VELOCITY: 12, 28 | MAX_CLOUDS: 6, 29 | MAX_OBSTACLE_LENGTH: 3, 30 | MAX_SPEED: 13, 31 | MIN_JUMP_HEIGHT: 35, 32 | SPEED: 6, 33 | SPEED_DROP_COEFFICIENT: 3, 34 | DINO_COUNT: 1, 35 | // Events 36 | onReset: noop, 37 | onRunning: noop, 38 | onCrash: noop 39 | }; 40 | 41 | static classes = { 42 | CANVAS: 'game-canvas', 43 | CONTAINER: 'game-container', 44 | }; 45 | 46 | static spriteDefinition = { 47 | CACTUS_LARGE: { x: 332, y: 2 }, 48 | CACTUS_SMALL: { x: 228, y: 2 }, 49 | CLOUD: { x: 86, y: 2 }, 50 | HORIZON: { x: 2, y: 54 }, 51 | PTERODACTYL: { x: 134, y: 2 }, 52 | RESTART: { x: 2, y: 2 }, 53 | TEXT_SPRITE: { x: 655, y: 2 }, 54 | TREX: { x: 848, y: 2 } 55 | }; 56 | 57 | /** 58 | * Key code mapping. 59 | * @enum {Object} 60 | */ 61 | static keycodes = { 62 | JUMP: { 38: 1, 32: 1 }, // Up, spacebar 63 | DUCK: { 40: 1 } // Down 64 | }; 65 | 66 | /** 67 | * Runner event names. 68 | * @enum {string} 69 | */ 70 | static events = { 71 | ANIM_END: 'webkitAnimationEnd', 72 | CLICK: 'click', 73 | KEYDOWN: 'keydown', 74 | KEYUP: 'keyup', 75 | RESIZE: 'resize', 76 | VISIBILITY: 'visibilitychange', 77 | BLUR: 'blur', 78 | FOCUS: 'focus', 79 | LOAD: 'load' 80 | }; 81 | 82 | constructor(outerContainerId, options) { 83 | // Singleton 84 | if (Runner.instance_) { 85 | return Runner.instance_; 86 | } 87 | Runner.instance_ = this; 88 | 89 | this.isFirstTime = false; 90 | this.outerContainerEl = document.querySelector(outerContainerId); 91 | this.generationEl = document.querySelector('.generation'); 92 | this.containerEl = null; 93 | 94 | this.config = Object.assign({}, Runner.config, options); 95 | 96 | this.dimensions = { 97 | WIDTH: CANVAS_WIDTH, 98 | HEIGHT: CANVAS_HEIGHT 99 | }; 100 | 101 | this.canvas = null; 102 | this.canvasCtx = null; 103 | 104 | this.tRex = null; 105 | 106 | this.distanceMeter = null; 107 | this.distanceRan = 0; 108 | 109 | this.highestScore = 0; 110 | 111 | this.time = 0; 112 | this.runningTime = 0; 113 | this.msPerFrame = 1000 / getFPS(); 114 | this.currentSpeed = this.config.SPEED; 115 | 116 | this.obstacles = []; 117 | 118 | this.activated = false; // Whether the easter egg has been activated. 119 | this.playing = false; // Whether the game is currently in play state. 120 | this.crashed = false; 121 | this.resizeTimerId_ = null; 122 | 123 | this.playCount = 0; 124 | 125 | // Images. 126 | this.images = {}; 127 | this.imagesLoaded = 0; 128 | } 129 | 130 | async init() { 131 | await loadImageSprite(); 132 | this.spriteDef = Runner.spriteDefinition; 133 | 134 | this.adjustDimensions(); 135 | this.setSpeed(); 136 | 137 | this.containerEl = document.createElement('div'); 138 | this.containerEl.className = Runner.classes.CONTAINER; 139 | this.containerEl.style.width = `${this.dimensions.WIDTH}px`; 140 | 141 | // Player canvas container. 142 | this.canvas = createCanvas( 143 | this.containerEl, 144 | this.dimensions.WIDTH, 145 | this.dimensions.HEIGHT, 146 | Runner.classes.PLAYER 147 | ); 148 | 149 | this.canvasCtx = this.canvas.getContext('2d'); 150 | this.canvasCtx.fillStyle = '#f7f7f7'; 151 | this.canvasCtx.fill(); 152 | Runner.updateCanvasScaling(this.canvas); 153 | 154 | // Horizon contains clouds, obstacles and the ground. 155 | this.horizon = new Horizon( 156 | this.canvas, 157 | this.spriteDef, 158 | this.dimensions, 159 | this.config.GAP_COEFFICIENT 160 | ); 161 | 162 | // Distance meter 163 | this.distanceMeter = new DistanceMeter( 164 | this.canvas, 165 | this.spriteDef.TEXT_SPRITE, 166 | this.dimensions.WIDTH 167 | ); 168 | 169 | // Draw t-rex 170 | this.tRexGroup = new TrexGroup(this.config.DINO_COUNT, this.canvas, this.spriteDef.TREX); 171 | this.tRexGroup.onRunning = this.config.onRunning; 172 | this.tRexGroup.onCrash = this.config.onCrash; 173 | this.tRex = this.tRexGroup.tRexes[0]; 174 | 175 | this.outerContainerEl.appendChild(this.containerEl); 176 | 177 | this.startListening(); 178 | this.update(); 179 | 180 | window.addEventListener( 181 | Runner.events.RESIZE, 182 | this.debounceResize.bind(this) 183 | ); 184 | 185 | this.restart(); 186 | } 187 | 188 | /** 189 | * Debounce the resize event. 190 | */ 191 | debounceResize() { 192 | if (!this.resizeTimerId_) { 193 | this.resizeTimerId_ = setInterval(this.adjustDimensions.bind(this), 250); 194 | } 195 | } 196 | 197 | /** 198 | * Adjust game space dimensions on resize. 199 | */ 200 | adjustDimensions() { 201 | clearInterval(this.resizeTimerId_); 202 | this.resizeTimerId_ = null; 203 | 204 | const boxStyles = window.getComputedStyle(this.outerContainerEl); 205 | const padding = Number( 206 | boxStyles.paddingLeft.substr(0, boxStyles.paddingLeft.length - 2) 207 | ); 208 | 209 | this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2; 210 | 211 | // Redraw the elements back onto the canvas. 212 | if (this.canvas) { 213 | this.canvas.width = this.dimensions.WIDTH; 214 | this.canvas.height = this.dimensions.HEIGHT; 215 | 216 | Runner.updateCanvasScaling(this.canvas); 217 | 218 | this.distanceMeter.calcXPos(this.dimensions.WIDTH); 219 | this.clearCanvas(); 220 | this.horizon.update(0, 0, true); 221 | this.tRexGroup.update(0); 222 | 223 | // Outer container and distance meter. 224 | if (this.playing || this.crashed) { 225 | this.containerEl.style.width = `${this.dimensions.WIDTH}px`; 226 | this.containerEl.style.height = `${this.dimensions.HEIGHT}px`; 227 | this.distanceMeter.update(0, Math.ceil(this.distanceRan)); 228 | this.stop(); 229 | } else { 230 | this.tRexGroup.draw(0, 0); 231 | } 232 | } 233 | } 234 | 235 | /** 236 | * Sets the game speed. Adjust the speed accordingly if on a smaller screen. 237 | * @param {number} speed 238 | */ 239 | setSpeed(speed) { 240 | this.currentSpeed = speed || this.currentSpeed; 241 | } 242 | 243 | /** 244 | * Update the game status to started. 245 | */ 246 | startGame() { 247 | this.runningTime = 0; 248 | this.containerEl.style.webkitAnimation = ''; 249 | this.playCount += 1; 250 | } 251 | 252 | clearCanvas() { 253 | this.canvasCtx.clearRect( 254 | 0, 255 | 0, 256 | this.dimensions.WIDTH, 257 | this.dimensions.HEIGHT 258 | ); 259 | } 260 | 261 | /** 262 | * Update the game frame and schedules the next one. 263 | */ 264 | update() { 265 | this.updatePending = false; 266 | 267 | const now = getTimeStamp(); 268 | let deltaTime = now - (this.time || now); 269 | this.time = now; 270 | 271 | if (this.playing) { 272 | this.clearCanvas(); 273 | 274 | this.tRexGroup.updateJump(deltaTime); 275 | 276 | this.runningTime += deltaTime; 277 | const hasObstacles = this.runningTime > this.config.CLEAR_TIME; 278 | 279 | // First time 280 | if (this.isFirstTime) { 281 | if (!this.activated && !this.crashed) { 282 | this.playing = true; 283 | this.activated = true; 284 | this.startGame(); 285 | } 286 | } 287 | 288 | deltaTime = !this.activated ? 0 : deltaTime; 289 | this.horizon.update(deltaTime, this.currentSpeed, hasObstacles); 290 | 291 | let gameOver = false; 292 | // Check for collisions. 293 | if (hasObstacles) { 294 | gameOver = this.tRexGroup.checkForCollision(this.horizon.obstacles[0]); 295 | } 296 | 297 | if (!gameOver) { 298 | this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame; 299 | 300 | if (this.currentSpeed < this.config.MAX_SPEED) { 301 | this.currentSpeed += this.config.ACCELERATION; 302 | } 303 | } else { 304 | this.gameOver(); 305 | } 306 | 307 | this.distanceMeter.update( 308 | deltaTime, 309 | Math.ceil(this.distanceRan) 310 | ); 311 | } 312 | 313 | if ( 314 | this.playing || 315 | (!this.activated) 316 | ) { 317 | this.tRexGroup.update(deltaTime); 318 | this.scheduleNextUpdate(); 319 | } 320 | 321 | const lives = this.tRexGroup.lives(); 322 | if (lives > 0) { 323 | this.generationEl.innerText = `GENERATION #${Runner.generation} | LIVE x ${this.tRexGroup.lives()}`; 324 | } else { 325 | this.generationEl.innerHTML = `
GENERATION #${Runner.generation} | GAME OVER
`; 326 | } 327 | } 328 | 329 | /** 330 | * Bind relevant key 331 | */ 332 | startListening() { 333 | document.addEventListener(Runner.events.KEYDOWN, (e) => { 334 | this.onKeyDown(e); 335 | }); 336 | document.addEventListener(Runner.events.KEYUP, (e) => { 337 | this.onKeyUp(e); 338 | }); 339 | } 340 | 341 | /** 342 | * Process keydown. 343 | * @param {Event} e 344 | */ 345 | onKeyDown(e) { 346 | if (!this.crashed && this.playing) { 347 | if (Runner.keycodes.JUMP[e.keyCode]) { 348 | e.preventDefault(); 349 | this.tRex.startJump(this.currentSpeed); 350 | } else if (Runner.keycodes.DUCK[e.keyCode]) { 351 | e.preventDefault(); 352 | if (this.tRex.jumping) { 353 | // Speed drop, activated only when jump key is not pressed. 354 | this.tRex.setSpeedDrop(); 355 | } else if (!this.tRex.jumping && !this.tRex.ducking) { 356 | // Duck. 357 | this.tRex.setDuck(true); 358 | } 359 | } 360 | } else if (this.crashed) { 361 | this.restart(); 362 | } 363 | } 364 | 365 | /** 366 | * Process key up. 367 | * @param {Event} e 368 | */ 369 | onKeyUp(e) { 370 | const keyCode = String(e.keyCode); 371 | const isJumpKey = Runner.keycodes.JUMP[keyCode]; 372 | 373 | if (this.isRunning() && isJumpKey) { 374 | this.tRex.endJump(); 375 | } else if (Runner.keycodes.DUCK[keyCode]) { 376 | this.tRex.speedDrop = false; 377 | this.tRex.setDuck(false); 378 | } else if (this.crashed) { 379 | if (Runner.keycodes.JUMP[keyCode]) { 380 | this.restart(); 381 | } 382 | } 383 | } 384 | 385 | /** 386 | * RequestAnimationFrame wrapper. 387 | */ 388 | scheduleNextUpdate() { 389 | if (!this.updatePending) { 390 | this.updatePending = true; 391 | this.raqId = requestAnimationFrame(this.update.bind(this)); 392 | } 393 | } 394 | 395 | /** 396 | * Whether the game is running. 397 | * @return {boolean} 398 | */ 399 | isRunning() { 400 | return !!this.raqId; 401 | } 402 | 403 | /** 404 | * Game over state. 405 | */ 406 | gameOver() { 407 | this.stop(); 408 | this.crashed = true; 409 | this.distanceMeter.acheivement = false; 410 | 411 | this.tRexGroup.update(100, Trex.status.CRASHED); 412 | 413 | // Update the high score. 414 | if (this.distanceRan > this.highestScore) { 415 | this.highestScore = Math.ceil(this.distanceRan); 416 | this.distanceMeter.setHighScore(this.highestScore); 417 | } 418 | 419 | // Reset the time clock. 420 | this.time = getTimeStamp(); 421 | 422 | setTimeout(() => { 423 | this.restart(); 424 | }, 500); 425 | } 426 | 427 | stop() { 428 | this.playing = false; 429 | cancelAnimationFrame(this.raqId); 430 | this.raqId = 0; 431 | } 432 | 433 | play() { 434 | if (!this.crashed) { 435 | this.playing = true; 436 | this.tRexGroup.update(0, Trex.status.RUNNING); 437 | this.time = getTimeStamp(); 438 | this.update(); 439 | } 440 | } 441 | 442 | restart() { 443 | if (!this.raqId) { 444 | this.playCount += 1; 445 | this.runningTime = 0; 446 | this.playing = true; 447 | this.crashed = false; 448 | this.distanceRan = 0; 449 | this.setSpeed(this.config.SPEED); 450 | this.time = getTimeStamp(); 451 | this.clearCanvas(); 452 | this.distanceMeter.reset(this.highestScore); 453 | this.horizon.reset(); 454 | this.tRexGroup.reset(); 455 | this.config.onReset(this.tRexGroup.tRexes ); 456 | this.update(); 457 | } else { 458 | this.isFirstTime = true; 459 | this.tRexGroup.reset(); 460 | this.config.onReset(this.tRexGroup.tRexes ); 461 | if (!this.playing) { 462 | this.playing = true; 463 | this.update(); 464 | } 465 | } 466 | Runner.generation += 1; 467 | } 468 | 469 | /** 470 | * Updates the canvas size taking into 471 | * account the backing store pixel ratio and 472 | * the device pixel ratio. 473 | * 474 | * See article by Paul Lewis: 475 | * http://www.html5rocks.com/en/tutorials/canvas/hidpi/ 476 | * 477 | * @param {HTMLCanvasElement} canvas 478 | * @param {number} width 479 | * @param {number} height 480 | * @return {boolean} Whether the canvas was scaled. 481 | */ 482 | static updateCanvasScaling(canvas, width, height) { 483 | const context = canvas.getContext('2d'); 484 | 485 | // Query the various pixel ratios 486 | const devicePixelRatio = Math.floor(window.devicePixelRatio) || 1; 487 | const backingStoreRatio = 488 | Math.floor(context.webkitBackingStorePixelRatio) || 1; 489 | const ratio = devicePixelRatio / backingStoreRatio; 490 | 491 | // Upscale the canvas if the two ratios don't match 492 | if (devicePixelRatio !== backingStoreRatio) { 493 | const oldWidth = width || canvas.width; 494 | const oldHeight = height || canvas.height; 495 | 496 | canvas.width = oldWidth * ratio; 497 | canvas.height = oldHeight * ratio; 498 | 499 | canvas.style.width = `${oldWidth}px`; 500 | canvas.style.height = `${oldHeight}px`; 501 | 502 | // Scale the context to counter the fact that we've manually scaled 503 | // our canvas element. 504 | context.scale(ratio, ratio); 505 | return true; 506 | } else if (devicePixelRatio === 1) { 507 | // Reset the canvas width / height. Fixes scaling bug when the page is 508 | // zoomed and the devicePixelRatio changes accordingly. 509 | canvas.style.width = `${canvas.width}px`; 510 | canvas.style.height = `${canvas.height}px`; 511 | } 512 | return false; 513 | } 514 | } 515 | 516 | /** 517 | * Create canvas element. 518 | * @param {HTMLElement} container Element to append canvas to. 519 | * @param {number} width 520 | * @param {number} height 521 | * @param {string} className 522 | * @return {HTMLCanvasElement} 523 | */ 524 | function createCanvas(container, width, height, className) { 525 | const canvas = document.createElement('canvas'); 526 | canvas.className = className 527 | ? `${Runner.classes.CANVAS} ${className}` 528 | : Runner.classes.CANVAS; 529 | canvas.width = width; 530 | canvas.height = height; 531 | container.appendChild(canvas); 532 | 533 | return canvas; 534 | } 535 | 536 | function noop() {} 537 | -------------------------------------------------------------------------------- /part1/src/game/RuntimeConfig.js: -------------------------------------------------------------------------------- 1 | let FPS = 60; 2 | 3 | export function getFPS() { 4 | return FPS; 5 | } 6 | 7 | export function setFPS(value) { 8 | FPS = value; 9 | } 10 | -------------------------------------------------------------------------------- /part1/src/game/Trex.js: -------------------------------------------------------------------------------- 1 | import { 2 | CANVAS_WIDTH, 3 | CANVAS_HEIGHT, 4 | RUNNER_BOTTOM_PAD 5 | } from './constants'; 6 | import { getFPS } from './RuntimeConfig'; 7 | import { getImageSprite } from './ImageSprite'; 8 | import { getTimeStamp } from './utils'; 9 | import CollisionBox from './CollisionBox'; 10 | import Runner from './Runner'; 11 | 12 | /** 13 | * T-rex game character. 14 | * @param {HTMLCanvas} canvas 15 | * @param {Object} spritePos Positioning within image sprite. 16 | * @constructor 17 | */ 18 | export default class Trex { 19 | static config = { 20 | DROP_VELOCITY: -5, 21 | GRAVITY: 0.6, 22 | HEIGHT: 47, 23 | HEIGHT_DUCK: 25, 24 | INIITAL_JUMP_VELOCITY: -10, 25 | MAX_JUMP_HEIGHT: 30, 26 | MIN_JUMP_HEIGHT: 30, 27 | SPEED_DROP_COEFFICIENT: 3, 28 | SPRITE_WIDTH: 262, 29 | START_X_POS: 50, 30 | WIDTH: 44, 31 | WIDTH_DUCK: 59 32 | }; 33 | 34 | /** 35 | * Used in collision detection. 36 | * @type {Array} 37 | */ 38 | static collisionBoxes = { 39 | DUCKING: [new CollisionBox(1, 18, 55, 25)], 40 | RUNNING: [ 41 | new CollisionBox(22, 0, 17, 16), 42 | new CollisionBox(1, 18, 30, 9), 43 | new CollisionBox(10, 35, 14, 8), 44 | new CollisionBox(1, 24, 29, 5), 45 | new CollisionBox(5, 30, 21, 4), 46 | new CollisionBox(9, 34, 15, 4) 47 | ] 48 | }; 49 | 50 | /** 51 | * Animation states. 52 | * @enum {string} 53 | */ 54 | static status = { 55 | CRASHED: 'CRASHED', 56 | DUCKING: 'DUCKING', 57 | JUMPING: 'JUMPING', 58 | RUNNING: 'RUNNING', 59 | WAITING: 'WAITING' 60 | }; 61 | 62 | /** 63 | * Blinking coefficient. 64 | * @const 65 | */ 66 | static BLINK_TIMING = 7000; 67 | 68 | /** 69 | * Animation config for different states. 70 | * @enum {Object} 71 | */ 72 | static animFrames = { 73 | WAITING: { 74 | frames: [44, 0], 75 | msPerFrame: 1000 / 3 76 | }, 77 | RUNNING: { 78 | frames: [88, 132], 79 | msPerFrame: 1000 / 12 80 | }, 81 | CRASHED: { 82 | frames: [220], 83 | msPerFrame: 1000 / 60 84 | }, 85 | JUMPING: { 86 | frames: [0], 87 | msPerFrame: 1000 / 60 88 | }, 89 | DUCKING: { 90 | frames: [262, 321], 91 | msPerFrame: 1000 / 8 92 | } 93 | }; 94 | 95 | constructor(canvas, spritePos) { 96 | this.canvas = canvas; 97 | this.canvasCtx = canvas.getContext('2d'); 98 | this.spritePos = spritePos; 99 | this.xPos = 0; 100 | this.yPos = 0; 101 | // Position when on the ground. 102 | this.groundYPos = 0; 103 | this.currentFrame = 0; 104 | this.currentAnimFrames = []; 105 | this.blinkDelay = 0; 106 | this.blinkCount = 0; 107 | this.animStartTime = 0; 108 | this.timer = 0; 109 | this.msPerFrame = 1000 / getFPS(); 110 | this.config = Trex.config; 111 | // Current status. 112 | this.status = Trex.status.WAITING; 113 | 114 | this.jumping = false; 115 | this.ducking = false; 116 | this.jumpVelocity = 0; 117 | this.reachedMinHeight = false; 118 | this.speedDrop = false; 119 | this.jumpCount = 0; 120 | this.jumpspotX = 0; 121 | 122 | this.init(); 123 | } 124 | 125 | /** 126 | * T-rex player initaliser. 127 | * Sets the t-rex to blink at random intervals. 128 | */ 129 | init() { 130 | this.groundYPos = CANVAS_HEIGHT - this.config.HEIGHT - RUNNER_BOTTOM_PAD; 131 | this.yPos = this.groundYPos; 132 | this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT; 133 | 134 | this.draw(0, 0); 135 | this.update(0, Trex.status.WAITING); 136 | } 137 | 138 | /** 139 | * Setter for the jump velocity. 140 | * The approriate drop velocity is also set. 141 | */ 142 | setJumpVelocity(setting) { 143 | this.config.INIITAL_JUMP_VELOCITY = -setting; 144 | this.config.DROP_VELOCITY = -setting / 2; 145 | } 146 | 147 | /** 148 | * Set the animation status. 149 | * @param {!number} deltaTime 150 | * @param {Trex.status} status Optional status to switch to. 151 | */ 152 | update(deltaTime, status) { 153 | this.timer += deltaTime; 154 | 155 | // Update the status. 156 | if (status) { 157 | this.status = status; 158 | this.currentFrame = 0; 159 | this.msPerFrame = Trex.animFrames[status].msPerFrame; 160 | this.currentAnimFrames = Trex.animFrames[status].frames; 161 | 162 | if (status === Trex.status.WAITING) { 163 | this.animStartTime = getTimeStamp(); 164 | this.setBlinkDelay(); 165 | } 166 | } 167 | 168 | if (this.status === Trex.status.WAITING) { 169 | this.blink(getTimeStamp()); 170 | } else { 171 | this.draw(this.currentAnimFrames[this.currentFrame], 0); 172 | } 173 | 174 | // Update the frame position. 175 | if (this.timer >= this.msPerFrame) { 176 | this.currentFrame = 177 | this.currentFrame === this.currentAnimFrames.length - 1 178 | ? 0 179 | : this.currentFrame + 1; 180 | this.timer = 0; 181 | } 182 | 183 | // Speed drop becomes duck if the down key is still being pressed. 184 | if (this.speedDrop && this.yPos === this.groundYPos) { 185 | this.speedDrop = false; 186 | this.setDuck(true); 187 | } 188 | } 189 | 190 | /** 191 | * Draw the t-rex to a particular position. 192 | * @param {number} x 193 | * @param {number} y 194 | */ 195 | draw(x, y) { 196 | let sourceX = x; 197 | let sourceY = y; 198 | const sourceWidth = 199 | this.ducking && this.status !== Trex.status.CRASHED 200 | ? this.config.WIDTH_DUCK 201 | : this.config.WIDTH; 202 | const sourceHeight = this.config.HEIGHT; 203 | 204 | // Adjustments for sprite sheet position. 205 | sourceX += this.spritePos.x; 206 | sourceY += this.spritePos.y; 207 | 208 | // Ducking. 209 | if (this.ducking && this.status !== Trex.status.CRASHED) { 210 | this.canvasCtx.drawImage( 211 | getImageSprite(), 212 | sourceX, 213 | sourceY, 214 | sourceWidth, 215 | sourceHeight, 216 | this.xPos, 217 | this.yPos, 218 | this.config.WIDTH_DUCK, 219 | this.config.HEIGHT 220 | ); 221 | } else { 222 | // Crashed whilst ducking. Trex is standing up so needs adjustment. 223 | if (this.ducking && this.status === Trex.status.CRASHED) { 224 | this.xPos += 1; 225 | } 226 | // Standing / running 227 | this.canvasCtx.drawImage( 228 | getImageSprite(), 229 | sourceX, 230 | sourceY, 231 | sourceWidth, 232 | sourceHeight, 233 | this.xPos, 234 | this.yPos, 235 | this.config.WIDTH, 236 | this.config.HEIGHT 237 | ); 238 | } 239 | } 240 | 241 | /** 242 | * Sets a random time for the blink to happen. 243 | */ 244 | setBlinkDelay() { 245 | this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING); 246 | } 247 | 248 | /** 249 | * Make t-rex blink at random intervals. 250 | * @param {number} time Current time in milliseconds. 251 | */ 252 | blink(time) { 253 | const deltaTime = time - this.animStartTime; 254 | 255 | if (deltaTime >= this.blinkDelay) { 256 | this.draw(this.currentAnimFrames[this.currentFrame], 0); 257 | 258 | if (this.currentFrame === 1) { 259 | // Set new random delay to blink. 260 | this.setBlinkDelay(); 261 | this.animStartTime = time; 262 | this.blinkCount += 1; 263 | } 264 | } 265 | } 266 | 267 | /** 268 | * Initialise a jump. 269 | * @param {number} speed 270 | */ 271 | startJump(speed) { 272 | if (speed === undefined) { 273 | speed = Runner.instance_.currentSpeed; 274 | } 275 | if (!this.jumping) { 276 | this.update(0, Trex.status.JUMPING); 277 | // Tweak the jump velocity based on the speed. 278 | this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - speed / 10; 279 | this.jumping = true; 280 | this.reachedMinHeight = false; 281 | this.speedDrop = false; 282 | } 283 | } 284 | 285 | /** 286 | * Jump is complete, falling down. 287 | */ 288 | endJump() { 289 | if ( 290 | this.reachedMinHeight && 291 | this.jumpVelocity < this.config.DROP_VELOCITY 292 | ) { 293 | this.jumpVelocity = this.config.DROP_VELOCITY; 294 | } 295 | } 296 | 297 | /** 298 | * Update frame for a jump. 299 | * @param {number} deltaTime 300 | * @param {number} speed 301 | */ 302 | updateJump(deltaTime, speed) { 303 | const { msPerFrame } = Trex.animFrames[this.status]; 304 | const framesElapsed = deltaTime / msPerFrame; 305 | 306 | // Speed drop makes Trex fall faster. 307 | if (this.speedDrop) { 308 | this.yPos += Math.round( 309 | this.jumpVelocity * this.config.SPEED_DROP_COEFFICIENT * framesElapsed 310 | ); 311 | } else { 312 | this.yPos += Math.round(this.jumpVelocity * framesElapsed); 313 | } 314 | 315 | this.jumpVelocity += this.config.GRAVITY * framesElapsed; 316 | 317 | // Minimum height has been reached. 318 | if (this.yPos < this.minJumpHeight || this.speedDrop) { 319 | this.reachedMinHeight = true; 320 | } 321 | 322 | // Reached max height 323 | if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) { 324 | this.endJump(); 325 | } 326 | 327 | // Back down at ground level. Jump completed. 328 | if (this.yPos > this.groundYPos) { 329 | this.reset(); 330 | this.jumpCount += 1; 331 | } 332 | 333 | this.update(deltaTime); 334 | } 335 | 336 | /** 337 | * Set the speed drop. Immediately cancels the current jump. 338 | */ 339 | setSpeedDrop() { 340 | this.speedDrop = true; 341 | this.jumpVelocity = 1; 342 | } 343 | 344 | /** 345 | * @param {boolean} isDucking. 346 | */ 347 | setDuck(isDucking) { 348 | if (isDucking && this.status !== Trex.status.DUCKING) { 349 | this.update(0, Trex.status.DUCKING); 350 | this.ducking = true; 351 | } else if (this.status === Trex.status.DUCKING) { 352 | this.update(0, Trex.status.RUNNING); 353 | this.ducking = false; 354 | } 355 | } 356 | 357 | /** 358 | * Reset the t-rex to running at start of game. 359 | */ 360 | reset() { 361 | this.yPos = this.groundYPos; 362 | this.jumpVelocity = 0; 363 | this.jumping = false; 364 | this.ducking = false; 365 | this.update(0, Trex.status.RUNNING); 366 | this.midair = false; 367 | this.speedDrop = false; 368 | this.jumpCount = 0; 369 | this.crashed = false; 370 | } 371 | } 372 | 373 | /** 374 | * Check for a collision. 375 | * @param {!Obstacle} obstacle 376 | * @param {!Trex} tRex T-rex object. 377 | * @param {HTMLCanvasContext} canvasContext Optional canvas context for drawing 378 | * collision boxes. 379 | * @return {Array} 380 | */ 381 | export function checkForCollision(obstacle, tRex) { 382 | const obstacleBoxXPos = CANVAS_WIDTH + obstacle.xPos; 383 | 384 | // Adjustments are made to the bounding box as there is a 1 pixel white 385 | // border around the t-rex and obstacles. 386 | const tRexBox = new CollisionBox( 387 | tRex.xPos + 1, 388 | tRex.yPos + 1, 389 | tRex.config.WIDTH - 2, 390 | tRex.config.HEIGHT - 2 391 | ); 392 | 393 | const obstacleBox = new CollisionBox( 394 | obstacle.xPos + 1, 395 | obstacle.yPos + 1, 396 | obstacle.typeConfig.width * obstacle.size - 2, 397 | obstacle.typeConfig.height - 2 398 | ); 399 | 400 | // Simple outer bounds check. 401 | if (boxCompare(tRexBox, obstacleBox)) { 402 | const { collisionBoxes } = obstacle; 403 | const tRexCollisionBoxes = tRex.ducking 404 | ? Trex.collisionBoxes.DUCKING 405 | : Trex.collisionBoxes.RUNNING; 406 | 407 | // Detailed axis aligned box check. 408 | for (let t = 0; t < tRexCollisionBoxes.length; t += 1) { 409 | for (let i = 0; i < collisionBoxes.length; i += 1) { 410 | // Adjust the box to actual positions. 411 | const adjTrexBox = createAdjustedCollisionBox( 412 | tRexCollisionBoxes[t], 413 | tRexBox 414 | ); 415 | const adjObstacleBox = createAdjustedCollisionBox( 416 | collisionBoxes[i], 417 | obstacleBox 418 | ); 419 | const crashed = boxCompare(adjTrexBox, adjObstacleBox); 420 | 421 | if (crashed) { 422 | return [adjTrexBox, adjObstacleBox]; 423 | } 424 | } 425 | } 426 | } 427 | return false; 428 | } 429 | 430 | /** 431 | * Adjust the collision box. 432 | * @param {!CollisionBox} box The original box. 433 | * @param {!CollisionBox} adjustment Adjustment box. 434 | * @return {CollisionBox} The adjusted collision box object. 435 | */ 436 | function createAdjustedCollisionBox(box, adjustment) { 437 | return new CollisionBox( 438 | box.x + adjustment.x, 439 | box.y + adjustment.y, 440 | box.width, 441 | box.height 442 | ); 443 | } 444 | 445 | /** 446 | * Compare two collision boxes for a collision. 447 | * @param {CollisionBox} tRexBox 448 | * @param {CollisionBox} obstacleBox 449 | * @return {boolean} Whether the boxes intersected. 450 | */ 451 | function boxCompare(tRexBox, obstacleBox) { 452 | let crashed = false; 453 | const tRexBoxX = tRexBox.x; 454 | const tRexBoxY = tRexBox.y; 455 | 456 | const obstacleBoxX = obstacleBox.x; 457 | const obstacleBoxY = obstacleBox.y; 458 | 459 | // Axis-Aligned Bounding Box method. 460 | if ( 461 | tRexBox.x < obstacleBoxX + obstacleBox.width && 462 | tRexBox.x + tRexBox.width > obstacleBoxX && 463 | tRexBox.y < obstacleBox.y + obstacleBox.height && 464 | tRexBox.height + tRexBox.y > obstacleBox.y 465 | ) { 466 | crashed = true; 467 | } 468 | 469 | return crashed; 470 | } 471 | -------------------------------------------------------------------------------- /part1/src/game/TrexGroup.js: -------------------------------------------------------------------------------- 1 | import Runner from './Runner'; 2 | import Trex, { checkForCollision } from './Trex'; 3 | 4 | export default class TrexGroup { 5 | onReset = noop; 6 | onRunning = noop; 7 | onCrash = noop; 8 | 9 | constructor(count, canvas, spriteDef) { 10 | this.tRexes = []; 11 | for (let i = 0; i < count; i += 1) { 12 | const tRex = new Trex(canvas, spriteDef); 13 | tRex.id = i; 14 | this.tRexes.push(tRex); 15 | } 16 | } 17 | 18 | update(deltaTime, status) { 19 | this.tRexes.forEach((tRex) => { 20 | if (!tRex.crashed) { 21 | tRex.update(deltaTime, status); 22 | } 23 | }); 24 | } 25 | 26 | draw(x, y) { 27 | this.tRexes.forEach((tRex) => { 28 | if (!tRex.crashed) { 29 | tRex.draw(x, y); 30 | } 31 | }); 32 | } 33 | 34 | updateJump(deltaTime, speed) { 35 | this.tRexes.forEach((tRex) => { 36 | if (tRex.jumping) { 37 | tRex.updateJump(deltaTime, speed); 38 | } 39 | }); 40 | } 41 | 42 | reset() { 43 | this.tRexes.forEach((tRex) => { 44 | tRex.reset(); 45 | this.onReset(tRex); 46 | }); 47 | } 48 | 49 | lives() { 50 | return this.tRexes.reduce((count, tRex) => tRex.crashed ? count : count + 1, 0); 51 | } 52 | 53 | checkForCollision(obstacle) { 54 | let crashes = 0; 55 | const state = { 56 | obstacleX: obstacle.xPos, 57 | obstacleY: obstacle.yPos, 58 | obstacleWidth: obstacle.width, 59 | speed: Runner.instance_.currentSpeed 60 | }; 61 | this.tRexes.forEach(async (tRex) => { 62 | if (!tRex.crashed) { 63 | const result = checkForCollision(obstacle, tRex); 64 | if (result) { 65 | crashes += 1; 66 | tRex.crashed = true; 67 | this.onCrash( tRex, state ); 68 | } else { 69 | const action = await this.onRunning(tRex, state ); 70 | if (action === 1) { 71 | tRex.startJump(); 72 | } else if (action === -1) { 73 | if (tRex.jumping) { 74 | // Speed drop, activated only when jump key is not pressed. 75 | tRex.setSpeedDrop(); 76 | } else if (!tRex.jumping && !tRex.ducking) { 77 | // Duck. 78 | tRex.setDuck(true); 79 | } 80 | } 81 | } 82 | } else { 83 | crashes += 1; 84 | } 85 | }); 86 | return crashes === this.tRexes.length; 87 | } 88 | } 89 | 90 | function noop() { } 91 | -------------------------------------------------------------------------------- /part1/src/game/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default game canvas size. 3 | */ 4 | export const CANVAS_WIDTH = 600; 5 | export const CANVAS_HEIGHT = 150; 6 | 7 | /** 8 | * Runner configs 9 | */ 10 | export const RUNNER_BOTTOM_PAD = 10; 11 | export const RUNNER_MAX_OBSTACLE_DUPLICATION = 2; 12 | -------------------------------------------------------------------------------- /part1/src/game/images/offline-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aayusharora/GeneticAlgorithms/82c137a0c01770ef7e859a2d6cdadb7885a50047/part1/src/game/images/offline-sprite.png -------------------------------------------------------------------------------- /part1/src/game/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aayusharora/GeneticAlgorithms/82c137a0c01770ef7e859a2d6cdadb7885a50047/part1/src/game/images/splash.png -------------------------------------------------------------------------------- /part1/src/game/index.js: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | 3 | export { default as Runner } from './Runner'; 4 | -------------------------------------------------------------------------------- /part1/src/game/index.less: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | width: 100%; 6 | height: 100%; 7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 8 | } 9 | 10 | h1 { 11 | font-size: 18px; 12 | font-weight: bold; 13 | text-align: center; 14 | margin-top: 20px; 15 | margin-bottom: 20px; 16 | } 17 | 18 | /* Offline page */ 19 | .page { 20 | margin-top: 20px; 21 | overflow: hidden; 22 | 23 | .game { 24 | position: relative; 25 | color: #2b2b2b; 26 | font-size: 1em; 27 | margin: 0 auto; 28 | max-width: 600px; 29 | width: 100%; 30 | padding-top: 50px; 31 | } 32 | 33 | .game-canvas { 34 | height: 150px; 35 | max-width: 600px; 36 | opacity: 1; 37 | overflow: hidden; 38 | top: 0; 39 | z-index: 2; 40 | } 41 | 42 | .generation { 43 | position: absolute; 44 | top: 4px; 45 | left: 0; 46 | right: 0; 47 | font-weight: bold; 48 | text-align: center; 49 | } 50 | } 51 | 52 | @media (max-height: 350px) { 53 | .game { 54 | margin-top: 5%; 55 | } 56 | } 57 | 58 | @media (min-width: 600px) and (max-width: 736px) and (orientation: landscape) { 59 | .page .game { 60 | margin-left: 0; 61 | margin-right: 0; 62 | } 63 | } 64 | 65 | @media (max-width: 240px) { 66 | .game { 67 | overflow: inherit; 68 | padding: 0 8px; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /part1/src/game/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return the current timestamp. 3 | * @return {number} 4 | */ 5 | export function getTimeStamp() { 6 | return performance.now(); 7 | } 8 | 9 | /** 10 | * Get random number. 11 | * @param {number} min 12 | * @param {number} max 13 | * @param {number} 14 | */ 15 | export function getRandomNum(min, max) { 16 | return Math.floor(Math.random() * (max - min + 1)) + min; 17 | } 18 | -------------------------------------------------------------------------------- /part1/src/nn.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import * as tf from '@tensorflow/tfjs'; 3 | import { CANVAS_WIDTH, CANVAS_HEIGHT } from './game/constants'; 4 | import { Runner } from './game'; 5 | 6 | let runner = null; 7 | // initial setup for the game the setup function is called when the dom gets loaded 8 | 9 | function setup() { 10 | // Initialize the game Runner. 11 | runner = new Runner('.game', { 12 | DINO_COUNT: 1, 13 | onReset: handleReset, 14 | onCrash: handleCrash, 15 | onRunning: handleRunning 16 | }); 17 | // Set runner as a global variable if you need runtime debugging. 18 | window.runner = runner; 19 | // Initialize everything in the game and start the game. 20 | runner.init(); 21 | } 22 | // variable which tells whether thethe game is being loaded for the first time i.e. not a reset 23 | 24 | let firstTime = true; 25 | 26 | 27 | function handleReset(dinos) { 28 | // running this for single dino at a time 29 | // console.log(dinos); 30 | 31 | const dino = dinos[0]; 32 | // if the game is being started for the first time initiate 33 | // the model and compile it to make it ready for training and predicting 34 | if (firstTime) { 35 | firstTime = false; 36 | // creating a tensorflow sequential model 37 | dino.model = tf.sequential(); 38 | // dino.model.init(); 39 | // adding the first hidden layer to the model using with 3 inputs , 40 | // sigmoid activation function 41 | // and output of 6 42 | dino.model.add(tf.layers.dense({ 43 | inputShape:[3], 44 | activation:'sigmoid', 45 | units:6 46 | })) 47 | 48 | /* this is the second output layer with 6 inputs coming from the previous hidden layer 49 | activation is again sigmoid and output is given as 2 units 10 for not jump and 01 for jump 50 | */ 51 | dino.model.add(tf.layers.dense({ 52 | inputShape:[6], 53 | activation:'sigmoid', 54 | units:2 55 | })) 56 | 57 | /* compiling the model using meanSquaredError loss function and adam 58 | optimizer with a learning rate of 0.1 */ 59 | dino.model.compile({ 60 | loss:'meanSquaredError', 61 | optimizer : tf.train.adam(0.1) 62 | }) 63 | 64 | // object which will containn training data and appropriate labels 65 | dino.training = { 66 | inputs: [], 67 | labels: [] 68 | }; 69 | 70 | } else { 71 | // Train the model before restarting. 72 | // log into console that model will now be trained 73 | console.info('Training'); 74 | // convert the inputs and labels to tensor2d format and then training the model 75 | console.info(tf.tensor2d(dino.training.inputs)) 76 | dino.model.fit(tf.tensor2d(dino.training.inputs), tf.tensor2d(dino.training.labels)); 77 | } 78 | } 79 | 80 | /** 81 | * documentation 82 | * @param {object} dino 83 | * @param {object} state 84 | * returns a promise resolved with an action 85 | */ 86 | 87 | function handleRunning( dino, state ) { 88 | return new Promise((resolve) => { 89 | if (!dino.jumping) { 90 | // whenever the dino is not jumping decide whether it needs to jump or not 91 | let action = 0;// variable for action 1 for jump 0 for not 92 | // call model.predict on the state vecotr after converting it to tensor2d object 93 | const prediction = dino.model.predict(tf.tensor2d([convertStateToVector(state)])); 94 | 95 | // the predict function returns a tensor we get the data in a promise as result 96 | // and based don result decide the action 97 | const predictionPromise = prediction.data(); 98 | 99 | predictionPromise.then((result) => { 100 | // console.log(result); 101 | // converting prediction to action 102 | if (result[1] > result[0]) { 103 | // we want to jump 104 | action = 1; 105 | // set last jumping state to current state 106 | dino.lastJumpingState = state; 107 | } else { 108 | // set running state to current state 109 | dino.lastRunningState = state; 110 | } 111 | resolve(action); 112 | }); 113 | } else { 114 | resolve(0); 115 | } 116 | }); 117 | } 118 | /** 119 | * 120 | * @param {object} dino 121 | * handles the crash of a dino before restarting the game 122 | * 123 | */ 124 | function handleCrash( dino ) { 125 | let input = null; 126 | let label = null; 127 | // check if at the time of crash dino was jumping or not 128 | if (dino.jumping) { 129 | // Should not jump next time 130 | // convert state object to array 131 | input = convertStateToVector(dino.lastJumpingState); 132 | label = [1, 0]; 133 | } else { 134 | // Should jump next time 135 | // convert state object to array 136 | input = convertStateToVector(dino.lastRunningState); 137 | label = [0, 1]; 138 | } 139 | // push the new input to the training set 140 | dino.training.inputs.push(input); 141 | // push the label to labels 142 | dino.training.labels.push(label); 143 | } 144 | 145 | /** 146 | * 147 | * @param {object} state 148 | * returns an array 149 | * converts state to a feature scaled array 150 | */ 151 | function convertStateToVector(state) { 152 | if (state) { 153 | return [ 154 | state.obstacleX / CANVAS_WIDTH, 155 | state.obstacleWidth / CANVAS_WIDTH, 156 | state.speed / 100 157 | ]; 158 | } 159 | return [0, 0, 0]; 160 | } 161 | // call setup on loading content 162 | document.addEventListener('DOMContentLoaded', setup); 163 | -------------------------------------------------------------------------------- /part1/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | const ASSETS_SOURCE_PATH = path.resolve('./src'); 5 | const ASSETS_BUILD_PATH = path.resolve('./assets'); 6 | const ASSETS_PUBLIC_PATH = '/assets'; 7 | 8 | module.exports = { 9 | context: ASSETS_SOURCE_PATH, 10 | entry: { 11 | nn: ['./nn.js'], 12 | }, 13 | output: { 14 | path: ASSETS_BUILD_PATH, 15 | publicPath: ASSETS_PUBLIC_PATH, 16 | filename: './[name].js' 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | enforce: 'pre', 22 | test: /\.jsx?$/, 23 | exclude: /node_modules|screen-capture/, 24 | loader: 'eslint-loader' 25 | }, 26 | { 27 | test: /\.js$/, 28 | exclude: /node_modules/, 29 | loader: 'babel-loader' 30 | }, 31 | { 32 | test: /\.less$/, 33 | exclude: /node_modules/, 34 | use: ['style-loader', 'css-loader', 'less-loader'] 35 | }, 36 | { 37 | test: /\.png$/, 38 | exclude: /node_modules/, 39 | use: [ 40 | { 41 | loader: 'url-loader', 42 | options: { 43 | limit: 8192, 44 | mimetype: 'image/png', 45 | name: 'images/[name].[ext]' 46 | } 47 | } 48 | ] 49 | } 50 | ] 51 | }, 52 | plugins: [new CleanWebpackPlugin([ASSETS_BUILD_PATH], { verbose: false })], 53 | optimization: { 54 | splitChunks: { 55 | cacheGroups: { 56 | vendor: { 57 | test: /node_modules/, 58 | chunks: 'initial', 59 | name: 'vendor', 60 | priority: 10, 61 | enforce: true 62 | } 63 | } 64 | } 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /part2/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "stage-1"] 3 | } 4 | -------------------------------------------------------------------------------- /part2/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /part2/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "airbnb-base", 5 | "airbnb-base/rules/strict", 6 | "prettier" 7 | ], 8 | "env": { 9 | "browser": true 10 | }, 11 | "rules": { 12 | "class-methods-use-this": "off", 13 | "comma-dangle": "off", 14 | "function-paren-newline": "off", 15 | "global-require": "off", 16 | "import/prefer-default-export": "off", 17 | "max-len": "warn", 18 | "no-loop-func": "off", 19 | "no-mixed-operators": "off", 20 | "no-param-reassign": "off", 21 | "no-unused-vars": "off", 22 | "no-use-before-define": "off", 23 | "no-console": [ 24 | "warn", 25 | { 26 | "allow": [ 27 | "info", 28 | "warn", 29 | "error" 30 | ] 31 | } 32 | ], 33 | "no-underscore-dangle": "off", 34 | "prefer-destructuring": "off" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /part2/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Compiled assets 9 | build 10 | 11 | # Dependency directories 12 | node_modules/ 13 | 14 | # Optional npm cache directory 15 | .npm 16 | 17 | # Optional eslint cache 18 | .eslintcache 19 | 20 | # VSCode 21 | .vscode 22 | 23 | # Package 24 | package-lock.json 25 | -------------------------------------------------------------------------------- /part2/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "arrowParens": "always" 5 | } 6 | -------------------------------------------------------------------------------- /part2/README.md: -------------------------------------------------------------------------------- 1 | # Automating the Chrome Dinosaur game 2 | 3 | A TensorFlow.js based AI for playing chrome dino game. 4 | 5 | 6 | ## About 7 | 8 | ![](https://9to5google.files.wordpress.com/2015/06/pterodactyl.png?w=1600&h=1000) 9 | 10 | [Chrome dino game](http://www.omgchrome.com/chrome-easter-egg-trex-game-offline/) is [originally](https://cs.chromium.org/chromium/src/components/neterror/resources/offline.js?q=t-rex+package:%5Echromium$&dr=C&l=7) an easter egg game inside chrome's offline error page. 11 | 12 | 13 | ## About TensorFlow.js 14 | 15 | The official version of TensorFlow in JavaScript. It is A WebGL accelerated, browser based JavaScript library for training and deploying ML models. 16 | Visit the [official website](https://js.tensorflow.org/) to discover more. 17 | 18 | 19 | ## About This Project 20 | This is the code for part 2 of the blog series Automating the Chrome Dinosaur game, in the project we use a Genetic algorithms which is used to play the chrome dino game ,which evolves the dinos into a higher better scoring dino just like actual evolution in nature. 21 | 22 | ## How to Install 23 | 24 | ```sh 25 | npm run build 26 | ``` 27 | 28 | 29 | ## How to Run 30 | 31 | 32 | ```sh 33 | npm run start 34 | ``` 35 | 36 | Visit http://localhost:8080 37 | -------------------------------------------------------------------------------- /part2/ScreenFlow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aayusharora/GeneticAlgorithms/82c137a0c01770ef7e859a2d6cdadb7885a50047/part2/ScreenFlow.gif -------------------------------------------------------------------------------- /part2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Genetic Algorithm - T-Rex Runner 8 | 9 | 10 | 11 |

Genetic Algorithm

12 |
13 |
14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /part2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-dino-game", 3 | "version": "1.0.0", 4 | "description": "A TensorFlow.js based AI player platform for chrome dino game. The dinosaur game is originally an easter egg game inside chrome.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --mode production", 8 | "start": "webpack-dev-server --mode development" 9 | }, 10 | "keywords": [ 11 | "tensorflow", 12 | "tensorflow.js", 13 | "neural network", 14 | "ai", 15 | "t-rex", 16 | "runner", 17 | "chrome", 18 | "easter", 19 | "egg", 20 | "multiplayer", 21 | "game" 22 | ], 23 | "author": "pratyush goel", 24 | "license": "", 25 | "devDependencies": { 26 | "babel-core": "^6.26.0", 27 | "babel-eslint": "^8.2.2", 28 | "babel-loader": "^7.1.4", 29 | "babel-preset-env": "^1.6.1", 30 | "babel-preset-stage-1": "^6.24.1", 31 | "clean-webpack-plugin": "^0.1.19", 32 | "css-loader": "^0.28.11", 33 | "eslint": "^4.19.1", 34 | "eslint-config-airbnb-base": "^12.1.0", 35 | "eslint-config-prettier": "^2.9.0", 36 | "eslint-loader": "^2.0.0", 37 | "eslint-plugin-import": "^2.10.0", 38 | "file-loader": "^1.1.11", 39 | "less": "^3.0.1", 40 | "less-loader": "^4.1.0", 41 | "style-loader": "^0.20.3", 42 | "url-loader": "^1.0.1", 43 | "webpack": "^4.20.2", 44 | "webpack-cli": "^3.1.1", 45 | "webpack-dev-server": "^3.1.3" 46 | }, 47 | "dependencies": { 48 | "@tensorflow/tfjs": "^0.9.0", 49 | "babel-polyfill": "^6.26.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /part2/src/ai/models/Model.js: -------------------------------------------------------------------------------- 1 | export default class Model { 2 | init() { 3 | throw new Error( 4 | 'Abstract method must be implemented in the derived class.' 5 | ); 6 | } 7 | 8 | predict(inputXs) { 9 | throw new Error( 10 | 'Abstract method must be implemented in the derived class.' 11 | ); 12 | } 13 | 14 | predictSingle(inputX) { 15 | return this.predict([inputX]); 16 | } 17 | 18 | train(inputXs, inputYs) { 19 | throw new Error( 20 | 'Abstract method must be implemented in the derived class.' 21 | ); 22 | } 23 | 24 | fit(inputXs, inputYs, iterationCount = 100) { 25 | for (let i = 0; i < iterationCount; i += 1) { 26 | this.train(inputXs, inputYs); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /part2/src/ai/models/genetic/GeneticModel.js: -------------------------------------------------------------------------------- 1 | import Model from '../Model'; 2 | 3 | export default class GeneticModel extends Model { 4 | train(chromosomes) { 5 | const parents = this.select(chromosomes); 6 | const offspring = this.crossOver(parents, chromosomes); 7 | this.mutate(offspring); 8 | } 9 | 10 | fit(chromosomes) { 11 | this.train(chromosomes); 12 | } 13 | 14 | select(chromosomes) { 15 | const parents = [chromosomes[0], chromosomes[1]]; 16 | return parents; 17 | } 18 | 19 | crossOver(parents, chromosomes) { 20 | // Clone from parents 21 | // console.info(parents) 22 | const offspring1 = parents[0]; 23 | const offspring2 = parents[1]; 24 | console.info("off1:",offspring1); 25 | console.info("off2:",offspring2); 26 | // Select a random crossover point 27 | const crossOverPoint = Math.floor(Math.random() * offspring1.length); 28 | console.info("cross here: ",crossOverPoint); 29 | // Swap values among parents 30 | for (let i = 0; i < crossOverPoint; i += 1) { 31 | const temp = offspring1[i]; 32 | offspring1[i] = offspring2[i]; 33 | offspring2[i] = temp; 34 | } 35 | const offspring = [offspring1, offspring2]; 36 | // Replace the last 2 with the new offspring 37 | for (let i = 0; i < 2; i += 1) { 38 | chromosomes[chromosomes.length - i - 1] = offspring[i]; 39 | } 40 | console.info("child: ",offspring) 41 | return offspring; 42 | } 43 | 44 | mutate(chromosomes) { 45 | chromosomes.forEach(chromosome => { 46 | const mutationPoint = Math.floor(Math.random() * chromosomes.length); 47 | chromosome[mutationPoint] = Math.random(); 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /part2/src/ai/models/random/RandomModel.js: -------------------------------------------------------------------------------- 1 | import Model from '../Model'; 2 | 3 | export default class RandomModel extends Model { 4 | weights = []; 5 | biases = []; 6 | 7 | init() { 8 | // console.info("i happened"); 9 | this.randomize(); 10 | } 11 | 12 | predict(inputXs) { 13 | const inputX = inputXs[0]; 14 | const y = 15 | this.weights[0] * inputX[0] + 16 | this.weights[1] * inputX[1]+ 17 | this.weights[2] * inputX[2] + 18 | this.biases[0]; 19 | return y < 0 ? 1 : 0; 20 | } 21 | 22 | train() { 23 | this.randomize(); 24 | } 25 | 26 | randomize() { 27 | this.weights[0] = random(); 28 | this.weights[1] = random(); 29 | this.weights[2] = random(); 30 | this.biases[0] = random(); 31 | } 32 | getChromosome() { 33 | return this.weights.concat(this.biases); 34 | } 35 | 36 | setChromosome(chromosome) { 37 | this.weights[0] = chromosome[0]; 38 | this.weights[1] = chromosome[1]; 39 | this.weights[2] = chromosome[2]; 40 | this.biases[0] = chromosome[3]; 41 | } 42 | } 43 | 44 | function random() { 45 | return (Math.random() - 0.5) * 2; 46 | } 47 | -------------------------------------------------------------------------------- /part2/src/apps/genetic.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import { CANVAS_WIDTH, CANVAS_HEIGHT } from '../game/constants'; 3 | import { Runner } from '../game'; 4 | import GeneticModel from '../ai/models/genetic/GeneticModel'; 5 | import RandomModel from '../ai/models/random/RandomModel'; 6 | 7 | // const DINO_COUNT = 10; 8 | 9 | let runner = null; 10 | 11 | const rankList = []; 12 | const geneticModel = new GeneticModel(); 13 | 14 | let firstTime = true; 15 | 16 | function setup() { 17 | // Initialize the game Runner. 18 | runner = new Runner('.game', { 19 | DINO_COUNT:10, 20 | onReset: handleReset, 21 | onCrash: handleCrash, 22 | onRunning: handleRunning 23 | }); 24 | // Set runner as a global variable if you need runtime debugging. 25 | window.runner = runner; 26 | // console.info(runner) 27 | // Initialize everything in the game and start the game. 28 | runner.init(); 29 | } 30 | 31 | 32 | function handleReset(Dinos) { 33 | if (firstTime) { 34 | firstTime = false; 35 | // console.info("in here") 36 | // console.info(Dinos) 37 | Dinos.forEach((dino) => { 38 | // console.info("happened"); 39 | dino.model = new RandomModel(); 40 | dino.model.init(); 41 | }); 42 | 43 | } 44 | else { 45 | // Train the model before restarting. 46 | console.info('Training'); 47 | const chromosomes = rankList.map((dino) => dino.model.getChromosome()); 48 | // console.info(chromosomes) 49 | // Clear rankList 50 | rankList.splice(0); 51 | geneticModel.fit(chromosomes); 52 | Dinos.forEach((dino, i) => { 53 | dino.model.setChromosome(chromosomes[i]); 54 | }); 55 | } 56 | } 57 | 58 | function handleRunning(dino, state) { 59 | let action = 0; 60 | if (!dino.jumping) { 61 | action = dino.model.predictSingle(convertStateToVector(state)); 62 | } 63 | return action; 64 | } 65 | 66 | function handleCrash(dino) { 67 | // console.info("i was called") 68 | if (!rankList.includes(dino)) { 69 | rankList.unshift(dino); 70 | } 71 | } 72 | 73 | function convertStateToVector(state) { 74 | if (state) { 75 | return [ 76 | state.obstacleX / CANVAS_WIDTH, 77 | state.obstacleWidth / CANVAS_WIDTH, 78 | state.speed / 100 79 | ]; 80 | } 81 | return [0, 0, 0]; 82 | } 83 | 84 | document.addEventListener('DOMContentLoaded', setup); 85 | -------------------------------------------------------------------------------- /part2/src/game/Cloud.js: -------------------------------------------------------------------------------- 1 | import { getImageSprite } from './ImageSprite'; 2 | import { getRandomNum } from './utils'; 3 | 4 | /** 5 | * Cloud background item. 6 | * Similar to an obstacle object but without collision boxes. 7 | * @param {HTMLCanvasElement} canvas Canvas element. 8 | * @param {Object} spritePos Position of image in sprite. 9 | * @param {number} containerWidth 10 | */ 11 | export default class Cloud { 12 | /** 13 | * Cloud object config. 14 | * @enum {number} 15 | */ 16 | static config = { 17 | HEIGHT: 14, 18 | MAX_CLOUD_GAP: 400, 19 | MAX_SKY_LEVEL: 30, 20 | MIN_CLOUD_GAP: 100, 21 | MIN_SKY_LEVEL: 71, 22 | WIDTH: 46 23 | }; 24 | 25 | constructor(canvas, spritePos, containerWidth) { 26 | this.canvas = canvas; 27 | this.canvasCtx = this.canvas.getContext('2d'); 28 | this.spritePos = spritePos; 29 | this.containerWidth = containerWidth; 30 | this.xPos = containerWidth; 31 | this.yPos = 0; 32 | this.remove = false; 33 | this.cloudGap = getRandomNum( 34 | Cloud.config.MIN_CLOUD_GAP, 35 | Cloud.config.MAX_CLOUD_GAP 36 | ); 37 | 38 | this.init(); 39 | } 40 | 41 | /** 42 | * Initialise the cloud. Sets the Cloud height. 43 | */ 44 | init() { 45 | this.yPos = getRandomNum( 46 | Cloud.config.MAX_SKY_LEVEL, 47 | Cloud.config.MIN_SKY_LEVEL 48 | ); 49 | this.draw(); 50 | } 51 | 52 | /** 53 | * Draw the cloud. 54 | */ 55 | draw() { 56 | this.canvasCtx.save(); 57 | const sourceWidth = Cloud.config.WIDTH; 58 | const sourceHeight = Cloud.config.HEIGHT; 59 | 60 | this.canvasCtx.drawImage( 61 | getImageSprite(), 62 | this.spritePos.x, 63 | this.spritePos.y, 64 | sourceWidth, 65 | sourceHeight, 66 | this.xPos, 67 | this.yPos, 68 | Cloud.config.WIDTH, 69 | Cloud.config.HEIGHT 70 | ); 71 | 72 | this.canvasCtx.restore(); 73 | } 74 | 75 | /** 76 | * Update the cloud position. 77 | * @param {number} speed 78 | */ 79 | update(speed) { 80 | if (!this.remove) { 81 | this.xPos -= Math.ceil(speed); 82 | this.draw(); 83 | 84 | // Mark as removeable if no longer in the canvas. 85 | if (!this.isVisible()) { 86 | this.remove = true; 87 | } 88 | } 89 | } 90 | 91 | /** 92 | * Check if the cloud is visible on the stage. 93 | * @return {boolean} 94 | */ 95 | isVisible() { 96 | return this.xPos + Cloud.config.WIDTH > 0; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /part2/src/game/CollisionBox.js: -------------------------------------------------------------------------------- 1 | import { CANVAS_WIDTH } from './constants'; 2 | import { getImageSprite } from './ImageSprite'; 3 | 4 | /** 5 | * Collision box object. 6 | * @param {number} x X position. 7 | * @param {number} y Y Position. 8 | * @param {number} w Width. 9 | * @param {number} h Height. 10 | */ 11 | export default class CollisionBox { 12 | constructor(x, y, w, h) { 13 | this.x = x; 14 | this.y = y; 15 | this.width = w; 16 | this.height = h; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /part2/src/game/DistanceMeter.js: -------------------------------------------------------------------------------- 1 | import { getImageSprite } from './ImageSprite'; 2 | 3 | /** 4 | * Handles displaying the distance meter. 5 | * @param {!HTMLCanvasElement} canvas 6 | * @param {Object} spritePos Image position in sprite. 7 | * @param {number} canvasWidth 8 | * @constructor 9 | */ 10 | export default class DistanceMeter { 11 | static dimensions = { 12 | WIDTH: 10, 13 | HEIGHT: 13, 14 | DEST_WIDTH: 11 15 | }; 16 | 17 | static yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120]; 18 | 19 | static config = { 20 | // Number of digits. 21 | MAX_DISTANCE_UNITS: 5, 22 | 23 | // Distance that causes achievement animation. 24 | ACHIEVEMENT_DISTANCE: 100, 25 | 26 | // Used for conversion from pixel distance to a scaled unit. 27 | COEFFICIENT: 0.025, 28 | 29 | // Flash duration in milliseconds. 30 | FLASH_DURATION: 1000 / 4, 31 | 32 | // Flash iterations for achievement animation. 33 | FLASH_ITERATIONS: 3 34 | }; 35 | 36 | constructor(canvas, spritePos, canvasWidth) { 37 | this.canvas = canvas; 38 | this.canvasCtx = canvas.getContext('2d'); 39 | this.image = getImageSprite(); 40 | this.spritePos = spritePos; 41 | this.x = 0; 42 | this.y = 5; 43 | 44 | this.currentDistance = 0; 45 | this.maxScore = 0; 46 | this.highScore = 0; 47 | this.container = null; 48 | 49 | this.digits = []; 50 | this.acheivement = false; 51 | this.defaultString = ''; 52 | this.flashTimer = 0; 53 | this.flashIterations = 0; 54 | this.invertTrigger = false; 55 | 56 | this.config = DistanceMeter.config; 57 | this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS; 58 | this.init(canvasWidth); 59 | } 60 | 61 | /** 62 | * Initialise the distance meter to '00000'. 63 | * @param {number} width Canvas width in px. 64 | */ 65 | init(width) { 66 | let maxDistanceStr = ''; 67 | 68 | this.calcXPos(width); 69 | this.maxScore = this.maxScoreUnits; 70 | for (let i = 0; i < this.maxScoreUnits; i += 1) { 71 | this.draw(i, 0); 72 | this.defaultString += '0'; 73 | maxDistanceStr += '9'; 74 | } 75 | 76 | this.maxScore = parseInt(maxDistanceStr, 0); 77 | } 78 | 79 | /** 80 | * Calculate the xPos in the canvas. 81 | * @param {number} canvasWidth 82 | */ 83 | calcXPos(canvasWidth) { 84 | this.x = 85 | canvasWidth - 86 | DistanceMeter.dimensions.DEST_WIDTH * (this.maxScoreUnits + 1); 87 | } 88 | 89 | /** 90 | * Draw a digit to canvas. 91 | * @param {number} digitPos Position of the digit. 92 | * @param {number} value Digit value 0-9. 93 | * @param {boolean} highScore Whether drawing the high score. 94 | */ 95 | draw(digitPos, value, highScore) { 96 | const sourceWidth = DistanceMeter.dimensions.WIDTH; 97 | const sourceHeight = DistanceMeter.dimensions.HEIGHT; 98 | let sourceX = DistanceMeter.dimensions.WIDTH * value; 99 | let sourceY = 0; 100 | 101 | const targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH; 102 | const targetY = this.y; 103 | const targetWidth = DistanceMeter.dimensions.WIDTH; 104 | const targetHeight = DistanceMeter.dimensions.HEIGHT; 105 | 106 | sourceX += this.spritePos.x; 107 | sourceY += this.spritePos.y; 108 | 109 | this.canvasCtx.save(); 110 | 111 | if (highScore) { 112 | // Left of the current score. 113 | const highScoreX = 114 | this.x - this.maxScoreUnits * 2 * DistanceMeter.dimensions.WIDTH; 115 | this.canvasCtx.translate(highScoreX, this.y); 116 | } else { 117 | this.canvasCtx.translate(this.x, this.y); 118 | } 119 | 120 | this.canvasCtx.drawImage( 121 | this.image, 122 | sourceX, 123 | sourceY, 124 | sourceWidth, 125 | sourceHeight, 126 | targetX, 127 | targetY, 128 | targetWidth, 129 | targetHeight 130 | ); 131 | 132 | this.canvasCtx.restore(); 133 | } 134 | 135 | /** 136 | * Covert pixel distance to a 'real' distance. 137 | * @param {number} distance Pixel distance ran. 138 | * @return {number} The 'real' distance ran. 139 | */ 140 | getActualDistance(distance) { 141 | return distance ? Math.round(distance * this.config.COEFFICIENT) : 0; 142 | } 143 | 144 | /** 145 | * Update the distance meter. 146 | * @param {number} distance 147 | * @param {number} deltaTime 148 | */ 149 | update(deltaTime, distance) { 150 | let paint = true; 151 | 152 | if (!this.acheivement) { 153 | distance = this.getActualDistance(distance); 154 | // Score has gone beyond the initial digit count. 155 | if ( 156 | distance > this.maxScore && 157 | this.maxScoreUnits === this.config.MAX_DISTANCE_UNITS 158 | ) { 159 | this.maxScoreUnits += 1; 160 | this.maxScore = parseInt(`${this.maxScore}9`, 1); 161 | } else { 162 | this.distance = 0; 163 | } 164 | 165 | if (distance > 0) { 166 | // Acheivement unlocked 167 | if (distance % this.config.ACHIEVEMENT_DISTANCE === 0) { 168 | this.acheivement = true; 169 | this.flashTimer = 0; 170 | } 171 | 172 | // Create a string representation of the distance with leading 0. 173 | const distanceStr = (this.defaultString + distance).substr( 174 | -this.maxScoreUnits 175 | ); 176 | this.digits = distanceStr.split(''); 177 | } else { 178 | this.digits = this.defaultString.split(''); 179 | } 180 | } else if (this.flashIterations <= this.config.FLASH_ITERATIONS) { 181 | this.flashTimer += deltaTime; 182 | 183 | if (this.flashTimer < this.config.FLASH_DURATION) { 184 | paint = false; 185 | } else if (this.flashTimer > this.config.FLASH_DURATION * 2) { 186 | this.flashTimer = 0; 187 | this.flashIterations += 1; 188 | } 189 | } else { 190 | this.acheivement = false; 191 | this.flashIterations = 0; 192 | this.flashTimer = 0; 193 | } 194 | 195 | // Draw the digits if not flashing. 196 | if (paint) { 197 | for (let i = this.digits.length - 1; i >= 0; i -= 1) { 198 | this.draw(i, parseInt(this.digits[i], 0)); 199 | } 200 | } 201 | 202 | this.drawHighScore(); 203 | } 204 | /** 205 | * Draw the high score. 206 | */ 207 | drawHighScore() { 208 | this.canvasCtx.save(); 209 | this.canvasCtx.globalAlpha = 0.8; 210 | for (let i = this.highScore.length - 1; i >= 0; i -= 1) { 211 | this.draw(i, parseInt(this.highScore[i], 10), true); 212 | } 213 | this.canvasCtx.restore(); 214 | } 215 | 216 | /** 217 | * Set the highscore as a array string. 218 | * Position of char in the sprite: H - 10, I - 11. 219 | * @param {number} distance Distance ran in pixels. 220 | */ 221 | setHighScore(distance) { 222 | distance = this.getActualDistance(distance); 223 | const highScoreStr = (this.defaultString + distance).substr( 224 | -this.maxScoreUnits 225 | ); 226 | 227 | this.highScore = ['10', '11', ''].concat(highScoreStr.split('')); 228 | } 229 | 230 | /** 231 | * Reset the distance meter back to '00000'. 232 | */ 233 | reset() { 234 | this.update(0); 235 | this.acheivement = false; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /part2/src/game/Horizon.js: -------------------------------------------------------------------------------- 1 | import { getRandomNum } from './utils'; 2 | import { RUNNER_MAX_OBSTACLE_DUPLICATION } from './constants'; 3 | import Cloud from './Cloud'; 4 | import HorizonLine from './HorizonLine'; 5 | import Obstacle from './Obstacle'; 6 | 7 | /** 8 | * Horizon background class. 9 | * @param {HTMLCanvasElement} canvas 10 | * @param {Object} spritePos Sprite positioning. 11 | * @param {Object} dimensions Canvas dimensions. 12 | * @param {number} gapCoefficient 13 | * @constructor 14 | */ 15 | export default class Horizon { 16 | /** 17 | * Horizon config. 18 | * @enum {number} 19 | */ 20 | static config = { 21 | BG_CLOUD_SPEED: 0.2, 22 | BUMPY_THRESHOLD: 0.3, 23 | CLOUD_FREQUENCY: 0.5, 24 | HORIZON_HEIGHT: 16, 25 | MAX_CLOUDS: 6 26 | }; 27 | 28 | constructor(canvas, spritePos, dimensions, gapCoefficient) { 29 | this.canvas = canvas; 30 | this.canvasCtx = this.canvas.getContext('2d'); 31 | this.config = Horizon.config; 32 | this.dimensions = dimensions; 33 | this.gapCoefficient = gapCoefficient; 34 | this.obstacles = []; 35 | this.obstacleHistory = []; 36 | this.horizonOffsets = [0, 0]; 37 | this.cloudFrequency = this.config.CLOUD_FREQUENCY; 38 | this.spritePos = spritePos; 39 | 40 | // Cloud 41 | this.clouds = []; 42 | this.cloudSpeed = this.config.BG_CLOUD_SPEED; 43 | 44 | // Horizon 45 | this.horizonLine = null; 46 | this.init(); 47 | } 48 | 49 | /** 50 | * Initialise the horizon. Just add the line and a cloud. No obstacles. 51 | */ 52 | init() { 53 | this.addCloud(); 54 | this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON); 55 | } 56 | 57 | /** 58 | * @param {number} deltaTime 59 | * @param {number} currentSpeed 60 | * @param {boolean} updateObstacles Used as an override to prevent 61 | * the obstacles from being updated / added. This happens in the 62 | * ease in section. 63 | */ 64 | update(deltaTime, currentSpeed, updateObstacles) { 65 | this.runningTime += deltaTime; 66 | this.horizonLine.update(deltaTime, currentSpeed); 67 | this.updateClouds(deltaTime, currentSpeed); 68 | 69 | if (updateObstacles) { 70 | this.updateObstacles(deltaTime, currentSpeed); 71 | } 72 | } 73 | 74 | /** 75 | * Update the cloud positions. 76 | * @param {number} deltaTime 77 | * @param {number} currentSpeed 78 | */ 79 | updateClouds(deltaTime, speed) { 80 | const cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed; 81 | const numClouds = this.clouds.length; 82 | 83 | if (numClouds) { 84 | for (let i = numClouds - 1; i >= 0; i -= 1) { 85 | this.clouds[i].update(cloudSpeed); 86 | } 87 | 88 | const lastCloud = this.clouds[numClouds - 1]; 89 | 90 | // Check for adding a new cloud. 91 | if ( 92 | numClouds < this.config.MAX_CLOUDS && 93 | this.dimensions.WIDTH - lastCloud.xPos > lastCloud.cloudGap && 94 | this.cloudFrequency > Math.random() 95 | ) { 96 | this.addCloud(); 97 | } 98 | 99 | // Remove expired clouds. 100 | this.clouds = this.clouds.filter(obj => !obj.remove); 101 | } else { 102 | this.addCloud(); 103 | } 104 | } 105 | 106 | /** 107 | * Update the obstacle positions. 108 | * @param {number} deltaTime 109 | * @param {number} currentSpeed 110 | */ 111 | updateObstacles(deltaTime, currentSpeed) { 112 | // Obstacles, move to Horizon layer. 113 | const updatedObstacles = this.obstacles.slice(0); 114 | 115 | for (let i = 0; i < this.obstacles.length; i += 1) { 116 | const obstacle = this.obstacles[i]; 117 | obstacle.update(deltaTime, currentSpeed); 118 | 119 | // Clean up existing obstacles. 120 | if (obstacle.remove) { 121 | updatedObstacles.shift(); 122 | } 123 | } 124 | this.obstacles = updatedObstacles; 125 | 126 | if (this.obstacles.length > 0) { 127 | const lastObstacle = this.obstacles[this.obstacles.length - 1]; 128 | 129 | if ( 130 | lastObstacle && 131 | !lastObstacle.followingObstacleCreated && 132 | lastObstacle.isVisible() && 133 | lastObstacle.xPos + lastObstacle.width + lastObstacle.gap < 134 | this.dimensions.WIDTH 135 | ) { 136 | this.addNewObstacle(currentSpeed); 137 | lastObstacle.followingObstacleCreated = true; 138 | } 139 | } else { 140 | // Create new obstacles. 141 | this.addNewObstacle(currentSpeed); 142 | } 143 | } 144 | 145 | removeFirstObstacle() { 146 | this.obstacles.shift(); 147 | } 148 | 149 | /** 150 | * Add a new obstacle. 151 | * @param {number} currentSpeed 152 | */ 153 | addNewObstacle(currentSpeed) { 154 | const obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1); 155 | const obstacleType = Obstacle.types[obstacleTypeIndex]; 156 | 157 | // Check for multiples of the same type of obstacle. 158 | // Also check obstacle is available at current speed. 159 | if ( 160 | this.duplicateObstacleCheck(obstacleType.type) || 161 | currentSpeed < obstacleType.minSpeed 162 | ) { 163 | this.addNewObstacle(currentSpeed); 164 | } else { 165 | const obstacleSpritePos = this.spritePos[obstacleType.type]; 166 | 167 | this.obstacles.push( 168 | new Obstacle( 169 | this.canvasCtx, 170 | obstacleType, 171 | obstacleSpritePos, 172 | this.dimensions, 173 | this.gapCoefficient, 174 | currentSpeed, 175 | obstacleType.width 176 | ) 177 | ); 178 | 179 | this.obstacleHistory.unshift(obstacleType.type); 180 | 181 | if (this.obstacleHistory.length > 1) { 182 | this.obstacleHistory.splice(RUNNER_MAX_OBSTACLE_DUPLICATION); 183 | } 184 | } 185 | } 186 | 187 | /** 188 | * Returns whether the previous two obstacles are the same as the next one. 189 | * Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION. 190 | * @return {boolean} 191 | */ 192 | duplicateObstacleCheck(nextObstacleType) { 193 | let duplicateCount = 0; 194 | 195 | for (let i = 0; i < this.obstacleHistory.length; i += 1) { 196 | duplicateCount = 197 | this.obstacleHistory[i] === nextObstacleType ? duplicateCount + 1 : 0; 198 | } 199 | return duplicateCount >= RUNNER_MAX_OBSTACLE_DUPLICATION; 200 | } 201 | 202 | /** 203 | * Reset the horizon layer. 204 | * Remove existing obstacles and reposition the horizon line. 205 | */ 206 | reset() { 207 | this.obstacles = []; 208 | this.horizonLine.reset(); 209 | } 210 | 211 | /** 212 | * Update the canvas width and scaling. 213 | * @param {number} width Canvas width. 214 | * @param {number} height Canvas height. 215 | */ 216 | resize(width, height) { 217 | this.canvas.width = width; 218 | this.canvas.height = height; 219 | } 220 | 221 | /** 222 | * Add a new cloud to the horizon. 223 | */ 224 | addCloud() { 225 | this.clouds.push( 226 | new Cloud(this.canvas, this.spritePos.CLOUD, this.dimensions.WIDTH) 227 | ); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /part2/src/game/HorizonLine.js: -------------------------------------------------------------------------------- 1 | import { getFPS } from './RuntimeConfig'; 2 | import { getImageSprite } from './ImageSprite'; 3 | 4 | /** 5 | * Horizon Line. 6 | * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon. 7 | * @param {HTMLCanvasElement} canvas 8 | * @param {Object} spritePos Horizon position in sprite. 9 | * @constructor 10 | */ 11 | export default class HorizonLine { 12 | /** 13 | * Horizon line dimensions. 14 | * @enum {number} 15 | */ 16 | static dimensions = { 17 | WIDTH: 600, 18 | HEIGHT: 12, 19 | YPOS: 127 20 | }; 21 | 22 | constructor(canvas, spritePos) { 23 | this.spritePos = spritePos; 24 | this.canvas = canvas; 25 | this.canvasCtx = canvas.getContext('2d'); 26 | this.sourceDimensions = {}; 27 | this.dimensions = HorizonLine.dimensions; 28 | this.sourceXPos = [ 29 | this.spritePos.x, 30 | this.spritePos.x + this.dimensions.WIDTH 31 | ]; 32 | this.xPos = []; 33 | this.yPos = 0; 34 | this.bumpThreshold = 0.5; 35 | 36 | this.setSourceDimensions(); 37 | this.draw(); 38 | } 39 | 40 | /** 41 | * Set the source dimensions of the horizon line. 42 | */ 43 | setSourceDimensions() { 44 | /* eslint-disable-next-line */ 45 | for (const dimension in HorizonLine.dimensions) { 46 | this.sourceDimensions[dimension] = HorizonLine.dimensions[dimension]; 47 | this.dimensions[dimension] = HorizonLine.dimensions[dimension]; 48 | } 49 | 50 | this.xPos = [0, HorizonLine.dimensions.WIDTH]; 51 | this.yPos = HorizonLine.dimensions.YPOS; 52 | } 53 | 54 | /** 55 | * Return the crop x position of a type. 56 | */ 57 | getRandomType() { 58 | return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0; 59 | } 60 | 61 | /** 62 | * Draw the horizon line. 63 | */ 64 | draw() { 65 | this.canvasCtx.drawImage( 66 | getImageSprite(), 67 | this.sourceXPos[0], 68 | this.spritePos.y, 69 | this.sourceDimensions.WIDTH, 70 | this.sourceDimensions.HEIGHT, 71 | this.xPos[0], 72 | this.yPos, 73 | this.dimensions.WIDTH, 74 | this.dimensions.HEIGHT 75 | ); 76 | 77 | this.canvasCtx.drawImage( 78 | getImageSprite(), 79 | this.sourceXPos[1], 80 | this.spritePos.y, 81 | this.sourceDimensions.WIDTH, 82 | this.sourceDimensions.HEIGHT, 83 | this.xPos[1], 84 | this.yPos, 85 | this.dimensions.WIDTH, 86 | this.dimensions.HEIGHT 87 | ); 88 | } 89 | 90 | /** 91 | * Update the x position of an indivdual piece of the line. 92 | * @param {number} pos Line position. 93 | * @param {number} increment 94 | */ 95 | updateXPos(pos, increment) { 96 | const line1 = pos; 97 | const line2 = pos === 0 ? 1 : 0; 98 | 99 | this.xPos[line1] -= increment; 100 | this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH; 101 | 102 | if (this.xPos[line1] <= -this.dimensions.WIDTH) { 103 | this.xPos[line1] += this.dimensions.WIDTH * 2; 104 | this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH; 105 | this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x; 106 | } 107 | } 108 | 109 | /** 110 | * Update the horizon line. 111 | * @param {number} deltaTime 112 | * @param {number} speed 113 | */ 114 | update(deltaTime, speed) { 115 | const increment = Math.floor(speed * (getFPS() / 1000) * deltaTime); 116 | 117 | if (this.xPos[0] <= 0) { 118 | this.updateXPos(0, increment); 119 | } else { 120 | this.updateXPos(1, increment); 121 | } 122 | this.draw(); 123 | } 124 | 125 | /** 126 | * Reset horizon to the starting position. 127 | */ 128 | reset() { 129 | this.xPos[0] = 0; 130 | this.xPos[1] = HorizonLine.dimensions.WIDTH; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /part2/src/game/ImageSprite.js: -------------------------------------------------------------------------------- 1 | let _imageSprite = null; 2 | 3 | export function getImageSprite() { 4 | return _imageSprite; 5 | } 6 | 7 | export function loadImageSprite() { 8 | return new Promise((resolve) => { 9 | const imageSprite = document.createElement('img'); 10 | imageSprite.src = require('./images/offline-sprite.png'); 11 | imageSprite.addEventListener('load', () => { 12 | _imageSprite = imageSprite; 13 | resolve(); 14 | }); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /part2/src/game/Obstacle.js: -------------------------------------------------------------------------------- 1 | import { getFPS } from './RuntimeConfig'; 2 | import { getImageSprite } from './ImageSprite'; 3 | import { getRandomNum } from './utils'; 4 | import CollisionBox from './CollisionBox'; 5 | 6 | /** 7 | * Obstacle. 8 | * @param {HTMLCanvasCtx} canvasCtx 9 | * @param {Obstacle.type} type 10 | * @param {Object} spritePos Obstacle position in sprite. 11 | * @param {Object} dimensions 12 | * @param {number} gapCoefficient Mutipler in determining the gap. 13 | * @param {number} speed 14 | * @param {number} offset 15 | */ 16 | export default class Obstacle { 17 | /** 18 | * Coefficient for calculating the maximum gap. 19 | * @const 20 | */ 21 | static MAX_GAP_COEFFICIENT = 1.5; 22 | 23 | /** 24 | * Maximum obstacle grouping count. 25 | * @const 26 | */ 27 | static MAX_OBSTACLE_LENGTH = 3; 28 | 29 | static types = [ 30 | { 31 | type: 'CACTUS_SMALL', 32 | width: 17, 33 | height: 35, 34 | yPos: 105, 35 | multipleSpeed: 4, 36 | minGap: 120, 37 | minSpeed: 0, 38 | collisionBoxes: [ 39 | new CollisionBox(0, 7, 5, 27), 40 | new CollisionBox(4, 0, 6, 34), 41 | new CollisionBox(10, 4, 7, 14) 42 | ] 43 | }, 44 | { 45 | type: 'CACTUS_LARGE', 46 | width: 25, 47 | height: 50, 48 | yPos: 90, 49 | multipleSpeed: 7, 50 | minGap: 120, 51 | minSpeed: 0, 52 | collisionBoxes: [ 53 | new CollisionBox(0, 12, 7, 38), 54 | new CollisionBox(8, 0, 7, 49), 55 | new CollisionBox(13, 10, 10, 38) 56 | ] 57 | }, 58 | { 59 | type: 'PTERODACTYL', 60 | width: 46, 61 | height: 40, 62 | yPos: [100, 75, 50], // Variable height. 63 | yPosMobile: [100, 50], // Variable height mobile. 64 | multipleSpeed: 999, 65 | minSpeed: 8.5, 66 | minGap: 150, 67 | collisionBoxes: [ 68 | new CollisionBox(15, 15, 16, 5), 69 | new CollisionBox(18, 21, 24, 6), 70 | new CollisionBox(2, 14, 4, 3), 71 | new CollisionBox(6, 10, 4, 7), 72 | new CollisionBox(10, 8, 6, 9) 73 | ], 74 | numFrames: 2, 75 | frameRate: 1000 / 6, 76 | speedOffset: 0.8 77 | } 78 | ]; 79 | 80 | constructor( 81 | canvasCtx, 82 | type, 83 | spriteImgPos, 84 | dimensions, 85 | gapCoefficient, 86 | speed, 87 | offset 88 | ) { 89 | this.canvasCtx = canvasCtx; 90 | this.spritePos = spriteImgPos; 91 | this.typeConfig = type; 92 | this.gapCoefficient = gapCoefficient; 93 | this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH); 94 | this.dimensions = dimensions; 95 | this.remove = false; 96 | this.xPos = dimensions.WIDTH + (offset || 0); 97 | this.yPos = 0; 98 | this.width = 0; 99 | this.collisionBoxes = []; 100 | this.gap = 0; 101 | this.speedOffset = 0; 102 | 103 | // For animated obstacles. 104 | this.currentFrame = 0; 105 | this.timer = 0; 106 | 107 | this.init(speed); 108 | } 109 | 110 | init(speed) { 111 | this.cloneCollisionBoxes(); 112 | 113 | // Only allow sizing if we're at the right speed. 114 | if (this.size > 1 && this.typeConfig.multipleSpeed > speed) { 115 | this.size = 1; 116 | } 117 | 118 | this.width = this.typeConfig.width * this.size; 119 | 120 | // Check if obstacle can be positioned at various heights. 121 | if (Array.isArray(this.typeConfig.yPos)) { 122 | const yPosConfig = this.typeConfig.yPos; 123 | this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)]; 124 | } else { 125 | this.yPos = this.typeConfig.yPos; 126 | } 127 | 128 | this.draw(); 129 | 130 | // Make collision box adjustments, 131 | // Central box is adjusted to the size as one box. 132 | // ____ ______ ________ 133 | // _| |-| _| |-| _| |-| 134 | // | |<->| | | |<--->| | | |<----->| | 135 | // | | 1 | | | | 2 | | | | 3 | | 136 | // |_|___|_| |_|_____|_| |_|_______|_| 137 | // 138 | if (this.size > 1) { 139 | this.collisionBoxes[1].width = 140 | this.width - 141 | this.collisionBoxes[0].width - 142 | this.collisionBoxes[2].width; 143 | this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width; 144 | } 145 | 146 | // For obstacles that go at a different speed from the horizon. 147 | if (this.typeConfig.speedOffset) { 148 | this.speedOffset = 149 | Math.random() > 0.5 150 | ? this.typeConfig.speedOffset 151 | : -this.typeConfig.speedOffset; 152 | } 153 | 154 | this.gap = this.getGap(this.gapCoefficient, speed); 155 | } 156 | 157 | /** 158 | * Draw and crop based on size. 159 | */ 160 | draw() { 161 | const sourceWidth = this.typeConfig.width; 162 | const sourceHeight = this.typeConfig.height; 163 | 164 | // X position in sprite. 165 | let sourceX = 166 | sourceWidth * this.size * (0.5 * (this.size - 1)) + this.spritePos.x; 167 | 168 | // Animation frames. 169 | if (this.currentFrame > 0) { 170 | sourceX += sourceWidth * this.currentFrame; 171 | } 172 | 173 | this.canvasCtx.drawImage( 174 | getImageSprite(), 175 | sourceX, 176 | this.spritePos.y, 177 | sourceWidth * this.size, 178 | sourceHeight, 179 | this.xPos, 180 | this.yPos, 181 | this.typeConfig.width * this.size, 182 | this.typeConfig.height 183 | ); 184 | } 185 | 186 | /** 187 | * Obstacle frame update. 188 | * @param {number} deltaTime 189 | * @param {number} speed 190 | */ 191 | update(deltaTime, speed) { 192 | if (!this.remove) { 193 | if (this.typeConfig.speedOffset) { 194 | speed += this.speedOffset; 195 | } 196 | this.xPos -= Math.floor(speed * getFPS() / 1000 * deltaTime); 197 | 198 | // Update frame 199 | if (this.typeConfig.numFrames) { 200 | this.timer += deltaTime; 201 | if (this.timer >= this.typeConfig.frameRate) { 202 | this.currentFrame = 203 | this.currentFrame === this.typeConfig.numFrames - 1 204 | ? 0 205 | : this.currentFrame + 1; 206 | this.timer = 0; 207 | } 208 | } 209 | this.draw(); 210 | 211 | if (!this.isVisible()) { 212 | this.remove = true; 213 | } 214 | } 215 | } 216 | 217 | /** 218 | * Calculate a random gap size. 219 | * - Minimum gap gets wider as speed increses 220 | * @param {number} gapCoefficient 221 | * @param {number} speed 222 | * @return {number} The gap size. 223 | */ 224 | getGap(gapCoefficient, speed) { 225 | const minGap = Math.round( 226 | this.width * speed + this.typeConfig.minGap * gapCoefficient 227 | ); 228 | const maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT); 229 | return getRandomNum(minGap, maxGap); 230 | } 231 | 232 | /** 233 | * Check if obstacle is visible. 234 | * @return {boolean} Whether the obstacle is in the game area. 235 | */ 236 | isVisible() { 237 | return this.xPos + this.width > 0; 238 | } 239 | 240 | /** 241 | * Make a copy of the collision boxes, since these will change based on 242 | * obstacle type and size. 243 | */ 244 | cloneCollisionBoxes() { 245 | const { collisionBoxes } = this.typeConfig; 246 | 247 | for (let i = collisionBoxes.length - 1; i >= 0; i -= 1) { 248 | this.collisionBoxes[i] = new CollisionBox( 249 | collisionBoxes[i].x, 250 | collisionBoxes[i].y, 251 | collisionBoxes[i].width, 252 | collisionBoxes[i].height 253 | ); 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /part2/src/game/Runner.js: -------------------------------------------------------------------------------- 1 | import { CANVAS_WIDTH, CANVAS_HEIGHT } from './constants'; 2 | import { getFPS } from './RuntimeConfig'; 3 | import { getImageSprite, setImageSprite, loadImageSprite } from './ImageSprite'; 4 | import { getTimeStamp } from './utils'; 5 | import DistanceMeter from './DistanceMeter'; 6 | import Horizon from './Horizon'; 7 | import Trex, { checkForCollision } from './Trex'; 8 | import TrexGroup from './TrexGroup'; 9 | 10 | /** 11 | * T-Rex runner. 12 | * @param {string} outerContainerId Outer containing element id. 13 | * @param {Object} options 14 | * @constructor 15 | * @export 16 | */ 17 | export default class Runner { 18 | static generation = 0; 19 | 20 | static config = { 21 | ACCELERATION: 0.001, 22 | BG_CLOUD_SPEED: 0.2, 23 | CLEAR_TIME: 0, 24 | CLOUD_FREQUENCY: 0.5, 25 | GAP_COEFFICIENT: 0.6, 26 | GRAVITY: 0.6, 27 | INITIAL_JUMP_VELOCITY: 12, 28 | MAX_CLOUDS: 6, 29 | MAX_OBSTACLE_LENGTH: 3, 30 | MAX_SPEED: 13, 31 | MIN_JUMP_HEIGHT: 35, 32 | SPEED: 6, 33 | SPEED_DROP_COEFFICIENT: 3, 34 | DINO_COUNT: 1, 35 | onReset: noop, 36 | onRunning: noop, 37 | onCrash: noop 38 | }; 39 | 40 | static classes = { 41 | CANVAS: 'game-canvas', 42 | CONTAINER: 'game-container', 43 | }; 44 | 45 | static spriteDefinition = { 46 | CACTUS_LARGE: { x: 332, y: 2 }, 47 | CACTUS_SMALL: { x: 228, y: 2 }, 48 | CLOUD: { x: 86, y: 2 }, 49 | HORIZON: { x: 2, y: 54 }, 50 | PTERODACTYL: { x: 134, y: 2 }, 51 | RESTART: { x: 2, y: 2 }, 52 | TEXT_SPRITE: { x: 655, y: 2 }, 53 | TREX: { x: 848, y: 2 } 54 | }; 55 | 56 | /** 57 | * Key code mapping. 58 | * @enum {Object} 59 | */ 60 | static keycodes = { 61 | JUMP: { 38: 1, 32: 1 }, // Up, spacebar 62 | DUCK: { 40: 1 } // Down 63 | }; 64 | 65 | /** 66 | * Runner event names. 67 | * @enum {string} 68 | */ 69 | static events = { 70 | ANIM_END: 'webkitAnimationEnd', 71 | CLICK: 'click', 72 | KEYDOWN: 'keydown', 73 | KEYUP: 'keyup', 74 | RESIZE: 'resize', 75 | VISIBILITY: 'visibilitychange', 76 | BLUR: 'blur', 77 | FOCUS: 'focus', 78 | LOAD: 'load' 79 | }; 80 | 81 | constructor(outerContainerId, options) { 82 | // Singleton 83 | if (Runner.instance_) { 84 | return Runner.instance_; 85 | } 86 | Runner.instance_ = this; 87 | 88 | this.isFirstTime = false; 89 | this.outerContainerEl = document.querySelector(outerContainerId); 90 | this.generationEl = document.querySelector('.generation'); 91 | this.containerEl = null; 92 | 93 | this.config = Object.assign({}, Runner.config, options); 94 | 95 | this.dimensions = { 96 | WIDTH: CANVAS_WIDTH, 97 | HEIGHT: CANVAS_HEIGHT 98 | }; 99 | 100 | this.canvas = null; 101 | this.canvasCtx = null; 102 | 103 | this.tRex = null; 104 | 105 | this.distanceMeter = null; 106 | this.distanceRan = 0; 107 | 108 | this.highestScore = 0; 109 | 110 | this.time = 0; 111 | this.runningTime = 0; 112 | this.msPerFrame = 1000 / getFPS(); 113 | this.currentSpeed = this.config.SPEED; 114 | 115 | this.obstacles = []; 116 | 117 | this.activated = false; // Whether the easter egg has been activated. 118 | this.playing = false; // Whether the game is currently in play state. 119 | this.crashed = false; 120 | this.resizeTimerId_ = null; 121 | 122 | this.playCount = 0; 123 | 124 | // Images. 125 | this.images = {}; 126 | this.imagesLoaded = 0; 127 | } 128 | 129 | async init() { 130 | await loadImageSprite(); 131 | this.spriteDef = Runner.spriteDefinition; 132 | 133 | this.adjustDimensions(); 134 | this.setSpeed(); 135 | 136 | this.containerEl = document.createElement('div'); 137 | this.containerEl.className = Runner.classes.CONTAINER; 138 | this.containerEl.style.width = `${this.dimensions.WIDTH}px`; 139 | 140 | // Player canvas container. 141 | this.canvas = createCanvas( 142 | this.containerEl, 143 | this.dimensions.WIDTH, 144 | this.dimensions.HEIGHT, 145 | Runner.classes.PLAYER 146 | ); 147 | 148 | this.canvasCtx = this.canvas.getContext('2d'); 149 | this.canvasCtx.fillStyle = '#f7f7f7'; 150 | this.canvasCtx.fill(); 151 | Runner.updateCanvasScaling(this.canvas); 152 | 153 | // Horizon contains clouds, obstacles and the ground. 154 | this.horizon = new Horizon( 155 | this.canvas, 156 | this.spriteDef, 157 | this.dimensions, 158 | this.config.GAP_COEFFICIENT 159 | ); 160 | 161 | // Distance meter 162 | this.distanceMeter = new DistanceMeter( 163 | this.canvas, 164 | this.spriteDef.TEXT_SPRITE, 165 | this.dimensions.WIDTH 166 | ); 167 | 168 | // Draw t-rex 169 | this.tRexGroup = new TrexGroup(this.config.DINO_COUNT, this.canvas, this.spriteDef.TREX); 170 | this.tRexGroup.onRunning = this.config.onRunning; 171 | this.tRexGroup.onCrash = this.config.onCrash; 172 | this.tRex = this.tRexGroup.tRexes[0]; 173 | 174 | this.outerContainerEl.appendChild(this.containerEl); 175 | 176 | this.startListening(); 177 | this.update(); 178 | 179 | window.addEventListener( 180 | Runner.events.RESIZE, 181 | this.debounceResize.bind(this) 182 | ); 183 | 184 | this.restart(); 185 | } 186 | 187 | /** 188 | * Debounce the resize event. 189 | */ 190 | debounceResize() { 191 | if (!this.resizeTimerId_) { 192 | this.resizeTimerId_ = setInterval(this.adjustDimensions.bind(this), 250); 193 | } 194 | } 195 | 196 | /** 197 | * Adjust game space dimensions on resize. 198 | */ 199 | adjustDimensions() { 200 | clearInterval(this.resizeTimerId_); 201 | this.resizeTimerId_ = null; 202 | 203 | const boxStyles = window.getComputedStyle(this.outerContainerEl); 204 | const padding = Number( 205 | boxStyles.paddingLeft.substr(0, boxStyles.paddingLeft.length - 2) 206 | ); 207 | 208 | this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2; 209 | 210 | // Redraw the elements back onto the canvas. 211 | if (this.canvas) { 212 | this.canvas.width = this.dimensions.WIDTH; 213 | this.canvas.height = this.dimensions.HEIGHT; 214 | 215 | Runner.updateCanvasScaling(this.canvas); 216 | 217 | this.distanceMeter.calcXPos(this.dimensions.WIDTH); 218 | this.clearCanvas(); 219 | this.horizon.update(0, 0, true); 220 | this.tRexGroup.update(0); 221 | 222 | // Outer container and distance meter. 223 | if (this.playing || this.crashed) { 224 | this.containerEl.style.width = `${this.dimensions.WIDTH}px`; 225 | this.containerEl.style.height = `${this.dimensions.HEIGHT}px`; 226 | this.distanceMeter.update(0, Math.ceil(this.distanceRan)); 227 | this.stop(); 228 | } else { 229 | this.tRexGroup.draw(0, 0); 230 | } 231 | } 232 | } 233 | 234 | /** 235 | * Sets the game speed. Adjust the speed accordingly if on a smaller screen. 236 | * @param {number} speed 237 | */ 238 | setSpeed(speed) { 239 | this.currentSpeed = speed || this.currentSpeed; 240 | } 241 | 242 | /** 243 | * Update the game status to started. 244 | */ 245 | startGame() { 246 | this.runningTime = 0; 247 | this.containerEl.style.webkitAnimation = ''; 248 | this.playCount += 1; 249 | } 250 | 251 | clearCanvas() { 252 | this.canvasCtx.clearRect( 253 | 0, 254 | 0, 255 | this.dimensions.WIDTH, 256 | this.dimensions.HEIGHT 257 | ); 258 | } 259 | 260 | /** 261 | * Update the game frame and schedules the next one. 262 | */ 263 | update() { 264 | this.updatePending = false; 265 | 266 | const now = getTimeStamp(); 267 | let deltaTime = now - (this.time || now); 268 | this.time = now; 269 | 270 | if (this.playing) { 271 | this.clearCanvas(); 272 | 273 | this.tRexGroup.updateJump(deltaTime); 274 | 275 | this.runningTime += deltaTime; 276 | const hasObstacles = this.runningTime > this.config.CLEAR_TIME; 277 | 278 | // First time 279 | if (this.isFirstTime) { 280 | if (!this.activated && !this.crashed) { 281 | this.playing = true; 282 | this.activated = true; 283 | this.startGame(); 284 | } 285 | } 286 | 287 | deltaTime = !this.activated ? 0 : deltaTime; 288 | this.horizon.update(deltaTime, this.currentSpeed, hasObstacles); 289 | 290 | let gameOver = false; 291 | // Check for collisions. 292 | if (hasObstacles) { 293 | gameOver = this.tRexGroup.checkForCollision(this.horizon.obstacles[0]); 294 | } 295 | 296 | if (!gameOver) { 297 | this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame; 298 | 299 | if (this.currentSpeed < this.config.MAX_SPEED) { 300 | this.currentSpeed += this.config.ACCELERATION; 301 | } 302 | } else { 303 | this.gameOver(); 304 | } 305 | 306 | this.distanceMeter.update( 307 | deltaTime, 308 | Math.ceil(this.distanceRan) 309 | ); 310 | } 311 | 312 | if ( 313 | this.playing || 314 | (!this.activated) 315 | ) { 316 | this.tRexGroup.update(deltaTime); 317 | this.scheduleNextUpdate(); 318 | } 319 | 320 | const lives = this.tRexGroup.lives(); 321 | if (lives > 0) { 322 | this.generationEl.innerText = `GENERATION #${Runner.generation} | LIVE x ${this.tRexGroup.lives()}`; 323 | } else { 324 | this.generationEl.innerHTML = `
GENERATION #${Runner.generation} | GAME OVER
`; 325 | } 326 | } 327 | 328 | /** 329 | * Bind relevant key 330 | */ 331 | startListening() { 332 | document.addEventListener(Runner.events.KEYDOWN, (e) => { 333 | this.onKeyDown(e); 334 | }); 335 | document.addEventListener(Runner.events.KEYUP, (e) => { 336 | this.onKeyUp(e); 337 | }); 338 | } 339 | 340 | /** 341 | * Process keydown. 342 | * @param {Event} e 343 | */ 344 | onKeyDown(e) { 345 | if (!this.crashed && this.playing) { 346 | if (Runner.keycodes.JUMP[e.keyCode]) { 347 | e.preventDefault(); 348 | this.tRex.startJump(this.currentSpeed); 349 | } else if (Runner.keycodes.DUCK[e.keyCode]) { 350 | e.preventDefault(); 351 | if (this.tRex.jumping) { 352 | // Speed drop, activated only when jump key is not pressed. 353 | this.tRex.setSpeedDrop(); 354 | } else if (!this.tRex.jumping && !this.tRex.ducking) { 355 | // Duck. 356 | this.tRex.setDuck(true); 357 | } 358 | } 359 | } else if (this.crashed) { 360 | this.restart(); 361 | } 362 | } 363 | 364 | /** 365 | * Process key up. 366 | * @param {Event} e 367 | */ 368 | onKeyUp(e) { 369 | const keyCode = String(e.keyCode); 370 | const isJumpKey = Runner.keycodes.JUMP[keyCode]; 371 | 372 | if (this.isRunning() && isJumpKey) { 373 | this.tRex.endJump(); 374 | } else if (Runner.keycodes.DUCK[keyCode]) { 375 | this.tRex.speedDrop = false; 376 | this.tRex.setDuck(false); 377 | } else if (this.crashed) { 378 | if (Runner.keycodes.JUMP[keyCode]) { 379 | this.restart(); 380 | } 381 | } 382 | } 383 | 384 | /** 385 | * RequestAnimationFrame wrapper. 386 | */ 387 | scheduleNextUpdate() { 388 | if (!this.updatePending) { 389 | this.updatePending = true; 390 | this.raqId = requestAnimationFrame(this.update.bind(this)); 391 | } 392 | } 393 | 394 | /** 395 | * Whether the game is running. 396 | * @return {boolean} 397 | */ 398 | isRunning() { 399 | return !!this.raqId; 400 | } 401 | 402 | /** 403 | * Game over state. 404 | */ 405 | gameOver() { 406 | this.stop(); 407 | this.crashed = true; 408 | this.distanceMeter.acheivement = false; 409 | 410 | this.tRexGroup.update(100, Trex.status.CRASHED); 411 | 412 | // Update the high score. 413 | if (this.distanceRan > this.highestScore) { 414 | this.highestScore = Math.ceil(this.distanceRan); 415 | this.distanceMeter.setHighScore(this.highestScore); 416 | } 417 | 418 | // Reset the time clock. 419 | this.time = getTimeStamp(); 420 | 421 | setTimeout(() => { 422 | this.restart(); 423 | }, 500); 424 | } 425 | 426 | stop() { 427 | this.playing = false; 428 | cancelAnimationFrame(this.raqId); 429 | this.raqId = 0; 430 | } 431 | 432 | play() { 433 | if (!this.crashed) { 434 | this.playing = true; 435 | this.tRexGroup.update(0, Trex.status.RUNNING); 436 | this.time = getTimeStamp(); 437 | this.update(); 438 | } 439 | } 440 | 441 | restart() { 442 | if (!this.raqId) { 443 | this.playCount += 1; 444 | this.runningTime = 0; 445 | this.playing = true; 446 | this.crashed = false; 447 | this.distanceRan = 0; 448 | this.setSpeed(this.config.SPEED); 449 | this.time = getTimeStamp(); 450 | this.clearCanvas(); 451 | this.distanceMeter.reset(this.highestScore); 452 | this.horizon.reset(); 453 | this.tRexGroup.reset(); 454 | this.config.onReset(this.tRexGroup.tRexes ); 455 | this.update(); 456 | } else { 457 | this.isFirstTime = true; 458 | this.tRexGroup.reset(); 459 | this.config.onReset(this.tRexGroup.tRexes ); 460 | if (!this.playing) { 461 | this.playing = true; 462 | this.update(); 463 | } 464 | } 465 | Runner.generation += 1; 466 | } 467 | 468 | /** 469 | * Updates the canvas size taking into 470 | * account the backing store pixel ratio and 471 | * the device pixel ratio. 472 | * 473 | * See article by Paul Lewis: 474 | * http://www.html5rocks.com/en/tutorials/canvas/hidpi/ 475 | * 476 | * @param {HTMLCanvasElement} canvas 477 | * @param {number} width 478 | * @param {number} height 479 | * @return {boolean} Whether the canvas was scaled. 480 | */ 481 | static updateCanvasScaling(canvas, width, height) { 482 | const context = canvas.getContext('2d'); 483 | 484 | // Query the various pixel ratios 485 | const devicePixelRatio = Math.floor(window.devicePixelRatio) || 1; 486 | const backingStoreRatio = 487 | Math.floor(context.webkitBackingStorePixelRatio) || 1; 488 | const ratio = devicePixelRatio / backingStoreRatio; 489 | 490 | // Upscale the canvas if the two ratios don't match 491 | if (devicePixelRatio !== backingStoreRatio) { 492 | const oldWidth = width || canvas.width; 493 | const oldHeight = height || canvas.height; 494 | 495 | canvas.width = oldWidth * ratio; 496 | canvas.height = oldHeight * ratio; 497 | 498 | canvas.style.width = `${oldWidth}px`; 499 | canvas.style.height = `${oldHeight}px`; 500 | 501 | // Scale the context to counter the fact that we've manually scaled 502 | // our canvas element. 503 | context.scale(ratio, ratio); 504 | return true; 505 | } else if (devicePixelRatio === 1) { 506 | // Reset the canvas width / height. Fixes scaling bug when the page is 507 | // zoomed and the devicePixelRatio changes accordingly. 508 | canvas.style.width = `${canvas.width}px`; 509 | canvas.style.height = `${canvas.height}px`; 510 | } 511 | return false; 512 | } 513 | } 514 | 515 | /** 516 | * Create canvas element. 517 | * @param {HTMLElement} container Element to append canvas to. 518 | * @param {number} width 519 | * @param {number} height 520 | * @param {string} className 521 | * @return {HTMLCanvasElement} 522 | */ 523 | function createCanvas(container, width, height, className) { 524 | const canvas = document.createElement('canvas'); 525 | canvas.className = className 526 | ? `${Runner.classes.CANVAS} ${className}` 527 | : Runner.classes.CANVAS; 528 | canvas.width = width; 529 | canvas.height = height; 530 | container.appendChild(canvas); 531 | 532 | return canvas; 533 | } 534 | 535 | function noop() {} 536 | -------------------------------------------------------------------------------- /part2/src/game/RuntimeConfig.js: -------------------------------------------------------------------------------- 1 | let FPS = 60; 2 | 3 | export function getFPS() { 4 | return FPS; 5 | } 6 | 7 | export function setFPS(value) { 8 | FPS = value; 9 | } 10 | -------------------------------------------------------------------------------- /part2/src/game/Trex.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | CANVAS_WIDTH, 4 | CANVAS_HEIGHT, 5 | RUNNER_BOTTOM_PAD 6 | } from './constants'; 7 | import { getFPS } from './RuntimeConfig'; 8 | import { getImageSprite } from './ImageSprite'; 9 | import { getTimeStamp } from './utils'; 10 | import CollisionBox from './CollisionBox'; 11 | import Runner from './Runner'; 12 | 13 | /** 14 | * T-rex game character. 15 | * @param {HTMLCanvas} canvas 16 | * @param {Object} spritePos Positioning within image sprite. 17 | * @constructor 18 | */ 19 | export default class Trex { 20 | static config = { 21 | DROP_VELOCITY: -5, 22 | GRAVITY: 0.6, 23 | HEIGHT: 47, 24 | HEIGHT_DUCK: 25, 25 | INIITAL_JUMP_VELOCITY: -10, 26 | MAX_JUMP_HEIGHT: 30, 27 | MIN_JUMP_HEIGHT: 30, 28 | SPEED_DROP_COEFFICIENT: 3, 29 | SPRITE_WIDTH: 262, 30 | START_X_POS: 50, 31 | WIDTH: 44, 32 | WIDTH_DUCK: 59 33 | }; 34 | 35 | /** 36 | * Used in collision detection. 37 | * @type {Array} 38 | */ 39 | static collisionBoxes = { 40 | DUCKING: [new CollisionBox(1, 18, 55, 25)], 41 | RUNNING: [ 42 | new CollisionBox(22, 0, 17, 16), 43 | new CollisionBox(1, 18, 30, 9), 44 | new CollisionBox(10, 35, 14, 8), 45 | new CollisionBox(1, 24, 29, 5), 46 | new CollisionBox(5, 30, 21, 4), 47 | new CollisionBox(9, 34, 15, 4) 48 | ] 49 | }; 50 | 51 | /** 52 | * Animation states. 53 | * @enum {string} 54 | */ 55 | static status = { 56 | CRASHED: 'CRASHED', 57 | DUCKING: 'DUCKING', 58 | JUMPING: 'JUMPING', 59 | RUNNING: 'RUNNING', 60 | WAITING: 'WAITING' 61 | }; 62 | 63 | /** 64 | * Blinking coefficient. 65 | * @const 66 | */ 67 | static BLINK_TIMING = 7000; 68 | 69 | /** 70 | * Animation config for different states. 71 | * @enum {Object} 72 | */ 73 | static animFrames = { 74 | WAITING: { 75 | frames: [44, 0], 76 | msPerFrame: 1000 / 3 77 | }, 78 | RUNNING: { 79 | frames: [88, 132], 80 | msPerFrame: 1000 / 12 81 | }, 82 | CRASHED: { 83 | frames: [220], 84 | msPerFrame: 1000 / 60 85 | }, 86 | JUMPING: { 87 | frames: [0], 88 | msPerFrame: 1000 / 60 89 | }, 90 | DUCKING: { 91 | frames: [262, 321], 92 | msPerFrame: 1000 / 8 93 | } 94 | }; 95 | 96 | constructor(canvas, spritePos) { 97 | this.canvas = canvas; 98 | this.canvasCtx = canvas.getContext('2d'); 99 | this.spritePos = spritePos; 100 | this.xPos = 0; 101 | this.yPos = 0; 102 | // Position when on the ground. 103 | this.groundYPos = 0; 104 | this.currentFrame = 0; 105 | this.currentAnimFrames = []; 106 | this.blinkDelay = 0; 107 | this.blinkCount = 0; 108 | this.animStartTime = 0; 109 | this.timer = 0; 110 | this.msPerFrame = 1000 / getFPS(); 111 | this.config = Trex.config; 112 | // Current status. 113 | this.status = Trex.status.WAITING; 114 | 115 | this.jumping = false; 116 | this.ducking = false; 117 | this.jumpVelocity = 0; 118 | this.reachedMinHeight = false; 119 | this.speedDrop = false; 120 | this.jumpCount = 0; 121 | this.jumpspotX = 0; 122 | 123 | this.init(); 124 | } 125 | 126 | /** 127 | * T-rex player initaliser. 128 | * Sets the t-rex to blink at random intervals. 129 | */ 130 | init() { 131 | this.groundYPos = CANVAS_HEIGHT - this.config.HEIGHT - RUNNER_BOTTOM_PAD; 132 | this.yPos = this.groundYPos; 133 | this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT; 134 | 135 | this.draw(0, 0); 136 | this.update(0, Trex.status.WAITING); 137 | } 138 | 139 | /** 140 | * Setter for the jump velocity. 141 | * The approriate drop velocity is also set. 142 | */ 143 | setJumpVelocity(setting) { 144 | this.config.INIITAL_JUMP_VELOCITY = -setting; 145 | this.config.DROP_VELOCITY = -setting / 2; 146 | } 147 | 148 | /** 149 | * Set the animation status. 150 | * @param {!number} deltaTime 151 | * @param {Trex.status} status Optional status to switch to. 152 | */ 153 | update(deltaTime, status) { 154 | this.timer += deltaTime; 155 | 156 | // Update the status. 157 | if (status) { 158 | this.status = status; 159 | this.currentFrame = 0; 160 | this.msPerFrame = Trex.animFrames[status].msPerFrame; 161 | this.currentAnimFrames = Trex.animFrames[status].frames; 162 | 163 | if (status === Trex.status.WAITING) { 164 | this.animStartTime = getTimeStamp(); 165 | this.setBlinkDelay(); 166 | } 167 | } 168 | 169 | if (this.status === Trex.status.WAITING) { 170 | this.blink(getTimeStamp()); 171 | } else { 172 | this.draw(this.currentAnimFrames[this.currentFrame], 0); 173 | } 174 | 175 | // Update the frame position. 176 | if (this.timer >= this.msPerFrame) { 177 | this.currentFrame = 178 | this.currentFrame === this.currentAnimFrames.length - 1 179 | ? 0 180 | : this.currentFrame + 1; 181 | this.timer = 0; 182 | } 183 | 184 | // Speed drop becomes duck if the down key is still being pressed. 185 | if (this.speedDrop && this.yPos === this.groundYPos) { 186 | this.speedDrop = false; 187 | this.setDuck(true); 188 | } 189 | } 190 | 191 | /** 192 | * Draw the t-rex to a particular position. 193 | * @param {number} x 194 | * @param {number} y 195 | */ 196 | draw(x, y) { 197 | let sourceX = x; 198 | let sourceY = y; 199 | const sourceWidth = 200 | this.ducking && this.status !== Trex.status.CRASHED 201 | ? this.config.WIDTH_DUCK 202 | : this.config.WIDTH; 203 | const sourceHeight = this.config.HEIGHT; 204 | 205 | // Adjustments for sprite sheet position. 206 | sourceX += this.spritePos.x; 207 | sourceY += this.spritePos.y; 208 | 209 | // Ducking. 210 | if (this.ducking && this.status !== Trex.status.CRASHED) { 211 | this.canvasCtx.drawImage( 212 | getImageSprite(), 213 | sourceX, 214 | sourceY, 215 | sourceWidth, 216 | sourceHeight, 217 | this.xPos, 218 | this.yPos, 219 | this.config.WIDTH_DUCK, 220 | this.config.HEIGHT 221 | ); 222 | } else { 223 | // Crashed whilst ducking. Trex is standing up so needs adjustment. 224 | if (this.ducking && this.status === Trex.status.CRASHED) { 225 | this.xPos += 1; 226 | } 227 | // Standing / running 228 | this.canvasCtx.drawImage( 229 | getImageSprite(), 230 | sourceX, 231 | sourceY, 232 | sourceWidth, 233 | sourceHeight, 234 | this.xPos, 235 | this.yPos, 236 | this.config.WIDTH, 237 | this.config.HEIGHT 238 | ); 239 | } 240 | } 241 | 242 | /** 243 | * Sets a random time for the blink to happen. 244 | */ 245 | setBlinkDelay() { 246 | this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING); 247 | } 248 | 249 | /** 250 | * Make t-rex blink at random intervals. 251 | * @param {number} time Current time in milliseconds. 252 | */ 253 | blink(time) { 254 | const deltaTime = time - this.animStartTime; 255 | 256 | if (deltaTime >= this.blinkDelay) { 257 | this.draw(this.currentAnimFrames[this.currentFrame], 0); 258 | 259 | if (this.currentFrame === 1) { 260 | // Set new random delay to blink. 261 | this.setBlinkDelay(); 262 | this.animStartTime = time; 263 | this.blinkCount += 1; 264 | } 265 | } 266 | } 267 | 268 | /** 269 | * Initialise a jump. 270 | * @param {number} speed 271 | */ 272 | startJump(speed) { 273 | if (speed === undefined) { 274 | speed = Runner.instance_.currentSpeed; 275 | } 276 | if (!this.jumping) { 277 | this.update(0, Trex.status.JUMPING); 278 | // Tweak the jump velocity based on the speed. 279 | this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - speed / 10; 280 | this.jumping = true; 281 | this.reachedMinHeight = false; 282 | this.speedDrop = false; 283 | } 284 | } 285 | 286 | /** 287 | * Jump is complete, falling down. 288 | */ 289 | endJump() { 290 | if ( 291 | this.reachedMinHeight && 292 | this.jumpVelocity < this.config.DROP_VELOCITY 293 | ) { 294 | this.jumpVelocity = this.config.DROP_VELOCITY; 295 | } 296 | } 297 | 298 | /** 299 | * Update frame for a jump. 300 | * @param {number} deltaTime 301 | * @param {number} speed 302 | */ 303 | updateJump(deltaTime, speed) { 304 | const { msPerFrame } = Trex.animFrames[this.status]; 305 | const framesElapsed = deltaTime / msPerFrame; 306 | 307 | // Speed drop makes Trex fall faster. 308 | if (this.speedDrop) { 309 | this.yPos += Math.round( 310 | this.jumpVelocity * this.config.SPEED_DROP_COEFFICIENT * framesElapsed 311 | ); 312 | } else { 313 | this.yPos += Math.round(this.jumpVelocity * framesElapsed); 314 | } 315 | 316 | this.jumpVelocity += this.config.GRAVITY * framesElapsed; 317 | 318 | // Minimum height has been reached. 319 | if (this.yPos < this.minJumpHeight || this.speedDrop) { 320 | this.reachedMinHeight = true; 321 | } 322 | 323 | // Reached max height 324 | if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) { 325 | this.endJump(); 326 | } 327 | 328 | // Back down at ground level. Jump completed. 329 | if (this.yPos > this.groundYPos) { 330 | this.reset(); 331 | this.jumpCount += 1; 332 | } 333 | 334 | this.update(deltaTime); 335 | } 336 | 337 | /** 338 | * Set the speed drop. Immediately cancels the current jump. 339 | */ 340 | setSpeedDrop() { 341 | this.speedDrop = true; 342 | this.jumpVelocity = 1; 343 | } 344 | 345 | /** 346 | * @param {boolean} isDucking. 347 | */ 348 | setDuck(isDucking) { 349 | if (isDucking && this.status !== Trex.status.DUCKING) { 350 | this.update(0, Trex.status.DUCKING); 351 | this.ducking = true; 352 | } else if (this.status === Trex.status.DUCKING) { 353 | this.update(0, Trex.status.RUNNING); 354 | this.ducking = false; 355 | } 356 | } 357 | 358 | /** 359 | * Reset the t-rex to running at start of game. 360 | */ 361 | reset() { 362 | this.yPos = this.groundYPos; 363 | this.jumpVelocity = 0; 364 | this.jumping = false; 365 | this.ducking = false; 366 | this.update(0, Trex.status.RUNNING); 367 | this.midair = false; 368 | this.speedDrop = false; 369 | this.jumpCount = 0; 370 | this.crashed = false; 371 | } 372 | } 373 | 374 | /** 375 | * Check for a collision. 376 | * @param {!Obstacle} obstacle 377 | * @param {!Trex} tRex T-rex object. 378 | * @param {HTMLCanvasContext} canvasContext Optional canvas context for drawing 379 | * collision boxes. 380 | * @return {Array} 381 | */ 382 | export function checkForCollision(obstacle, tRex) { 383 | const obstacleBoxXPos = CANVAS_WIDTH + obstacle.xPos; 384 | 385 | // Adjustments are made to the bounding box as there is a 1 pixel white 386 | // border around the t-rex and obstacles. 387 | const tRexBox = new CollisionBox( 388 | tRex.xPos + 1, 389 | tRex.yPos + 1, 390 | tRex.config.WIDTH - 2, 391 | tRex.config.HEIGHT - 2 392 | ); 393 | 394 | const obstacleBox = new CollisionBox( 395 | obstacle.xPos + 1, 396 | obstacle.yPos + 1, 397 | obstacle.typeConfig.width * obstacle.size - 2, 398 | obstacle.typeConfig.height - 2 399 | ); 400 | 401 | // Simple outer bounds check. 402 | if (boxCompare(tRexBox, obstacleBox)) { 403 | const { collisionBoxes } = obstacle; 404 | const tRexCollisionBoxes = tRex.ducking 405 | ? Trex.collisionBoxes.DUCKING 406 | : Trex.collisionBoxes.RUNNING; 407 | 408 | // Detailed axis aligned box check. 409 | for (let t = 0; t < tRexCollisionBoxes.length; t += 1) { 410 | for (let i = 0; i < collisionBoxes.length; i += 1) { 411 | // Adjust the box to actual positions. 412 | const adjTrexBox = createAdjustedCollisionBox( 413 | tRexCollisionBoxes[t], 414 | tRexBox 415 | ); 416 | const adjObstacleBox = createAdjustedCollisionBox( 417 | collisionBoxes[i], 418 | obstacleBox 419 | ); 420 | const crashed = boxCompare(adjTrexBox, adjObstacleBox); 421 | 422 | if (crashed) { 423 | return [adjTrexBox, adjObstacleBox]; 424 | } 425 | } 426 | } 427 | } 428 | return false; 429 | } 430 | 431 | /** 432 | * Adjust the collision box. 433 | * @param {!CollisionBox} box The original box. 434 | * @param {!CollisionBox} adjustment Adjustment box. 435 | * @return {CollisionBox} The adjusted collision box object. 436 | */ 437 | function createAdjustedCollisionBox(box, adjustment) { 438 | return new CollisionBox( 439 | box.x + adjustment.x, 440 | box.y + adjustment.y, 441 | box.width, 442 | box.height 443 | ); 444 | } 445 | 446 | /** 447 | * Compare two collision boxes for a collision. 448 | * @param {CollisionBox} tRexBox 449 | * @param {CollisionBox} obstacleBox 450 | * @return {boolean} Whether the boxes intersected. 451 | */ 452 | function boxCompare(tRexBox, obstacleBox) { 453 | let crashed = false; 454 | const tRexBoxX = tRexBox.x; 455 | const tRexBoxY = tRexBox.y; 456 | 457 | const obstacleBoxX = obstacleBox.x; 458 | const obstacleBoxY = obstacleBox.y; 459 | 460 | // Axis-Aligned Bounding Box method. 461 | if ( 462 | tRexBox.x < obstacleBoxX + obstacleBox.width && 463 | tRexBox.x + tRexBox.width > obstacleBoxX && 464 | tRexBox.y < obstacleBox.y + obstacleBox.height && 465 | tRexBox.height + tRexBox.y > obstacleBox.y 466 | ) { 467 | crashed = true; 468 | } 469 | 470 | return crashed; 471 | } 472 | -------------------------------------------------------------------------------- /part2/src/game/TrexGroup.js: -------------------------------------------------------------------------------- 1 | import Runner from './Runner'; 2 | import Trex, { checkForCollision } from './Trex'; 3 | 4 | export default class TrexGroup { 5 | onReset = noop; 6 | onRunning = noop; 7 | onCrash = noop; 8 | 9 | constructor(count, canvas, spriteDef) { 10 | this.tRexes = []; 11 | for (let i = 0; i < count; i += 1) { 12 | const tRex = new Trex(canvas, spriteDef); 13 | tRex.id = i; 14 | this.tRexes.push(tRex); 15 | } 16 | } 17 | 18 | update(deltaTime, status) { 19 | this.tRexes.forEach((tRex) => { 20 | if (!tRex.crashed) { 21 | tRex.update(deltaTime, status); 22 | } 23 | }); 24 | } 25 | 26 | draw(x, y) { 27 | this.tRexes.forEach((tRex) => { 28 | if (!tRex.crashed) { 29 | tRex.draw(x, y); 30 | } 31 | }); 32 | } 33 | 34 | updateJump(deltaTime, speed) { 35 | this.tRexes.forEach((tRex) => { 36 | if (tRex.jumping) { 37 | tRex.updateJump(deltaTime, speed); 38 | } 39 | }); 40 | } 41 | 42 | reset() { 43 | this.tRexes.forEach((tRex) => { 44 | tRex.reset(); 45 | this.onReset({ tRex }); 46 | }); 47 | } 48 | 49 | lives() { 50 | return this.tRexes.reduce((count, tRex) => tRex.crashed ? count : count + 1, 0); 51 | } 52 | 53 | checkForCollision(obstacle) { 54 | let crashes = 0; 55 | const state = { 56 | obstacleX: obstacle.xPos, 57 | obstacleY: obstacle.yPos, 58 | obstacleWidth: obstacle.width, 59 | speed: Runner.instance_.currentSpeed 60 | }; 61 | this.tRexes.forEach(async (tRex) => { 62 | if (!tRex.crashed) { 63 | const result = checkForCollision(obstacle, tRex); 64 | if (result) { 65 | crashes += 1; 66 | tRex.crashed = true; 67 | this.onCrash(tRex, state ); 68 | } else { 69 | const action = await this.onRunning( tRex, state ); 70 | if (action === 1) { 71 | tRex.startJump(); 72 | } else if (action === -1) { 73 | if (tRex.jumping) { 74 | // Speed drop, activated only when jump key is not pressed. 75 | tRex.setSpeedDrop(); 76 | } else if (!tRex.jumping && !tRex.ducking) { 77 | // Duck. 78 | tRex.setDuck(true); 79 | } 80 | } 81 | } 82 | } else { 83 | crashes += 1; 84 | } 85 | }); 86 | return crashes === this.tRexes.length; 87 | } 88 | } 89 | 90 | function noop() { } 91 | -------------------------------------------------------------------------------- /part2/src/game/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default game canvas size. 3 | */ 4 | export const CANVAS_WIDTH = 600; 5 | export const CANVAS_HEIGHT = 150; 6 | 7 | /** 8 | * Runner configs 9 | */ 10 | export const RUNNER_BOTTOM_PAD = 10; 11 | export const RUNNER_MAX_OBSTACLE_DUPLICATION = 2; 12 | -------------------------------------------------------------------------------- /part2/src/game/images/offline-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aayusharora/GeneticAlgorithms/82c137a0c01770ef7e859a2d6cdadb7885a50047/part2/src/game/images/offline-sprite.png -------------------------------------------------------------------------------- /part2/src/game/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aayusharora/GeneticAlgorithms/82c137a0c01770ef7e859a2d6cdadb7885a50047/part2/src/game/images/splash.png -------------------------------------------------------------------------------- /part2/src/game/index.js: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | 3 | export { default as Runner } from './Runner'; 4 | -------------------------------------------------------------------------------- /part2/src/game/index.less: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | width: 100%; 6 | height: 100%; 7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 8 | } 9 | 10 | h1 { 11 | font-size: 18px; 12 | font-weight: bold; 13 | text-align: center; 14 | margin-top: 20px; 15 | margin-bottom: 20px; 16 | } 17 | 18 | /* Offline page */ 19 | .page { 20 | margin-top: 20px; 21 | overflow: hidden; 22 | 23 | .game { 24 | position: relative; 25 | color: #2b2b2b; 26 | font-size: 1em; 27 | margin: 0 auto; 28 | max-width: 600px; 29 | width: 100%; 30 | padding-top: 50px; 31 | } 32 | 33 | .game-canvas { 34 | height: 150px; 35 | max-width: 600px; 36 | opacity: 1; 37 | overflow: hidden; 38 | top: 0; 39 | z-index: 2; 40 | } 41 | 42 | .generation { 43 | position: absolute; 44 | top: 4px; 45 | left: 0; 46 | right: 0; 47 | font-weight: bold; 48 | text-align: center; 49 | } 50 | } 51 | 52 | @media (max-height: 350px) { 53 | .game { 54 | margin-top: 5%; 55 | } 56 | } 57 | 58 | @media (min-width: 600px) and (max-width: 736px) and (orientation: landscape) { 59 | .page .game { 60 | margin-left: 0; 61 | margin-right: 0; 62 | } 63 | } 64 | 65 | @media (max-width: 240px) { 66 | .game { 67 | overflow: inherit; 68 | padding: 0 8px; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /part2/src/game/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return the current timestamp. 3 | * @return {number} 4 | */ 5 | export function getTimeStamp() { 6 | return performance.now(); 7 | } 8 | 9 | /** 10 | * Get random number. 11 | * @param {number} min 12 | * @param {number} max 13 | * @param {number} 14 | */ 15 | export function getRandomNum(min, max) { 16 | return Math.floor(Math.random() * (max - min + 1)) + min; 17 | } 18 | -------------------------------------------------------------------------------- /part2/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | const ASSETS_SOURCE_PATH = path.resolve('./src'); 5 | const ASSETS_BUILD_PATH = path.resolve('./assets'); 6 | const ASSETS_PUBLIC_PATH = '/assets'; 7 | 8 | module.exports = { 9 | context: ASSETS_SOURCE_PATH, 10 | entry: { 11 | 'genetic': ['./apps/genetic.js'] 12 | }, 13 | output: { 14 | path: ASSETS_BUILD_PATH, 15 | publicPath: ASSETS_PUBLIC_PATH, 16 | filename: './[name].js' 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | enforce: 'pre', 22 | test: /\.jsx?$/, 23 | exclude: /node_modules|screen-capture/, 24 | loader: 'eslint-loader' 25 | }, 26 | { 27 | test: /\.js$/, 28 | exclude: /node_modules/, 29 | loader: 'babel-loader' 30 | }, 31 | { 32 | test: /\.less$/, 33 | exclude: /node_modules/, 34 | use: ['style-loader', 'css-loader', 'less-loader'] 35 | }, 36 | { 37 | test: /\.png$/, 38 | exclude: /node_modules/, 39 | use: [ 40 | { 41 | loader: 'url-loader', 42 | options: { 43 | limit: 8192, 44 | mimetype: 'image/png', 45 | name: 'images/[name].[ext]' 46 | } 47 | } 48 | ] 49 | } 50 | ] 51 | }, 52 | plugins: [new CleanWebpackPlugin([ASSETS_BUILD_PATH], { verbose: false })], 53 | optimization: { 54 | splitChunks: { 55 | cacheGroups: { 56 | vendor: { 57 | test: /node_modules/, 58 | chunks: 'initial', 59 | name: 'vendor', 60 | priority: 10, 61 | enforce: true 62 | } 63 | } 64 | } 65 | } 66 | }; 67 | --------------------------------------------------------------------------------