├── 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 | 
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 |
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 | 
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 |
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 |
--------------------------------------------------------------------------------