├── .github
└── FUNDING.yml
├── LICENSE
├── README.md
├── css
└── main.css
├── favicon.png
├── index.html
├── js
├── Cannon.js
├── CannonBall.js
├── ComputerController.js
├── Dejavu.js
├── Matrix.js
└── main.js
└── screenshot.png
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | patreon: victorqribeiro
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Victor Ribeiro
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BangBang
2 |
3 | 
4 |
5 | [live version](https://victorribeiro.com/bangBang)
6 | [alternative link](https://victorqribeiro.github.io/bangBang/)
7 |
8 | ## About
9 |
10 | Play BangBang against a Neural Network (Dejavu, a toy neural network that I wrote myself).
11 | The Neural Network is fresh each round, it will shoot at random at the first time then it will learn how to improve it's accuracy, until it hits you; if you don't hit it first.
12 |
13 | ## How to Play
14 |
15 | ### On Desktop
16 |
17 | **Left** and **Right** arrows **rotate** the cannon. **Up** and **Down** arrows increase and decrease the **strength** of the projectile. **Spacebar shoots**.
18 |
19 | ### On Mobile
20 |
21 | **Touch and drag up and down** on the **right** side of the screen to **rotate** the cannon.
22 | **Touch and drag up and down** on the **left** side of the screen to adjust the **strength**.
23 | **Tap the screen** to **shoot**.
24 |
--------------------------------------------------------------------------------
/css/main.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | margin: 0;
3 | padding: 0;
4 | overscroll-behavior: contain;
5 | }
6 |
7 | canvas {
8 | display: block;
9 | touch-action: none;
10 | }
--------------------------------------------------------------------------------
/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/victorqribeiro/bangBang/4a4b48ef3f6b72e3ee0a998299dcd7eecdfc9f52/favicon.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | BangBang ML
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/js/Cannon.js:
--------------------------------------------------------------------------------
1 | class Cannon {
2 |
3 | constructor(pos, turn, right = false) {
4 | this.pos = {
5 | x: pos.x || 0,
6 | y: pos.y || 0
7 | }
8 | this.turn = turn
9 | this.right = right
10 | this.angle = right ? Math.PI : 0
11 | this.dir = right ? -0.05 : 0.05
12 | this.minLimit = right ? Math.PI : 0
13 | this.maxLimit = right ? Math.PI + Math.PI / 2 : -Math.PI / 2
14 | this.strength = 10
15 | this.isRotatingUp = false
16 | this.isRotatingDown = false
17 | this.isIncreasingStrength = false
18 | this.isDecreasingStrength = false
19 | this._min = right ? Math.min : Math.max
20 | this._max = right ? Math.max : Math.min
21 | }
22 |
23 | update() {
24 | if (this.isRotatingUp)
25 | this.angle = this._min(this.angle - this.dir, this.maxLimit)
26 | else if (this.isRotatingDown)
27 | this.angle = this._max(this.angle + this.dir, this.minLimit)
28 | if (this.isIncreasingStrength && this.strength < 100)
29 | this.strength += 0.5
30 | else if (this.isDecreasingStrength && this.strength > 0)
31 | this.strength -= 0.5
32 | }
33 |
34 | show() {
35 | c.save()
36 | c.translate(this.pos.x, this.pos.y)
37 | c.fillStyle = "red"
38 | c.strokeStyle = "black"
39 | c.fillRect(this.right ? 30 : -40, -30, 10, -this.strength)
40 | c.strokeRect(this.right ? 30 : -40, -30, 10, -100)
41 | c.fillStyle = "black"
42 | c.beginPath()
43 | c.arc(0, 0, 30, 0, Math.PI, true)
44 | c.lineTo(-30, 10)
45 | c.lineTo(30, 10)
46 | c.fillStyle = this.right ? "gray" : "black"
47 | c.fill()
48 | c.rotate(this.angle)
49 | c.fillRect(-5, -7, 50, 14)
50 | c.restore()
51 | }
52 |
53 | shoot() {
54 | if (!cannonBall && turn === this.turn)
55 | cannonBall = new CannonBall(this.pos, this.angle, this.strength, this.turn ? human.pos : computerController.player.pos)
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/js/CannonBall.js:
--------------------------------------------------------------------------------
1 | class CannonBall {
2 |
3 | constructor(pos, angle, strength, target) {
4 | this.pos = {
5 | x: pos.x || 0,
6 | y: pos.y || 0
7 | }
8 | this.angle = angle || 0
9 | this.strength = strength || 0
10 | this.target = target
11 | this.acc = {
12 | x: Math.cos(this.angle) * this.strength,
13 | y: Math.sin(this.angle) * this.strength
14 | }
15 | }
16 |
17 | isGone() {
18 | return this.pos.y > h
19 | }
20 |
21 | isHit() {
22 | const dX = this.pos.x - this.target.x
23 | const dY = this.pos.y - this.target.y
24 | return Math.sqrt(dX * dX + dY * dY) < 30
25 | }
26 |
27 | update() {
28 | if (this.isHit()) {
29 | return gameOver = true
30 | }
31 | if (this.isGone()) {
32 | if (turn) {
33 | computerController.fit(this.pos.x)
34 | }
35 | changeTurn()
36 | cannonBall = null
37 | return
38 | }
39 | this.pos.x += this.acc.x
40 | this.pos.y += this.acc.y
41 | this.acc.x *= 0.975
42 | this.acc.y *= 0.975
43 | this.acc.y += 1
44 | }
45 |
46 | show() {
47 | c.beginPath()
48 | c.arc(this.pos.x, this.pos.y, 5, 0, TWOPI)
49 | c.fillStyle = "black"
50 | c.fill()
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/js/ComputerController.js:
--------------------------------------------------------------------------------
1 | class ComputerController {
2 |
3 | constructor(player, neuralNetwork) {
4 | this.player = player
5 | this.nn = neuralNetwork
6 | this._x = null
7 | this.desiredTarget = null
8 | this.angleDone = false
9 | this.strengthDone = false
10 | this.done = false
11 | }
12 |
13 | update() {
14 | if (this.done || this.player.turn !== turn) return
15 |
16 | if (this.angleDone && this.strengthDone) {
17 | this.player.shoot()
18 | this.done = true
19 | this.reset()
20 | }
21 |
22 | if (!this.desiredTarget)
23 | this.getDesiredTarget()
24 |
25 | this.adjustAngle()
26 | this.adjustStrength()
27 | this.player.update()
28 | }
29 |
30 | getDesiredTarget() {
31 | this._x = [
32 | human.pos.x / w,
33 | human.pos.y / h,
34 | this.player.angle / (-Math.PI / 2),
35 | this.player.strength / 100
36 | ]
37 | const prediction = this.nn.predict(this._x).data
38 | const angle = prediction[0] * (-Math.PI / 2)
39 | const strength = prediction[1] * 100
40 | this.desiredTarget = { angle, strength }
41 | }
42 |
43 | adjustAngle() {
44 | const deltaAngle = this.player.angle - this.desiredTarget.angle
45 | if (Math.abs(deltaAngle) > 0.1) {
46 | if (deltaAngle > 0)
47 | this.player.isRotatingUp = true
48 | else
49 | this.player.isRotatingDown = true
50 | } else {
51 | this.player.angle = this.desiredTarget.angle
52 | this.player.isRotatingUp = false
53 | this.player.isRotatingDown = false
54 | this.angleDone = true
55 | }
56 | }
57 |
58 | adjustStrength() {
59 | const deltaStrength = this.player.strength - this.desiredTarget.strength
60 | if (Math.abs(deltaStrength) > 1) {
61 | if (deltaStrength < 0)
62 | this.player.isIncreasingStrength = true
63 | else
64 | this.player.isDecreasingStrength = true
65 | } else {
66 | this.player.strength = this.desiredTarget.strength
67 | this.player.isIncreasingStrength = false
68 | this.player.isDecreasingStrength = false
69 | this.strengthDone = true
70 | }
71 | }
72 |
73 | fit(posX) {
74 | this.nn.fit([[...this._x]], human.pos.x < posX ? [[1, 0]] : [[0, 1]])
75 | }
76 |
77 | reset() {
78 | this.desiredTarget = null
79 | this.angleDone = false
80 | this.strengthDone = false
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/js/Dejavu.js:
--------------------------------------------------------------------------------
1 | class Dejavu {
2 |
3 | constructor(nn = [0], learningRate = 0.1, iterations = 100) {
4 |
5 | this.layers = { 'length': 0 };
6 |
7 | for (let i = 0; i < nn.length - 1; i++) {
8 |
9 | this.layers[i] = {};
10 | this.layers[i]['weights'] = new Matrix(nn[i + 1], nn[i], "RANDOM");
11 | this.layers[i]['bias'] = new Matrix(nn[i + 1], 1, "RANDOM");
12 | this.layers[i]['activation'] = 'sigmoid';
13 | this.layers['length'] += 1;
14 |
15 | }
16 |
17 | this.lr = learningRate;
18 |
19 | this.it = iterations;
20 |
21 | this.activations = {
22 | 'sigmoid': {
23 | 'func': x => 1 / (1 + Math.exp(-x)),
24 | 'dfunc': x => x * (1 - x)
25 | },
26 | 'relu': {
27 | 'func': x => x > 0 ? x : 0,
28 | 'dfunc': x => x > 0 ? 1 : 0
29 | },
30 | 'tanh': {
31 | 'func': x => Math.tanh(x),
32 | 'dfunc': x => 1 - (x * x)
33 | }
34 | };
35 | }
36 |
37 | predict(input) {
38 |
39 | let output = (input instanceof Matrix) ? input : new Matrix(input.length, 1, input);
40 |
41 | for (let i = 0; i < this.layers.length; i++) {
42 |
43 | output = this.layers[i]['weights'].multiply(output)
44 | this.layers[i]['output'] = output;
45 | this.layers[i]['output'].add(this.layers[i]['bias']);
46 | this.layers[i]['output'].foreach(this.activations[this.layers[i]['activation']]['func']);
47 |
48 | }
49 |
50 | return this.layers[this.layers.length - 1]['output'];
51 |
52 | }
53 |
54 | fit(inputs, labels) {
55 |
56 | let it = 0;
57 |
58 | while (it < this.it) {
59 |
60 | let s = 0;
61 |
62 | //this.shuffle( inputs, labels );
63 |
64 | for (let i = 0; i < inputs.length; i++) {
65 |
66 | const input = new Matrix(inputs[i].length, 1, inputs[i]);
67 |
68 | this.predict(input);
69 |
70 | let output_error = new Matrix(labels[0].length, 1, labels[i]);
71 |
72 | output_error.subtract(this.layers[this.layers.length - 1]['output']);
73 |
74 | let sum = 0
75 | for (let i = 0; i < output_error.data.length; i++) {
76 | sum += output_error.data[i] ** 2;
77 | }
78 | s += sum / this.layers[this.layers.length - 1]['output'].rows;
79 |
80 | for (let i = this.layers.length - 1; i >= 0; i--) {
81 |
82 | let gradient = this.layers[i]['output'].copy();
83 | gradient.foreach(this.activations[this.layers[i]['activation']]['dfunc']);
84 | gradient.hadamard(output_error);
85 | gradient.scalar(this.lr);
86 |
87 | let layer = (i) ? this.layers[i - 1]['output'].copy() : input.copy();
88 | layer.transpose();
89 | let delta = gradient.multiply(layer);
90 |
91 | this.layers[i]['weights'].add(delta);
92 | this.layers[i]['bias'].add(gradient);
93 |
94 | let error = this.layers[i]['weights'].copy()
95 | error.transpose();
96 | output_error = error.multiply(output_error);
97 |
98 | }
99 |
100 | }
101 |
102 | it++;
103 |
104 | }
105 |
106 | }
107 |
108 | shuffle(x, y) {
109 |
110 | for (let i = 0; i < y.length; i++) {
111 | const pos = Math.floor(Math.random() * y.length);
112 | const tmpy = y[i];
113 | const tmpx = x[i];
114 | y[i] = y[pos];
115 | x[i] = x[pos];
116 | y[pos] = tmpy;
117 | x[pos] = tmpx;
118 | }
119 |
120 | }
121 |
122 | save(filename) {
123 |
124 | const nn = {
125 | 'layers': this.layers,
126 | 'lr': this.lr,
127 | 'it': this.it
128 | };
129 | const blob = new Blob([JSON.stringify(nn)], { type: 'text/json' });
130 | const link = document.createElement('a');
131 | link.href = window.URL.createObjectURL(blob);
132 | link.download = filename;
133 | link.click();
134 |
135 | }
136 |
137 | load(nn) {
138 |
139 | this.lr = nn.lr;
140 |
141 | this.it = nn.it;
142 |
143 | for (let i = 0; i < nn.layers.length; i++) {
144 |
145 | const layer = nn.layers[i];
146 |
147 | this.layers[i] = {};
148 | this.layers[i]['weights'] = new Matrix(layer['weights'].rows, layer['weights'].cols, layer['weights'].data);
149 | this.layers[i]['bias'] = new Matrix(layer['bias'].rows, layer['bias'].cols, layer['bias'].data);
150 | this.layers[i]['output'] = new Matrix(layer['output'].rows, layer['output'].cols, layer['output'].data);
151 | this.layers[i]['activation'] = layer['activation'];
152 | this.layers['length'] += 1;
153 |
154 | }
155 |
156 | console.log('loaded');
157 |
158 | }
159 |
160 | }
161 |
--------------------------------------------------------------------------------
/js/Matrix.js:
--------------------------------------------------------------------------------
1 | class Matrix {
2 |
3 | constructor(rows, cols, values = 0) {
4 | this.rows = rows || 0;
5 | this.cols = cols || 0;
6 | if (values instanceof Array) {
7 | this.data = values.slice();
8 | } else if (values == "RANDOM") {
9 | this.data = Array(this.rows * this.cols).fill().map(_ => Math.random() * 2 - 1);
10 | } else {
11 | this.data = Array(this.rows * this.cols).fill(values);
12 | }
13 | }
14 |
15 | multiply(b) {
16 |
17 | if (b.rows !== this.cols) {
18 | throw new Error('Cols from Matrix A different from Rows of Matrix B');
19 | return;
20 | }
21 |
22 | let result = new Matrix(this.rows, b.cols);
23 |
24 | for (let i = 0; i < this.rows; i++) {
25 | for (let j = 0; j < b.cols; j++) {
26 | let s = 0;
27 | for (let k = 0; k < this.cols; k++) {
28 | s += this.data[i * this.cols + k] * b.data[k * b.cols + j];
29 | }
30 | result.data[i * result.cols + j] = s;
31 | }
32 | }
33 | return result;
34 | }
35 |
36 | transpose() {
37 | for (let i = 0; i < this.rows; i++) {
38 | for (let j = 0; j < this.cols; j++) {
39 | let temp = this.data[i * this.cols + j];
40 | this.data[i * this.cols + j] = this.data[j * this.rows + i];
41 | this.data[j * this.rows + i] = temp;
42 | }
43 | }
44 | let temp = this.cols;
45 | this.cols = this.rows;
46 | this.rows = temp;
47 | }
48 |
49 | add(a) {
50 | if (this.rows != a.rows || this.cols != a.cols) {
51 | throw new Error('Cant add Matrix of different sizes!');
52 | return;
53 | }
54 | for (let i = 0; i < this.data.length; i++) {
55 | this.data[i] += a.data[i];
56 | }
57 | }
58 |
59 | subtract(a) {
60 | if (this.rows != a.rows || this.cols != a.cols) {
61 | throw new Error('Cant subtract Matrix of different sizes!');
62 | return;
63 | }
64 | for (let i = 0; i < this.data.length; i++) {
65 | this.data[i] -= a.data[i];
66 | }
67 | }
68 |
69 | scalar(a) {
70 | for (let i = 0; i < this.data.length; i++) {
71 | this.data[i] *= a;
72 | }
73 | }
74 |
75 | hadamard(a) {
76 | if (this.rows != a.rows || this.cols != a.cols) {
77 | throw new Error('Cant multiply Matrix of different sizes!');
78 | return;
79 | }
80 | for (let i = 0; i < this.data.length; i++) {
81 | this.data[i] *= a.data[i];
82 | }
83 | }
84 |
85 | copy() {
86 | return new Matrix(this.rows, this.cols, this.data);
87 | }
88 |
89 | foreach(func) {
90 | for (let i = 0; i < this.data.length; i++) {
91 | this.data[i] = func(this.data[i]);
92 | }
93 | }
94 |
95 | }
96 |
--------------------------------------------------------------------------------
/js/main.js:
--------------------------------------------------------------------------------
1 | let canvas, c, w, h, scale, human, computerController, u, cannonBall, turn, gameOver, oldY
2 |
3 | const TWOPI = 2 * Math.PI
4 |
5 | const FRAMES_PER_SECOND = 60
6 |
7 | const FRAME_MIN_TIME = (1000/60) * (60 / FRAMES_PER_SECOND) - (1000/60) * 0.5
8 |
9 | const $ = _ => document.querySelector(_)
10 |
11 | const $c = _ => document.createElement(_)
12 |
13 | const init = () => {
14 | lastFrameTime = 0
15 | scale = window.devicePixelRatio
16 | u && cancelAnimationFrame(u)
17 | canvas = $('canvas')
18 | w = Math.min(innerWidth, 1920)
19 | h = Math.min(innerHeight, 1080)
20 | canvas.width = w * scale
21 | canvas.height = h * scale
22 | canvas.style.width = w + 'px'
23 | canvas.style.height = h + 'px'
24 | c = canvas.getContext('2d')
25 | c.scale(scale, scale)
26 | cannonBall = null
27 | human = new Cannon({ x: w - 100, y: h - 100 }, 0, true)
28 | const computer = new Cannon({ x: 100, y: h - 100 }, 1)
29 | const nn = new Dejavu([4, 2, 2], 0.1, 10)
30 | computerController = new ComputerController(computer, nn)
31 | turn = 0
32 | gameOver = false
33 | mainLoop(0)
34 | }
35 |
36 | const mainLoop = time => {
37 | if(time - lastFrameTime < FRAME_MIN_TIME){
38 | u = requestAnimationFrame(mainLoop)
39 | return
40 | }
41 | lastFrameTime = time
42 | if (turn === human.turn)
43 | human.update()
44 | else
45 | computerController.update()
46 | cannonBall && cannonBall.update()
47 | draw()
48 | u = requestAnimationFrame(mainLoop)
49 | }
50 |
51 | const draw = () => {
52 | c.clearRect(0, 0, w, h)
53 | cannonBall && cannonBall.show()
54 | human.show()
55 | computerController.player.show()
56 | c.textAlign = 'left'
57 | c.fillText(turn ? 'Computer' : 'Human', 10, 10)
58 | if (gameOver) {
59 | c.textAlign = 'center'
60 | c.fillText(["Human", "Computer"][turn] + ' wins', w / 2, h / 2)
61 | c.fillText('Press shoot to restart', w / 2, h / 2 + 40)
62 | }
63 | }
64 |
65 | const changeTurn = () => {
66 | turn = (turn + 1) % 2
67 | if (turn) computerController.done = false
68 | }
69 |
70 | const bindEvents = () => {
71 | $('body').addEventListener('keydown', e => {
72 | switch (e.keyCode) {
73 | case 32:
74 | human.shoot()
75 | if (gameOver) init()
76 | break
77 | case 37:
78 | human.isRotatingDown = true
79 | break
80 | case 38:
81 | human.isIncreasingStrength = true
82 | break
83 | case 39:
84 | human.isRotatingUp = true
85 | break;
86 | case 40:
87 | human.isDecreasingStrength = true
88 | break
89 | }
90 | })
91 | $('body').addEventListener('keyup', e => {
92 | switch (e.keyCode) {
93 | case 37:
94 | human.isRotatingDown = false
95 | break
96 | case 38:
97 | human.isIncreasingStrength = false
98 | break
99 | case 39:
100 | human.isRotatingUp = false
101 | break;
102 | case 40:
103 | human.isDecreasingStrength = false
104 | break
105 | }
106 | })
107 | $('body').addEventListener('touchmove', e => {
108 | const touch = e.changedTouches[0]
109 | if (!oldY)
110 | oldY = 0
111 | const deltaY = oldY - touch.clientY
112 | if (touch.clientX > w / 2) {
113 | if (deltaY > 0) {
114 | human.isRotatingUp = true
115 | human.isRotatingDown = false
116 | } else {
117 | human.isRotatingUp = false
118 | human.isRotatingDown = true
119 | }
120 | } else {
121 | if (deltaY > 0) {
122 | human.isIncreasingStrength = true
123 | human.isDecreasingStrength = false
124 | } else {
125 | human.isIncreasingStrength = false
126 | human.isDecreasingStrength = true
127 | }
128 | }
129 | oldY = touch.clientY
130 | })
131 | $('body').addEventListener('touchend', e => {
132 | human.isRotatingDown = false
133 | human.isRotatingUp = false
134 | human.isIncreasingStrength = false
135 | human.isDecreasingStrength = false
136 | })
137 | $('body').addEventListener('click', e => {
138 | if (scale == 1) return
139 | human.shoot()
140 | if (gameOver) init()
141 | })
142 | }
143 |
144 | bindEvents()
145 | init()
146 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/victorqribeiro/bangBang/4a4b48ef3f6b72e3ee0a998299dcd7eecdfc9f52/screenshot.png
--------------------------------------------------------------------------------