├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .prettierrc
├── README.md
├── ScreenFlow.gif
├── genetic-neural-network.html
├── genetic.html
├── index.html
├── neural-network-multiplayer.html
├── neural-network.html
├── package.json
├── random.html
├── src
├── ai
│ ├── models
│ │ ├── Model.js
│ │ ├── genetic-nn
│ │ │ └── NNModel.js
│ │ ├── genetic
│ │ │ ├── GeneticModel.js
│ │ │ └── RandomModel.js
│ │ ├── nn
│ │ │ └── NNModel.js
│ │ └── random
│ │ │ └── RandomModel.js
│ └── utils
│ │ └── index.js
├── apps
│ ├── genetic-nn.js
│ ├── genetic.js
│ ├── nn.js
│ ├── nnm.js
│ ├── random.js
│ └── template.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
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env", "stage-1"]
3 | }
4 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "singleQuote": true,
4 | "arrowParens": "always"
5 | }
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tensorflow-rex-run
2 |
3 | A TensorFlow.js based AI player platform for T-Rex Runner.
4 |
5 | ## About This Project (Chinese)
6 | [神经网络与遗传算法: TensorFlow.js 学会游戏通关](https://zhuanlan.zhihu.com/p/35451395)
7 |
8 | ## About T-Rex Runner Game
9 |
10 | 
11 |
12 | [T-Rex Runner](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.
13 |
14 |
15 | ## About TensorFlow.js
16 |
17 | The official version of TensorFlow in JavaScript. It is A WebGL accelerated, browser based JavaScript library for training and deploying ML models.
18 | Visit the [official website](https://js.tensorflow.org/) to discover more.
19 |
20 |
21 | ## About This Project
22 |
23 | `t-trex-run` is an artificial intelligent player platform designed for T-Rex Runner, and enpowered by TensorFlow.js.
24 |
25 | 
26 |
27 | ## Key Features
28 |
29 | * Totally rewritten in ES6/ES7, LESS and Webpack
30 | * Multiplayer mode supported (means you can use genetic algorithm now)
31 | * Events like `onReset`, `onRunning` and `onCrushed` are supported
32 | * Example models are provided
33 |
34 |
35 | ---
36 |
37 |
38 | ## How to Install
39 |
40 | ```sh
41 | npm install
42 | ```
43 |
44 |
45 | ## How to Run
46 |
47 |
48 | ```sh
49 | npm start
50 | ```
51 |
52 | Visit http://localhost:8080
53 |
--------------------------------------------------------------------------------
/ScreenFlow.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MagicCube/tensorflow-rex-run/eb0ea3658d7f959e41337aacf452f85fc608f12b/ScreenFlow.gif
--------------------------------------------------------------------------------
/genetic-neural-network.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Genetic Algorithm - T-Rex Runner
8 |
9 |
10 |
11 | Genetic Algorithm Model
12 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/genetic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Genetic Algorithm - T-Rex Runner
8 |
9 |
10 |
11 | Genetic Algorithm
12 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Welcome to T-Rex Run
8 |
9 |
10 | Welcome to T-Rex Run
11 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/neural-network-multiplayer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Neural Network for Multiplayers - T-Rex Runner
8 |
9 |
10 |
11 | Neural Network for Multiplayers
12 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/neural-network.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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "t-rex-run",
3 | "version": "1.0.0",
4 | "description": "A TensorFlow.js based AI player platform for T-Rex Runner. T-Rex Runner 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": "Henry Li ",
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.3.12",
45 | "webpack-dev-server": "^3.1.3"
46 | },
47 | "dependencies": {
48 | "@tensorflow/tfjs": "^0.9.0",
49 | "babel-polyfill": "^6.26.0",
50 | "package.json": "^2.0.1"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/random.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Random - T-Rex Runner
8 |
9 |
10 |
11 | Random for Fun
12 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/ai/models/Model.js:
--------------------------------------------------------------------------------
1 | import { tensor } from '../utils';
2 |
3 | export default class Model {
4 | init() {
5 | throw new Error(
6 | 'Abstract method must be implemented in the derived class.'
7 | );
8 | }
9 |
10 | predict(inputXs) {
11 | throw new Error(
12 | 'Abstract method must be implemented in the derived class.'
13 | );
14 | }
15 |
16 | predictSingle(inputX) {
17 | return this.predict([inputX]);
18 | }
19 |
20 | train(inputXs, inputYs) {
21 | throw new Error(
22 | 'Abstract method must be implemented in the derived class.'
23 | );
24 | }
25 |
26 | fit(inputXs, inputYs, iterationCount = 100) {
27 | for (let i = 0; i < iterationCount; i += 1) {
28 | this.train(inputXs, inputYs);
29 | }
30 | }
31 |
32 | loss(predictedYs, labels) {
33 | const meanSquareError = predictedYs
34 | .sub(tensor(labels))
35 | .square()
36 | .mean();
37 | return meanSquareError;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/ai/models/genetic-nn/NNModel.js:
--------------------------------------------------------------------------------
1 | import * as tf from '@tensorflow/tfjs';
2 | import NNModel from '../nn/NNModel';
3 |
4 | export default class GeneticNNModel extends NNModel {
5 | getChromosome() {
6 | const result = tf.concat([
7 | this.weights[0].flatten(),
8 | this.biases[0].flatten(),
9 | this.weights[1].flatten(),
10 | this.biases[1].flatten()
11 | ]);
12 | return result.dataSync();
13 | }
14 |
15 | setChromosome(chromosome) {
16 | let weight = chromosome.slice(0, 3 * 6);
17 | let bias = chromosome.slice(3 * 6, 3 * 6 + 1);
18 | this.weights[0].assign(tf.tensor(weight, [3, 6]));
19 | this.biases[0].assign(tf.tensor(bias[0]));
20 | weight = chromosome.slice(3 * 6 + 1, 3 * 6 + 1 + 6 * 2);
21 | bias = chromosome.slice(3 * 6 + 1 + 6 * 2, 3 * 6 + 1 + 6 * 2 + 1);
22 | this.weights[1].assign(tf.tensor(weight, [6, 2]));
23 | this.biases[1].assign(tf.tensor(bias[0]));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/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 | const offspring1 = parents[0].slice();
22 | const offspring2 = parents[1].slice();
23 | // Select a random crossover point
24 | const crossOverPoint = Math.floor(Math.random() * chromosomes.length);
25 | // Swap values among parents
26 | for (let i = 0; i < crossOverPoint; i += 1) {
27 | const temp = offspring1[i];
28 | offspring1[i] = offspring2[i];
29 | offspring2[i] = temp;
30 | }
31 | const offspring = [offspring1, offspring2];
32 | // Replace the last 2 with the new offspring
33 | for (let i = 0; i < 2; i += 1) {
34 | chromosomes[chromosomes.length - i - 1] = offspring[i];
35 | }
36 | return offspring;
37 | }
38 |
39 | mutate(chromosomes) {
40 | chromosomes.forEach(chromosome => {
41 | const mutationPoint = Math.floor(Math.random() * chromosomes.length);
42 | chromosome[mutationPoint] = Math.random();
43 | });
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/ai/models/genetic/RandomModel.js:
--------------------------------------------------------------------------------
1 | import RandomModel from '../random/RandomModel';
2 |
3 | export default class GeneticRandomModel extends RandomModel {
4 | getChromosome() {
5 | return this.weights.concat(this.biases);
6 | }
7 |
8 | setChromosome(chromosome) {
9 | this.weights[0] = chromosome[0];
10 | this.weights[1] = chromosome[1];
11 | this.weights[2] = chromosome[2];
12 | this.biases[0] = chromosome[3];
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/ai/models/nn/NNModel.js:
--------------------------------------------------------------------------------
1 | import * as tf from '@tensorflow/tfjs';
2 |
3 | import { tensor } from '../../utils';
4 | import Model from '../Model';
5 |
6 | /**
7 | * Simple Neural Network Model
8 | */
9 | export default class NNModel extends Model {
10 | weights = [];
11 | biases = [];
12 |
13 | constructor({
14 | inputSize = 3,
15 | hiddenLayerSize = inputSize * 2,
16 | outputSize = 2,
17 | learningRate = 0.1
18 | } = {}) {
19 | super();
20 | this.hiddenLayerSize = hiddenLayerSize;
21 | this.inputSize = inputSize;
22 | this.outputSize = outputSize;
23 | // Using ADAM optimizer
24 | this.optimizer = tf.train.adam(learningRate);
25 | }
26 |
27 | init() {
28 | // Hidden layer
29 | this.weights[0] = tf.variable(
30 | tf.randomNormal([this.inputSize, this.hiddenLayerSize])
31 | );
32 | this.biases[0] = tf.variable(tf.scalar(Math.random()));
33 | // Output layer
34 | this.weights[1] = tf.variable(
35 | tf.randomNormal([this.hiddenLayerSize, this.outputSize])
36 | );
37 | this.biases[1] = tf.variable(tf.scalar(Math.random()));
38 | }
39 |
40 | predict(inputXs) {
41 | const x = tensor(inputXs);
42 | const prediction = tf.tidy(() => {
43 | const hiddenLayer = tf.sigmoid(x.matMul(this.weights[0]).add(this.biases[0]));
44 | const outputLayer = tf.sigmoid(hiddenLayer.matMul(this.weights[1]).add(this.biases[1]));
45 | return outputLayer;
46 | });
47 | return prediction;
48 | }
49 |
50 | train(inputXs, inputYs) {
51 | this.optimizer.minimize(() => {
52 | const predictedYs = this.predict(inputXs);
53 | return this.loss(predictedYs, inputYs);
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/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 | this.randomize();
9 | }
10 |
11 | predict(inputXs) {
12 | const inputX = inputXs[0];
13 | const y =
14 | this.weights[0] * inputX[0] +
15 | this.weights[1] * inputX[1]+
16 | this.weights[2] * inputX[2] +
17 | this.biases[0];
18 | return y < 0 ? 1 : 0;
19 | }
20 |
21 | train() {
22 | this.randomize();
23 | }
24 |
25 | randomize() {
26 | this.weights[0] = random();
27 | this.weights[1] = random();
28 | this.weights[2] = random();
29 | this.biases[0] = random();
30 | }
31 | }
32 |
33 | function random() {
34 | return (Math.random() - 0.5) * 2;
35 | }
36 |
--------------------------------------------------------------------------------
/src/ai/utils/index.js:
--------------------------------------------------------------------------------
1 | import * as tf from '@tensorflow/tfjs';
2 |
3 | export function isTensor(obj) {
4 | return obj instanceof tf.Tensor;
5 | }
6 |
7 | export function tensor(obj) {
8 | if (obj instanceof tf.Tensor) {
9 | return obj;
10 | }
11 | if (typeof obj === 'number') {
12 | return tf.scalar(obj);
13 | } else if (Array.isArray(obj)) {
14 | return tf.tensor(obj);
15 | }
16 | throw new Error(
17 | 'tensor() only supports number or array as the input parameter.'
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/apps/genetic-nn.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 |
3 | import { CANVAS_WIDTH, CANVAS_HEIGHT } from '../game/constants';
4 | import { Runner } from '../game';
5 | import NNModel from '../ai/models/genetic-nn/NNModel';
6 | import GeneticModel from '../ai/models/genetic/GeneticModel';
7 |
8 | const T_REX_COUNT = 10;
9 |
10 | const geneticModel = new GeneticModel();
11 | const rankList = [];
12 |
13 | let runner = null;
14 |
15 | function setup() {
16 | // Initialize the game Runner.
17 | runner = new Runner('.game', {
18 | T_REX_COUNT,
19 | onReset: handleReset,
20 | onCrash: handleCrash,
21 | onRunning: handleRunning
22 | });
23 | // Set runner as a global variable if you need runtime debugging.
24 | window.runner = runner;
25 | // Initialize everything in the game and start the game.
26 | runner.init();
27 | }
28 |
29 | let firstTime = true;
30 | function handleReset({ tRexes }) {
31 | if (firstTime) {
32 | // Initialize all the tRexes for the very first time.
33 | firstTime = false;
34 | tRexes.forEach((tRex) => {
35 | tRex.model = new NNModel();
36 | tRex.model.init();
37 | tRex.training = {
38 | inputs: [],
39 | labels: []
40 | };
41 | });
42 | } else {
43 | // Train the model before restarting.
44 | console.info('Training');
45 | // Do the NN training first
46 | tRexes.forEach((tRex) => {
47 | tRex.model.fit(tRex.training.inputs, tRex.training.labels);
48 | });
49 | // Genetic training
50 | const chromosomes = rankList.map((tRex) => tRex.model.getChromosome());
51 | // Clear rankList
52 | rankList.splice(0);
53 | geneticModel.fit(chromosomes);
54 | tRexes.forEach((tRex, i) => {
55 | tRex.model.setChromosome(chromosomes[i]);
56 | });
57 | }
58 | }
59 |
60 | function handleRunning({ tRex, state }) {
61 | return new Promise((resolve) => {
62 | if (!tRex.jumping) {
63 | let action = 0;
64 | const prediction = tRex.model.predictSingle(convertStateToVector(state));
65 | prediction.data().then((result) => {
66 | if (result[1] > result[0]) {
67 | action = 1;
68 | tRex.lastJumpingState = state;
69 | } else {
70 | tRex.lastRunningState = state;
71 | }
72 | resolve(action);
73 | });
74 | } else {
75 | resolve(0);
76 | }
77 | });
78 | }
79 |
80 | function handleCrash({ tRex }) {
81 | let input = null;
82 | let label = null;
83 | if (tRex.jumping) {
84 | input = convertStateToVector(tRex.lastJumpingState);
85 | label = [1, 0];
86 | } else {
87 | input = convertStateToVector(tRex.lastRunningState);
88 | label = [0, 1];
89 | }
90 | tRex.training.inputs.push(input);
91 | tRex.training.labels.push(label);
92 | if (!rankList.includes(tRex)) {
93 | rankList.unshift(tRex);
94 | }
95 | }
96 |
97 | function convertStateToVector(state) {
98 | if (state) {
99 | return [
100 | state.obstacleX / CANVAS_WIDTH,
101 | state.obstacleWidth / CANVAS_WIDTH,
102 | state.speed / 100
103 | ];
104 | }
105 | return [0, 0, 0];
106 | }
107 |
108 | document.addEventListener('DOMContentLoaded', setup);
109 |
--------------------------------------------------------------------------------
/src/apps/genetic.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 |
3 | import { CANVAS_WIDTH, CANVAS_HEIGHT } from '../game/constants';
4 | import { Runner } from '../game';
5 | import GeneticModel from '../ai/models/genetic/GeneticModel';
6 | import RandomModel from '../ai/models/genetic/RandomModel';
7 |
8 | const T_REX_COUNT = 10;
9 |
10 | let runner = null;
11 |
12 | const rankList = [];
13 | const geneticModel = new GeneticModel();
14 |
15 | function setup() {
16 | // Initialize the game Runner.
17 | runner = new Runner('.game', {
18 | T_REX_COUNT,
19 | onReset: handleReset,
20 | onCrash: handleCrash,
21 | onRunning: handleRunning
22 | });
23 | // Set runner as a global variable if you need runtime debugging.
24 | window.runner = runner;
25 | // Initialize everything in the game and start the game.
26 | runner.init();
27 | }
28 |
29 | let firstTime = true;
30 | function handleReset({ tRexes }) {
31 | if (firstTime) {
32 | // Initialize all the tRexes with random models
33 | // for the very first time.
34 | firstTime = false;
35 | tRexes.forEach((tRex) => {
36 | tRex.model = new RandomModel();
37 | tRex.model.init();
38 | });
39 | } else {
40 | // Train the model before restarting.
41 | console.info('Training');
42 | const chromosomes = rankList.map((tRex) => tRex.model.getChromosome());
43 | // Clear rankList
44 | rankList.splice(0);
45 | geneticModel.fit(chromosomes);
46 | tRexes.forEach((tRex, i) => {
47 | tRex.model.setChromosome(chromosomes[i]);
48 | });
49 | }
50 | }
51 |
52 | function handleRunning({ tRex, state }) {
53 | let action = 0;
54 | if (!tRex.jumping) {
55 | action = tRex.model.predictSingle(convertStateToVector(state));
56 | }
57 | return action;
58 | }
59 |
60 | function handleCrash({ tRex }) {
61 | if (!rankList.includes(tRex)) {
62 | rankList.unshift(tRex);
63 | }
64 | }
65 |
66 | function convertStateToVector(state) {
67 | if (state) {
68 | return [
69 | state.obstacleX / CANVAS_WIDTH,
70 | state.obstacleWidth / CANVAS_WIDTH,
71 | state.speed / 100
72 | ];
73 | }
74 | return [0, 0, 0];
75 | }
76 |
77 | document.addEventListener('DOMContentLoaded', setup);
78 |
--------------------------------------------------------------------------------
/src/apps/nn.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 |
3 | import { CANVAS_WIDTH, CANVAS_HEIGHT } from '../game/constants';
4 | import { Runner } from '../game';
5 | import NNModel from '../ai/models/nn/NNModel';
6 |
7 | let runner = null;
8 |
9 | function setup() {
10 | // Initialize the game Runner.
11 | runner = new Runner('.game', {
12 | T_REX_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 |
23 | let firstTime = true;
24 | function handleReset({ tRexes }) {
25 | const tRex = tRexes[0];
26 | if (firstTime) {
27 | firstTime = false;
28 | tRex.model = new NNModel();
29 | tRex.model.init();
30 | tRex.training = {
31 | inputs: [],
32 | labels: []
33 | };
34 | } else {
35 | // Train the model before restarting.
36 | console.info('Training');
37 | tRex.model.fit(tRex.training.inputs, tRex.training.labels);
38 | }
39 | }
40 |
41 | function handleRunning({ tRex, state }) {
42 | return new Promise((resolve) => {
43 | if (!tRex.jumping) {
44 | let action = 0;
45 | const prediction = tRex.model.predictSingle(convertStateToVector(state));
46 | prediction.data().then((result) => {
47 | if (result[1] > result[0]) {
48 | action = 1;
49 | tRex.lastJumpingState = state;
50 | } else {
51 | tRex.lastRunningState = state;
52 | }
53 | resolve(action);
54 | });
55 | } else {
56 | resolve(0);
57 | }
58 | });
59 | }
60 |
61 | function handleCrash({ tRex }) {
62 | let input = null;
63 | let label = null;
64 | if (tRex.jumping) {
65 | // Should not jump next time
66 | input = convertStateToVector(tRex.lastJumpingState);
67 | label = [1, 0];
68 | } else {
69 | // Should jump next time
70 | input = convertStateToVector(tRex.lastRunningState);
71 | label = [0, 1];
72 | }
73 | tRex.training.inputs.push(input);
74 | tRex.training.labels.push(label);
75 | }
76 |
77 | function convertStateToVector(state) {
78 | if (state) {
79 | return [
80 | state.obstacleX / CANVAS_WIDTH,
81 | state.obstacleWidth / CANVAS_WIDTH,
82 | state.speed / 100
83 | ];
84 | }
85 | return [0, 0, 0];
86 | }
87 |
88 | document.addEventListener('DOMContentLoaded', setup);
89 |
--------------------------------------------------------------------------------
/src/apps/nnm.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 |
3 | import { CANVAS_WIDTH, CANVAS_HEIGHT } from '../game/constants';
4 | import { Runner } from '../game';
5 | import NNModel from '../ai/models/nn/NNModel';
6 |
7 | const T_REX_COUNT = 3;
8 |
9 | let runner = null;
10 |
11 | const training = {
12 | inputs: [],
13 | labels: []
14 | };
15 |
16 | function setup() {
17 | // Initialize the game Runner.
18 | runner = new Runner('.game', {
19 | T_REX_COUNT,
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 | // Initialize everything in the game and start the game.
27 | runner.init();
28 | }
29 |
30 | let firstTime = true;
31 | function handleReset({ tRexes }) {
32 | if (firstTime) {
33 | // Initialize all the tRexes for the very first time.
34 | firstTime = false;
35 | tRexes.forEach((tRex) => {
36 | tRex.model = new NNModel();
37 | tRex.model.init();
38 | });
39 | } else {
40 | // Train the model before restarting.
41 | console.info('Training');
42 | tRexes.forEach((tRex) => {
43 | tRex.model.fit(training.inputs, training.labels);
44 | });
45 | }
46 | }
47 |
48 | function handleRunning({ tRex, state }) {
49 | return new Promise((resolve) => {
50 | if (!tRex.jumping) {
51 | let action = 0;
52 | const prediction = tRex.model.predictSingle(convertStateToVector(state));
53 | prediction.data().then((result) => {
54 | if (result[1] > result[0]) {
55 | action = 1;
56 | tRex.lastJumpingState = state;
57 | } else {
58 | tRex.lastRunningState = state;
59 | }
60 | resolve(action);
61 | });
62 | } else {
63 | resolve(0);
64 | }
65 | });
66 | }
67 |
68 | function handleCrash({ tRex }) {
69 | let input = null;
70 | let label = null;
71 | if (tRex.jumping) {
72 | input = convertStateToVector(tRex.lastJumpingState);
73 | label = [1, 0];
74 | } else {
75 | input = convertStateToVector(tRex.lastRunningState);
76 | label = [0, 1];
77 | }
78 | training.inputs.push(input);
79 | training.labels.push(label);
80 | }
81 |
82 | function convertStateToVector(state) {
83 | if (state) {
84 | return [
85 | state.obstacleX / CANVAS_WIDTH,
86 | state.obstacleWidth / CANVAS_WIDTH,
87 | state.speed / 100
88 | ];
89 | }
90 | return [0, 0, 0];
91 | }
92 |
93 | document.addEventListener('DOMContentLoaded', setup);
94 |
--------------------------------------------------------------------------------
/src/apps/random.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 |
3 | import { CANVAS_WIDTH, CANVAS_HEIGHT } from '../game/constants';
4 | import { Runner } from '../game';
5 | import RandomModel from '../ai/models/random/RandomModel';
6 |
7 | let runner = null;
8 |
9 | function setup() {
10 | // Initialize the game Runner.
11 | runner = new Runner('.game', {
12 | T_REX_COUNT: 10,
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 |
23 | let firstTime = true;
24 | function handleReset({ tRexes }) {
25 | if (firstTime) {
26 | firstTime = false;
27 | tRexes.forEach((tRex) => {
28 | if (!tRex.model) {
29 | // Initialize all the tRexes with random models
30 | // for the very first time.
31 | tRex.model = new RandomModel();
32 | tRex.model.init();
33 | }
34 | });
35 | }
36 | }
37 |
38 | function handleRunning({ tRex, state }) {
39 | let action = 0;
40 | if (!tRex.jumping) {
41 | action = tRex.model.predictSingle(convertStateToVector(state));
42 | }
43 | return action;
44 | }
45 |
46 | function handleCrash({ tRex }) {
47 | tRex.model.train();
48 | }
49 |
50 | function convertStateToVector(state) {
51 | if (state) {
52 | return [
53 | state.obstacleX / CANVAS_WIDTH,
54 | state.obstacleWidth / CANVAS_WIDTH,
55 | state.speed / 100
56 | ];
57 | }
58 | return [0, 0, 0];
59 | }
60 |
61 | document.addEventListener('DOMContentLoaded', setup);
62 |
--------------------------------------------------------------------------------
/src/apps/template.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 |
3 | import { CANVAS_WIDTH, CANVAS_HEIGHT } from '../game/constants';
4 | import { Runner } from '../game';
5 |
6 | let runner = null;
7 |
8 | function setup() {
9 | // Initialize the game Runner.
10 | runner = new Runner('.game', {
11 | onReset: handleReset,
12 | onRunning: handleRunning,
13 | onCrash: handleCrash
14 | });
15 | // Set runner as a global variable if you need runtime debugging.
16 | window.runner = runner;
17 | // Initialize everything in the game and start the game.
18 | runner.init();
19 | }
20 |
21 | function handleReset({ tRexes }) {
22 | // Add initialization of tRexes here.
23 | // This method is called everytime the game restarts.
24 | }
25 |
26 | function handleRunning({ tRex, state }) {
27 | // Decide whether this `tRex` should jump(return 1) or keep walking(return 0)
28 | // based on the `state` provided in the parameter.
29 | }
30 |
31 | function handleCrash({ tRex }) {
32 | // Fires when the `tRex` hit a obstacle(like cactus or bird).
33 | }
34 |
35 | function convertStateToVector(state) {
36 | // Here's an example of how to convert the state which provided in handleRunning()
37 | // into a three-dimensional vector as a array.
38 | if (state) {
39 | return [
40 | state.obstacleX / CANVAS_WIDTH,
41 | state.obstacleWidth / CANVAS_WIDTH,
42 | state.speed / 100
43 | ];
44 | }
45 | return [0, 0, 0];
46 | }
47 |
48 | document.addEventListener('DOMContentLoaded', setup);
49 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | T_REX_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.T_REX_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({ tRexes: this.tRexGroup.tRexes });
456 | this.update();
457 | } else {
458 | this.isFirstTime = true;
459 | this.tRexGroup.reset();
460 | this.config.onReset({ tRexes: 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/game/images/offline-sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MagicCube/tensorflow-rex-run/eb0ea3658d7f959e41337aacf452f85fc608f12b/src/game/images/offline-sprite.png
--------------------------------------------------------------------------------
/src/game/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MagicCube/tensorflow-rex-run/eb0ea3658d7f959e41337aacf452f85fc608f12b/src/game/images/splash.png
--------------------------------------------------------------------------------
/src/game/index.js:
--------------------------------------------------------------------------------
1 | import './index.less';
2 |
3 | export { default as Runner } from './Runner';
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | 'genetic-nn': ['./apps/genetic-nn.js'],
13 | nn: ['./apps/nn.js'],
14 | nnm: ['./apps/nnm.js'],
15 | random: ['./apps/random.js']
16 | },
17 | output: {
18 | path: ASSETS_BUILD_PATH,
19 | publicPath: ASSETS_PUBLIC_PATH,
20 | filename: './[name].js'
21 | },
22 | module: {
23 | rules: [
24 | {
25 | enforce: 'pre',
26 | test: /\.jsx?$/,
27 | exclude: /node_modules|screen-capture/,
28 | loader: 'eslint-loader'
29 | },
30 | {
31 | test: /\.js$/,
32 | exclude: /node_modules/,
33 | loader: 'babel-loader'
34 | },
35 | {
36 | test: /\.less$/,
37 | exclude: /node_modules/,
38 | use: ['style-loader', 'css-loader', 'less-loader']
39 | },
40 | {
41 | test: /\.png$/,
42 | exclude: /node_modules/,
43 | use: [
44 | {
45 | loader: 'url-loader',
46 | options: {
47 | limit: 8192,
48 | mimetype: 'image/png',
49 | name: 'images/[name].[ext]'
50 | }
51 | }
52 | ]
53 | }
54 | ]
55 | },
56 | plugins: [new CleanWebpackPlugin([ASSETS_BUILD_PATH], { verbose: false })],
57 | optimization: {
58 | splitChunks: {
59 | cacheGroups: {
60 | vendor: {
61 | test: /node_modules/,
62 | chunks: 'initial',
63 | name: 'vendor',
64 | priority: 10,
65 | enforce: true
66 | }
67 | }
68 | }
69 | }
70 | };
71 |
--------------------------------------------------------------------------------