├── .github
└── FUNDING.yml
├── LICENSE
├── README.md
├── artwork.png
├── controls.png
├── css
└── main.css
├── favicon.png
├── index.html
├── js
├── Bullet.js
├── Dejavu.js
├── Genetics.js
├── GuiControls.js
├── Matrix.js
├── Player.js
├── aux.js
└── main.js
├── manifest.json
├── sounds
└── shoot.mp3
└── sw.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | patreon: victorqribeiro
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 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 | # Aim and Shoot
2 |
3 | 
4 |
5 | You're Nole Ksum (the k is silent), a citizen concern about the uprising of the machine who decided to take matters into your own hands and put an end to all artificial intelligence. You must kill all the evil robots controlled by Neural Networks and stop them from evolving into more dangerous beings. The entire human race counts on you, don't let them down.
6 |
7 | Play it [here](https://victorribeiro.com/aimAndShoot) | Alternative link [here](https://victorqribeiro.github.io/aimAndShoot/)
8 |
9 | ## How To Play
10 |
11 | ### Controls
12 |
13 | **w, a, s, d** - Move the player up, left, down, right. Arrow keys do the same.
14 |
15 | **mouse** - Aims and shoots (click).
16 |
17 | ### I do not recommend using this on a mobile, but if you must
18 |
19 | 
20 |
21 | The big left circle moves the player.
22 | The big right circle aims the player.
23 | The two little circles above them, shoot.
24 |
25 | You've been warned.
26 |
27 | ### Objectives
28 |
29 | Kill the bots, don't get killed. Also don't touch the borders of the screen, they hurt you. But, feel free to push the bots into them.
30 |
31 | ### Status Bars
32 |
33 | Above the bots there are two status bars.
34 | The red one indicates health, if it's empty you die.
35 | The green one is the cool down meter, if it's empty you can't shoot until it regenerates.
36 |
37 | ## About the Experiment
38 |
39 | I've always wanted to take the time to make a [Neuroevolution](https://en.wikipedia.org/wiki/Neuroevolution) experiment, so I did.
40 |
41 | Each bot is controlled by it's own Neural Network (that I made a while back - [here](https://github.com/victorqribeiro/digitRecognition)). When all the bots die, the genetic algorithm evaluates their fitness score (based on how many shots they fired, how many hits the got, how many friends they shot, how much they hurt themselves and how much they moved during the round) and cross the ones with the highest scores.
42 |
43 |
44 |
45 |
46 | This goes on forever, until the player dies (which will happen eventually, so Nole can't never save the human race, after all). By the way, the background history is a joke. I don't mean to make fun of anyone. The idea just seems funny and fit the project.
47 |
48 |
49 | *Fun Fact: the artwork was created using my [PaintDraw](https://github.com/victorqribeiro/paintDraw) tool.*
50 |
51 | ------
52 | [](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=76N3LUCQ9FENS¤cy_code=BRL&source=url)
53 |
--------------------------------------------------------------------------------
/artwork.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/victorqribeiro/aimAndShoot/988d141892711b805f608a178bc940f6e9bb4323/artwork.png
--------------------------------------------------------------------------------
/controls.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/victorqribeiro/aimAndShoot/988d141892711b805f608a178bc940f6e9bb4323/controls.png
--------------------------------------------------------------------------------
/css/main.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | margin: 0;
3 | padding: 0;
4 | height: 100%;
5 | -webkit-touch-callout: none;
6 | -webkit-user-select: none;
7 | -khtml-user-select: none;
8 | -moz-user-select: none;
9 | -ms-user-select: none;
10 | user-select: none;
11 | }
12 |
13 | #game {
14 | display: block;
15 | width: 100%;
16 | height: 100%;
17 | }
18 |
19 | #GuiControls {
20 | position: fixed;
21 | bottom: 0;
22 | display: flex;
23 | flex-direction: column;
24 | align-items: center;
25 | justify-content: center;
26 | width: 100%;
27 | }
28 |
29 | #GuiControls > div {
30 | display: flex;
31 | align-items: center;
32 | justify-content: space-between;
33 | width: 100%;
34 | }
35 |
--------------------------------------------------------------------------------
/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/victorqribeiro/aimAndShoot/988d141892711b805f608a178bc940f6e9bb4323/favicon.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Aim and Shoot
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/js/Bullet.js:
--------------------------------------------------------------------------------
1 | class Bullet {
2 |
3 | constructor(owner, x, y, size, angle, speed, damage, targets = []){
4 |
5 | this.pos = {
6 |
7 | x: x || 0,
8 |
9 | y: y || 0
10 |
11 | };
12 |
13 | this.owner = owner || null;
14 |
15 | this.size = size || 5;
16 |
17 | this.angle = angle || 0;
18 |
19 | this.speed = speed || 0;
20 |
21 | this.damage = damage || 1;
22 |
23 | this.isGone = false;
24 |
25 | this.targets = targets;
26 |
27 | }
28 |
29 |
30 | update(){
31 |
32 | if( this.isGone )
33 |
34 | return
35 |
36 |
37 | if( this.pos.x < -this.size || this.pos.y < -this.size || this.pos.x > w+this.size || this.pos.y > h+this.size ){
38 |
39 | this.isGone = true;
40 |
41 | return;
42 |
43 | }
44 |
45 | this.pos.x += Math.cos(this.angle) * this.speed * deltaTime;
46 |
47 | this.pos.y += Math.sin(this.angle) * this.speed * deltaTime;
48 |
49 | for(let i = this.targets.length-1; i >= 0; i--){
50 |
51 | if(this.targets[i].isDead)
52 |
53 | continue
54 |
55 | if( this.distance( this.targets[i] ) < this.targets[i].size+this.size ){
56 |
57 | if(this.owner.ai !== this.targets[i].ai)
58 |
59 | this.owner.hits++;
60 |
61 | else
62 |
63 | this.owner.friendlyFire++;
64 |
65 | this.targets[i].speed.x += Math.cos(this.angle) * 0.1;
66 |
67 | this.targets[i].speed.y += Math.sin(this.angle) * 0.1;
68 |
69 | this.targets[i].health -= this.damage;
70 |
71 | this.isGone = true;
72 |
73 | break;
74 |
75 | }
76 |
77 | }
78 |
79 | }
80 |
81 |
82 | distance(target){
83 |
84 | return Math.abs(this.pos.x - target.pos.x) + Math.abs(this.pos.y - target.pos.y);
85 |
86 | }
87 |
88 |
89 | show(){
90 |
91 | c.fillStyle = "black";
92 |
93 | c.beginPath();
94 |
95 | c.arc(this.pos.x, this.pos.y, this.size, 0, TWOPI);
96 |
97 | c.fill();
98 |
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/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 |
11 | this.layers[i]['weights'] = new Matrix( nn[i+1], nn[i], "RANDOM" ) ;
12 |
13 | this.layers[i]['bias'] = new Matrix( nn[i+1], 1, "RANDOM" );
14 |
15 | this.layers[i]['activation'] = 'tanh';
16 |
17 | this.layers['length'] += 1;
18 |
19 | }
20 |
21 | this.lr = learningRate;
22 |
23 | this.it = iterations;
24 |
25 | this.activations = {
26 |
27 | 'sigmoid': {
28 |
29 | 'func': x => 1 / (1 + Math.exp(-x)),
30 |
31 | 'dfunc': x => x * (1 - x)
32 |
33 | },
34 |
35 | 'relu': {
36 |
37 | 'func': x => x < 0 ? 0 : x,
38 |
39 | 'dfunc': x => x < 0 ? 0 : 1
40 | },
41 |
42 | 'tanh': {
43 |
44 | 'func': x => Math.tanh(x),
45 |
46 | 'dfunc': x => 1 - (x * x)
47 |
48 | },
49 |
50 | 'identity': {
51 |
52 | 'func': x => x,
53 |
54 | 'dfunc': x => 1
55 |
56 | }
57 | };
58 | }
59 |
60 | predict(input){
61 |
62 | let output = ( input instanceof Matrix ) ? input : new Matrix(input.length, 1, input);
63 |
64 | for(let i = 0; i < this.layers.length; i++){
65 |
66 | output = this.layers[i]['weights'].multiply( output );
67 |
68 | this.layers[i]['output'] = output;
69 |
70 | this.layers[i]['output'].add( this.layers[i]['bias'] );
71 |
72 | this.layers[i]['output'].foreach( this.activations[ this.layers[i]['activation'] ]['func'] );
73 |
74 | }
75 |
76 | return this.layers[this.layers.length-1]['output'];
77 |
78 | }
79 |
80 | fit(inputs, labels){
81 |
82 | let it = 0;
83 |
84 | while( it < this.it ){
85 |
86 | let s = 0;
87 |
88 | this.shuffle( inputs, labels );
89 |
90 | for(let i = 0; i < inputs.length; i++){
91 |
92 | const input = new Matrix(inputs[i].length, 1, inputs[i]);
93 |
94 | this.predict( input );
95 |
96 | let output_error = new Matrix(labels[0].length, 1, labels[i]);
97 |
98 | output_error.subtract( this.layers[this.layers.length-1]['output'] );
99 |
100 | let sum = 0
101 |
102 | for(let i = 0; i < output_error.data.length; i++){
103 |
104 | sum += output_error.data[i] ** 2;
105 |
106 | }
107 |
108 | s += sum/this.layers[this.layers.length-1]['output'].rows;
109 |
110 | for(let i = this.layers.length-1; i >= 0; i--){
111 |
112 | let gradient = this.layers[i]['output'].copy();
113 |
114 | gradient.foreach( this.activations[ this.layers[i]['activation'] ]['dfunc'] );
115 |
116 | gradient.hadamard( output_error );
117 |
118 | gradient.scalar( this.lr );
119 |
120 | let layer = ( i ) ? this.layers[i-1]['output'].copy() : input.copy();
121 |
122 | layer.transpose();
123 |
124 | let delta = gradient.multiply( layer );
125 |
126 | this.layers[i]['weights'].add( delta );
127 |
128 | this.layers[i]['bias'].add( gradient );
129 |
130 | let error = this.layers[i]['weights'].copy();
131 |
132 | error.transpose();
133 |
134 | output_error = error.multiply( output_error );
135 |
136 | }
137 |
138 | }
139 |
140 | it++;
141 |
142 | if( !(it % 1) ) console.log( it, s );
143 |
144 | }
145 |
146 | }
147 |
148 | shuffle(x,y){
149 |
150 | for(let i = 0; i < y.length; i++){
151 |
152 | const pos = Math.floor( Math.random() * y.length );
153 |
154 | const tmpy = y[i];
155 |
156 | const tmpx = x[i];
157 |
158 | y[i] = y[pos];
159 |
160 | x[i] = x[pos];
161 |
162 | y[pos] = tmpy;
163 |
164 | x[pos] = tmpx;
165 |
166 | }
167 |
168 | }
169 |
170 | save(filename){
171 |
172 | const nn = {
173 |
174 | 'layers': this.layers,
175 |
176 | 'lr': this.lr,
177 |
178 | 'it': this.it
179 |
180 | };
181 |
182 | const blob = new Blob([JSON.stringify(nn)], {type: 'text/json'});
183 |
184 | const link = document.createElement('a');
185 |
186 | link.href = window.URL.createObjectURL(blob);
187 |
188 | link.download = filename;
189 |
190 | link.click();
191 |
192 | }
193 |
194 | load(nn){
195 |
196 | this.lr = nn.lr;
197 |
198 | this.it = nn.it;
199 |
200 | for(let i = 0; i < nn.layers.length; i++){
201 |
202 | const layer = nn.layers[i];
203 |
204 | this.layers[i] = {};
205 |
206 | this.layers[i]['weights'] = new Matrix( layer['weights'].rows, layer['weights'].cols, layer['weights'].data ) ;
207 |
208 | this.layers[i]['bias'] = new Matrix( layer['bias'].rows, layer['bias'].cols, layer['bias'].data );
209 |
210 | this.layers[i]['output'] = new Matrix( layer['output'].rows, layer['output'].cols, layer['output'].data );
211 |
212 | this.layers[i]['activation'] = layer['activation'];
213 |
214 | this.layers['length'] += 1;
215 |
216 | }
217 |
218 | console.log( 'loaded' );
219 |
220 | }
221 |
222 | }
223 |
--------------------------------------------------------------------------------
/js/Genetics.js:
--------------------------------------------------------------------------------
1 | class Genetics {
2 |
3 | constructor(populationSize, populationFeaturesSize){
4 |
5 | this.population = [];
6 |
7 | this.populationTmp = [];
8 |
9 | }
10 |
11 | getRandomColor(){
12 |
13 | return [Math.floor( Math.random() * 256 ),
14 |
15 | Math.floor( Math.random() * 256 ),
16 |
17 | Math.floor( Math.random() * 256 )];
18 |
19 | }
20 |
21 | createPopulation(){
22 |
23 | this.population = [];
24 |
25 | for(let i = 0; i < maxEnemies; i++){
26 |
27 | const enemy = new Player(Math.random() * w, Math.random() * h, Math.random() * TWOPI, this.getRandomColor(), true);
28 |
29 | enemy.brain = new Dejavu([6 * maxEnemies, 6, 7], 0.1, 100);
30 |
31 | this.population.push( enemy );
32 |
33 | }
34 |
35 | }
36 |
37 |
38 | divide(a, b){
39 |
40 | if(b == 0)
41 |
42 | return 0
43 |
44 | return a / b;
45 |
46 | }
47 |
48 |
49 | evaluate(){
50 |
51 | let totalBulletsFired = player.shootsFired;
52 |
53 | for(let i = 0; i < this.population.length; i++){
54 |
55 | totalBulletsFired += this.population[i].shootsFired;
56 |
57 | }
58 |
59 | for(let i = 0; i < this.population.length; i++){
60 |
61 | const agressive = this.divide(this.population[i].shootsFired, totalBulletsFired);
62 |
63 | const survial = this.divide(this.population[i].age, totalTime);
64 |
65 | const hits = this.divide(this.population[i].hits, this.population[i].shootsFired);
66 |
67 | const friendlyFire = this.divide(this.population[i].friendlyFire, this.population[i].shootsFired);
68 |
69 | const selfInjury = this.divide(this.population[i].selfInjury, 40);
70 |
71 | this.population[i].fitness += agressive * 0.23;
72 |
73 | this.population[i].fitness += survial * 0.02;
74 |
75 | this.population[i].fitness += hits * 0.55;
76 |
77 | this.population[i].fitness -= friendlyFire * 0.08;
78 |
79 | this.population[i].fitness -= selfInjury * 0.12;
80 |
81 | this.population[i].fitness *= (this.population[i].move / 100);
82 |
83 | this.population[i].fitness = Math.max(0, this.population[i].fitness);
84 |
85 | }
86 |
87 | }
88 |
89 |
90 | selectParent(){
91 |
92 | let total = 0;
93 |
94 | for(let i = 0; i < this.populationTmp.length; i++){
95 |
96 | total += this.populationTmp[i].fitness;
97 |
98 | }
99 |
100 | let prob = Math.random() * total;
101 |
102 | for(let i = 0; i < this.populationTmp.length; i++){
103 |
104 | if( prob < this.populationTmp[i].fitness ){
105 |
106 | return this.populationTmp.splice(i,1)[0];
107 |
108 | }
109 |
110 | prob -= this.populationTmp[i].fitness
111 |
112 | }
113 |
114 | return null
115 |
116 | }
117 |
118 |
119 | crossOver(a, b){
120 |
121 | if( !a ){
122 |
123 | a = new Player( Math.random() * w, Math.random() * h, Math.random() * TWOPI, this.getRandomColor(), true);
124 |
125 | a.brain = new Dejavu([6 * maxEnemies, 6, 7], 0.1, 100);
126 |
127 | }
128 |
129 | if( !b ){
130 |
131 | b = new Player( Math.random() * w, Math.random() * h, Math.random() * TWOPI, this.getRandomColor(), true);
132 |
133 | b.brain = new Dejavu([6 * maxEnemies, 6, 7], 0.1, 100);
134 |
135 | }
136 |
137 |
138 | const color = Array(3);
139 |
140 | for(let i = 0; i < color.length; i++){
141 |
142 | if( Math.random() < 0.5 )
143 |
144 | color[i] = (a.color[i] + b.color[i]) / 2;
145 |
146 | else
147 |
148 | color[i] = Math.random() < 0.5 ? a.color[i] : b.color[i];
149 |
150 | }
151 |
152 | const child = new Player( Math.random() * w, Math.random() * h, Math.random() * TWOPI, color, true);
153 |
154 | child.brain = new Dejavu([6 * maxEnemies, 6, 7], 0.1, 100);
155 |
156 | for(let i = 0; i < child.brain.layers.length; i++){
157 |
158 | for(let j = 0; j < child.brain.layers[i].bias.data.length; j++){
159 |
160 | if( !(j%2) )
161 |
162 | child.brain.layers[i].bias.data[j] = a.brain.layers[i].bias.data[j];
163 |
164 | else
165 |
166 | child.brain.layers[i].bias.data[j] = b.brain.layers[i].bias.data[j];
167 |
168 | }
169 |
170 | for(let j = 0; j < child.brain.layers[i].weights.data.length; j++){
171 |
172 | if( j%2 )
173 |
174 | child.brain.layers[i].weights.data[j] = a.brain.layers[i].weights.data[j];
175 |
176 | else
177 |
178 | child.brain.layers[i].weights.data[j] = b.brain.layers[i].weights.data[j];
179 |
180 | }
181 |
182 | }
183 |
184 | return child;
185 |
186 | }
187 |
188 |
189 | mutate(child){
190 |
191 | for(let i = 0, end = Math.floor( Math.random() * 3); i < end; i++){
192 |
193 | child.color[ Math.floor( Math.random() * 3) ] = Math.floor( Math.random() * 256 );
194 |
195 | }
196 |
197 | const what = Math.random() > 0.5 ? 'bias' : 'weights';
198 |
199 | for(let i = 0; i < child.brain.layers.length; i++){
200 |
201 | for(let j = 0; j < child.brain.layers[i][what].data.length; j += 2){
202 |
203 | child.brain.layers[i][what].data[j] = Math.random() * 2 - 1;
204 |
205 | }
206 |
207 | }
208 |
209 | return child;
210 |
211 | }
212 |
213 |
214 | evolve(){
215 |
216 | this.evaluate();
217 |
218 | let newPopulation = [];
219 |
220 | for(let x = 0; x < this.population.length; x++){
221 |
222 | this.populationTmp = this.population.slice();
223 |
224 | let a = this.selectParent();
225 |
226 | let b = this.selectParent();
227 |
228 | let child = this.crossOver(a,b);
229 |
230 | if( Math.random() < 0.25 ){
231 |
232 | child = this.mutate(child);
233 |
234 | }
235 |
236 | newPopulation.push( child );
237 |
238 | }
239 |
240 | this.population = newPopulation;
241 |
242 | }
243 |
244 |
245 | }
246 |
--------------------------------------------------------------------------------
/js/GuiControls.js:
--------------------------------------------------------------------------------
1 | class GuiControls {
2 |
3 | constructor(){
4 |
5 | document.body.addEventListener('touchstart', e => {
6 |
7 | e.preventDefault();
8 |
9 | });
10 |
11 | document.body.addEventListener('touchend', e => {
12 |
13 | e.preventDefault();
14 |
15 | });
16 |
17 | document.body.addEventListener('touchmove', e => {
18 |
19 | e.preventDefault();
20 |
21 | });
22 |
23 | this.main = document.createElement('div');
24 |
25 | this.main.id = "GuiControls";
26 |
27 | document.body.appendChild( this.main );
28 |
29 | const top = document.createElement('div');
30 |
31 | top.appendChild( this.createFire() );
32 |
33 | top.appendChild( this.createFire() );
34 |
35 | this.main.appendChild( top )
36 |
37 | const bottom = document.createElement('div');
38 |
39 | const directional = this.createDirectional();
40 |
41 | bottom.appendChild( directional );
42 |
43 | const lookAt = this.createLookAt();
44 |
45 | bottom.appendChild( lookAt );
46 |
47 | this.main.appendChild( bottom )
48 |
49 | }
50 |
51 | createCanvas(size){
52 |
53 | const canvas = document.createElement('canvas');
54 |
55 | canvas.width = size || 128;
56 |
57 | canvas.height = size || 128;
58 |
59 | const cd = canvas.getContext('2d');
60 |
61 | cd.beginPath();
62 |
63 | cd.arc(size/2, size/2, size/2, 0, 2 * Math.PI);
64 |
65 | cd.fillStyle = "rgba(0,0,0,0.3)"
66 |
67 | cd.fill();
68 |
69 | return canvas;
70 |
71 | }
72 |
73 | createDirectional(){
74 |
75 | const directional = this.createCanvas(128);
76 |
77 | const notMove = function(){
78 |
79 | player.isMoving.right = false;
80 |
81 | player.isMoving.left = false;
82 |
83 | player.isMoving.up = false;
84 |
85 | player.isMoving.down = false;
86 |
87 | }
88 |
89 | const move = function(e){
90 |
91 | e.preventDefault();
92 |
93 | const rect = directional.getBoundingClientRect(),
94 |
95 | _x = directional.width/rect.width,
96 |
97 | _y = directional.height/rect.height;
98 |
99 | let x = (e.targetTouches[0].pageX - rect.left) * _x;
100 |
101 | let y = (e.targetTouches[0].pageY - rect.top) * _y;
102 |
103 | if( x > 64 )
104 |
105 | player.isMoving.right = true;
106 |
107 | else
108 |
109 | player.isMoving.left = true;
110 |
111 | if( y < 64 )
112 |
113 | player.isMoving.up = true;
114 |
115 | else
116 |
117 | player.isMoving.down = true;
118 |
119 | }
120 |
121 | directional.addEventListener('touchstart', e => {
122 |
123 | move(e)
124 |
125 | });
126 |
127 | directional.addEventListener('touchmove', e => {
128 |
129 | notMove();
130 |
131 | move(e);
132 |
133 | });
134 |
135 | directional.addEventListener('touchend', e => {
136 |
137 | notMove();
138 |
139 | });
140 |
141 | return directional;
142 |
143 | }
144 |
145 | createLookAt(){
146 |
147 | const look = this.createCanvas(128);
148 |
149 | look.addEventListener('touchmove', e => {
150 |
151 | e.preventDefault();
152 |
153 | const rect = look.getBoundingClientRect(),
154 | _x = look.width/rect.width,
155 | _y = look.height/rect.height;
156 |
157 |
158 | let x = (e.targetTouches[0].pageX - rect.left) * _x;
159 | let y = (e.targetTouches[0].pageY - rect.top) * _y;
160 |
161 |
162 | player.lookAt( (x/128*w)*2-w2, (y/128*h)*2-h2 );
163 |
164 | });
165 |
166 | return look;
167 |
168 | }
169 |
170 | createFire(){
171 |
172 | const btn = this.createCanvas(64);
173 |
174 | btn.addEventListener('touchstart', e => {
175 |
176 | e.preventDefault();
177 |
178 | if( isGameover ){
179 |
180 | init();
181 |
182 | return;
183 |
184 | }
185 |
186 | if( isStarting ){
187 |
188 | isStarting = false;
189 |
190 | update();
191 |
192 | return;
193 |
194 | }
195 |
196 | player.isShooting = true;
197 |
198 | });
199 |
200 | btn.addEventListener('touchend', e => {
201 |
202 | player.isShooting = false;
203 |
204 | });
205 |
206 | return btn;
207 |
208 | }
209 |
210 | }
211 |
--------------------------------------------------------------------------------
/js/Matrix.js:
--------------------------------------------------------------------------------
1 | class Matrix {
2 |
3 | constructor(rows, cols, values = 0){
4 |
5 | this.rows = rows || 0;
6 |
7 | this.cols = cols || 0;
8 |
9 | if( values instanceof Array ){
10 |
11 | if( this.rows * this.cols != values.length ){
12 |
13 | console.log( this.rows, this.cols, values.length );
14 |
15 | throw new Error('The number of rows * cols should be equal to the length of the values');
16 |
17 | }
18 |
19 | this.data = values.slice();
20 |
21 | }else if( values == "RANDOM" ){
22 |
23 | this.data = Array( this.rows * this.cols ).fill().map( _ => Math.random() * 2 - 1 );
24 |
25 | }else{
26 |
27 | this.data = Array( this.rows * this.cols ).fill( values );
28 |
29 | }
30 |
31 | }
32 |
33 | multiply(b){
34 |
35 | if( b.rows !== this.cols ){
36 |
37 | throw new Error('Cols from Matrix A should be equal to Rows of Matrix B');
38 |
39 | }
40 |
41 | let result = new Matrix( this.rows, b.cols );
42 |
43 | for(let i=0, j=0; i this.size && this.pos.x + _x < w - this.size )
160 |
161 | this.pos.x += _x;
162 |
163 | else{
164 |
165 | this.speed.x = -this.speed.x;
166 |
167 | this.selfInjury += 1;
168 |
169 | this.health -= 0.25;
170 |
171 | }
172 |
173 | if( this.pos.y + _y > this.size && this.pos.y + _y < h - this.size )
174 |
175 | this.pos.y += _y;
176 |
177 | else{
178 |
179 | this.speed.y = -this.speed.y
180 |
181 | this.selfInjury += 1;
182 |
183 | this.health -= 0.25;
184 |
185 | }
186 |
187 | this.speed.x *= this.friction;
188 |
189 | this.speed.y *= this.friction;
190 |
191 | for(let i = 0; i < players.length; i++){
192 |
193 | if( players[i] == this || players[i].isDead )
194 |
195 | continue
196 |
197 | if( this.distance( players[i] ) <= players[i].size + this.size ) {
198 |
199 | players[i].speed.x += (this.speed.x);
200 |
201 | players[i].speed.y += (this.speed.y);
202 |
203 | this.speed.x += -players[i].speed.x;
204 |
205 | this.speed.y += -players[i].speed.y;
206 |
207 | this.speed.x *= 0.005;
208 |
209 | this.speed.y *= 0.005;
210 |
211 | }
212 |
213 | }
214 |
215 | if( this.isShooting && this.coolDown > 0 && this.spread < 1 ){
216 |
217 | if(aPlayer.paused)
218 |
219 | aPlayer.play().then( _ => _ ).catch( e => e );
220 |
221 | else
222 |
223 | aPlayer.currentTime = 0
224 |
225 | this.spread = this.spreadInit;
226 |
227 | this.coolDown -= 1
228 |
229 | const targets = players.slice( 0 );
230 |
231 | targets.splice( targets.indexOf(this), 1);
232 |
233 | bullets.push(
234 |
235 | new Bullet( this, this.pos.x + Math.cos(this.angle) * 40, this.pos.y + Math.sin(this.angle) * 40, 5, this.angle, 1.2, 1, targets)
236 |
237 | );
238 |
239 | this.shootsFired++;
240 |
241 | }
242 |
243 | if( this.coolDown < this.coolDownInit && !this.isShooting )
244 |
245 | this.coolDown += 0.25;
246 |
247 | this.spread -= 1;
248 |
249 | }
250 |
251 | distance(target){
252 |
253 | return Math.sqrt( (this.pos.x - target.pos.x)**2 + (this.pos.y - target.pos.y)**2 );
254 |
255 | }
256 |
257 | updateAI(target){
258 |
259 | const data = Array( 6 * maxEnemies ).fill(0);
260 |
261 | let t = 0, i = 0;
262 |
263 | while(t < maxEnemies){
264 |
265 | t++;
266 |
267 | if( players[i] === this )
268 |
269 | continue
270 |
271 | data[i*5+0] = players[i].isDead ? 0 : players[i].pos.x / w;
272 |
273 | data[i*5+1] = players[i].isDead ? 0 : players[i].pos.y / h;
274 |
275 | data[i*5+2] = players[i].isDead ? 0 : players[i].looking.x / w;
276 |
277 | data[i*5+3] = players[i].isDead ? 0 : players[i].looking.y / h;
278 |
279 | data[i*5+4] = players[i].isDead ? 0 : players[i].isShooting ? 1 : 0;
280 |
281 | data[i*5+5] = players[i].isDead ? 0 : players[i].ai ? 1 : 0;
282 |
283 | i++;
284 |
285 | }
286 |
287 | const action = this.brain.predict( data ).data;
288 |
289 | action[0] > 0.5 ? this.isMoving.left = true : this.isMoving.left = false;
290 |
291 | action[1] > 0.5 ? this.isMoving.up = true : this.isMoving.up = false;
292 |
293 | action[2] > 0.5 ? this.isMoving.right = true : this.isMoving.right = false;
294 |
295 | action[3] > 0.5 ? this.isMoving.down = true : this.isMoving.down = false;
296 |
297 | this.looking.x = action[4] * w;
298 |
299 | this.looking.y = action[5] * h;
300 |
301 | action[6] > 0.5 ? this.isShooting = true : this.isShooting = false;
302 |
303 | }
304 |
305 |
306 | showHealthBar(){
307 |
308 | c.fillStyle = "red";
309 |
310 | c.fillRect(this.pos.x - 50, this.pos.y - 60, this.health * 10 , 10);
311 |
312 | c.strokeRect(this.pos.x - 50, this.pos.y - 60, 100, 10);
313 |
314 | }
315 |
316 | showCooldownBar(){
317 |
318 | c.fillStyle = "green";
319 |
320 | c.fillRect(this.pos.x - 50, this.pos.y - 45, Math.max(0, this.coolDown / this.coolDownInit * 100) , 10);
321 |
322 | c.strokeRect(this.pos.x - 50, this.pos.y - 45, 100, 10);
323 |
324 | }
325 |
326 | show(){
327 |
328 | if( this.isDead ){
329 |
330 | this.iAnim += 0.1
331 |
332 | c.fillStyle = "rgba("+this.color[0]+","+this.color[1]+","+this.color[2]+","+(1-this.iAnim)+")";
333 |
334 | c.beginPath();
335 |
336 | c.arc(this.pos.x, this.pos.y, this.size, 0, TWOPI);
337 |
338 | c.fill();
339 |
340 | c.save();
341 |
342 | c.translate(this.pos.x, this.pos.y);
343 |
344 | c.rotate(this.angle+this.iAnim);
345 |
346 | c.fillRect(this.iAnim*50, -9, 50, 18);
347 |
348 | c.restore();
349 |
350 | return
351 |
352 | }
353 |
354 | this.showHealthBar();
355 |
356 | this.showCooldownBar();
357 |
358 | c.fillStyle = "rgb("+this.color[0]+","+this.color[1]+","+this.color[2]+")";
359 |
360 | c.shadowColor = "black";
361 |
362 | c.shadowBlur = 5;
363 |
364 | c.save();
365 |
366 | c.translate(this.pos.x, this.pos.y);
367 |
368 | c.rotate(this.angle);
369 |
370 | c.fillRect(0, -9, 50, 18);
371 |
372 | c.restore();
373 |
374 | c.beginPath();
375 |
376 | c.arc(this.pos.x, this.pos.y, this.size, 0, TWOPI);
377 |
378 | c.fill();
379 |
380 | c.shadowBlur = 0;
381 |
382 | }
383 |
384 | }
385 |
--------------------------------------------------------------------------------
/js/aux.js:
--------------------------------------------------------------------------------
1 | if('serviceWorker' in navigator) {
2 | navigator.serviceWorker
3 | .register('/aimAndShoot/sw.js', {scope: './'})
4 | .then(response => response)
5 | .catch(reason => reason);
6 | }
7 |
8 | let deferredPrompt;
9 | const addBtn = document.createElement('button');
10 |
11 | window.addEventListener('beforeinstallprompt', (e) => {
12 | e.preventDefault();
13 | deferredPrompt = e;
14 | addBtn.style.display = 'block';
15 | addBtn.addEventListener('click', (e) => {
16 | addBtn.style.display = 'none';
17 | deferredPrompt.prompt();
18 | deferredPrompt.userChoice.then((choiceResult) => {
19 | deferredPrompt = null;
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/js/main.js:
--------------------------------------------------------------------------------
1 | let artwork, canvas, rect, _x, _y, c, w, h, w2, h2, TWOPI, genetics, player, enemies, bullets, players, prevTime, nextTime, deltaTime, startTime, totalTime, isGameover, u, aPlayer, maxEnemies, generation = 1, isStarting = true;
2 |
3 | const init = function(){
4 |
5 | maxEnemies = 7;
6 |
7 | isGameover = false;
8 |
9 | const oldCanvas = document.querySelector('#game');
10 |
11 | if( oldCanvas )
12 |
13 | oldCanvas.remove();
14 |
15 | else
16 |
17 | addEventsListener();
18 |
19 | canvas = document.createElement('canvas');
20 |
21 | canvas.id = "game";
22 |
23 | canvas.width = w = 1366;
24 |
25 | canvas.height = h = 768;
26 |
27 | w2 = w/2;
28 |
29 | h2 = h/2;
30 |
31 | TWOPI = Math.PI * 2;
32 |
33 | prevTime = nextTime = deltaTime = startTime = Date.now();
34 |
35 | totalTime = 0;
36 |
37 | c = canvas.getContext('2d');
38 |
39 | c.font = "25px Arial";
40 |
41 | c.textAlign = "center";
42 |
43 | document.body.appendChild(canvas);
44 |
45 | rect = canvas.getBoundingClientRect();
46 |
47 | _x = w/rect.width;
48 |
49 | _y = h/rect.height;
50 |
51 | genetics = new Genetics();
52 |
53 | genetics.createPopulation();
54 |
55 | player = new Player();
56 |
57 | enemies = genetics.population.slice();
58 |
59 | bullets = Array();
60 |
61 | players = [player, ...enemies];
62 |
63 | if( isStarting ){
64 |
65 | startScreen();
66 |
67 | }else{
68 |
69 | update();
70 |
71 | }
72 |
73 | }
74 |
75 |
76 | const update = function(){
77 |
78 | nextTime = Date.now();
79 |
80 | deltaTime = nextTime - prevTime;
81 |
82 | totalTime += deltaTime;
83 |
84 | for(let i = bullets.length-1; i >= 0; i--){
85 |
86 | bullets[i].update();
87 |
88 | if( bullets[i].isGone )
89 |
90 | bullets.splice(i, 1)
91 |
92 | }
93 |
94 | for(let i = players.length-1; i >= 0 ; i--){
95 |
96 | if( !players[i].isDead )
97 |
98 | players[i].update(player);
99 |
100 | }
101 |
102 | draw();
103 |
104 | if( player.isDead ){
105 |
106 | gameover()
107 |
108 | return
109 |
110 | }
111 |
112 | let allDead = true;
113 |
114 | for(let i = 0; i < enemies.length; i++ ){
115 |
116 | if( !enemies[i].isDead ){
117 |
118 | allDead = false;
119 |
120 | break;
121 |
122 | }
123 |
124 | }
125 |
126 | if( allDead ){
127 |
128 | endRound()
129 |
130 | return
131 |
132 | }
133 |
134 | prevTime = nextTime;
135 |
136 | u = requestAnimationFrame( update );
137 |
138 | }
139 |
140 |
141 | const draw = function(){
142 |
143 | c.clearRect(0, 0, w, h);
144 |
145 | for(let i = 0; i < bullets.length; i++){
146 |
147 | bullets[i].show();
148 |
149 | }
150 |
151 | for(let i = 0; i < players.length; i++){
152 |
153 | players[i].show();
154 |
155 | }
156 |
157 | c.textAlign = "start";
158 |
159 | c.fillStyle = "black";
160 |
161 | c.fillText("Generation: "+generation, 10, 30 )
162 |
163 | c.textAlign = "center";
164 |
165 | }
166 |
167 | const endRound = function(){
168 |
169 | totalTime = (Date.now() - startTime) / 1000;
170 |
171 | genetics.evolve();
172 |
173 | enemies = genetics.population.slice();
174 |
175 | players = [player, ...enemies];
176 |
177 | startTime = Date.now();
178 |
179 | generation += 1;
180 |
181 | player.health = Math.min(10, player.health + player.health * 0.15)
182 |
183 | update();
184 |
185 | }
186 |
187 | const startScreen = function(){
188 |
189 | c.clearRect(0,0,w,h);
190 |
191 | c.drawImage(artwork, 0, 0, artwork.width, artwork.height, 0, 0, w, h);
192 |
193 | c.fillColor = "black";
194 |
195 | c.fillText("Click to Start", w-w2/2, h2 )
196 |
197 | }
198 |
199 | const gameover = function(){
200 |
201 | if(u)
202 |
203 | cancelAnimationFrame(u)
204 |
205 | generation = 1;
206 |
207 | let i = 0;
208 |
209 | const drawGameover = function(){
210 |
211 | c.fillStyle = "rgba(0,0,0,"+(i += 0.01)+")";
212 |
213 | c.fillRect(0,0,w,h);
214 |
215 | c.fillStyle = "white";
216 |
217 | c.fillText("You have failed the human race.", w2, h2-25);
218 |
219 | c.fillText("You should move to mars or something.", w2, h2+25);
220 |
221 | if( i <= 1 ){
222 |
223 | requestAnimationFrame( drawGameover );
224 |
225 | }else{
226 |
227 | c.fillText("Click to try again.", w2, h2/2);
228 |
229 | isGameover = true;
230 |
231 | }
232 |
233 | }
234 |
235 | drawGameover();
236 |
237 | }
238 |
239 | const addEventsListener = function(){
240 |
241 | document.body.addEventListener('mousemove', e => {
242 |
243 | player.lookAt(e.clientX * _x, e.clientY * _y);
244 |
245 | });
246 |
247 |
248 | document.body.addEventListener('keydown', e => {
249 |
250 | e.preventDefault();
251 |
252 | switch(e.keyCode){
253 |
254 | case 37 :
255 | case 65 :
256 |
257 | player.isMoving.left = true;
258 |
259 | break;
260 |
261 | case 38 :
262 | case 87 :
263 |
264 | player.isMoving.up = true;
265 |
266 | break;
267 |
268 | case 39 :
269 | case 68 :
270 |
271 | player.isMoving.right = true;
272 |
273 | break;
274 |
275 | case 40 :
276 | case 83 :
277 |
278 | player.isMoving.down = true;
279 |
280 | break;
281 | }
282 |
283 | });
284 |
285 |
286 | document.body.addEventListener('keyup', e => {
287 |
288 | e.preventDefault();
289 |
290 | switch(e.keyCode){
291 |
292 | case 37 :
293 | case 65 :
294 |
295 | player.isMoving.left = false;
296 |
297 | break;
298 |
299 | case 38 :
300 | case 87 :
301 |
302 | player.isMoving.up = false;
303 |
304 | break;
305 |
306 | case 39 :
307 | case 68 :
308 |
309 | player.isMoving.right = false;
310 |
311 | break;
312 |
313 | case 40 :
314 | case 83 :
315 |
316 | player.isMoving.down = false;
317 |
318 | break;
319 | }
320 |
321 | });
322 |
323 |
324 | document.body.addEventListener('mouseup', e => {
325 |
326 | e.preventDefault();
327 |
328 | player.isShooting = false;
329 |
330 | });
331 |
332 | document.body.addEventListener('mousedown', e => {
333 |
334 | e.preventDefault();
335 |
336 | if( isGameover ){
337 |
338 | init();
339 |
340 | return;
341 |
342 | }
343 |
344 | if( isStarting ){
345 |
346 | isStarting = false;
347 |
348 | update();
349 |
350 | return;
351 |
352 | }
353 |
354 | player.isShooting = true;
355 |
356 | });
357 |
358 | window.onresize = _ => {
359 |
360 | if(u)
361 |
362 | cancelAnimationFrame(u)
363 |
364 | isStarting = true;
365 |
366 | init();
367 |
368 | }
369 |
370 | }
371 |
372 | aPlayer = document.createElement('audio');
373 |
374 | aPlayer.src = "sounds/shoot.mp3";
375 |
376 | artwork = new Image();
377 |
378 | artwork.src = "artwork.png";
379 |
380 | artwork.onload = _ => {
381 |
382 | init();
383 |
384 | if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ){
385 |
386 | const control = new GuiControls();
387 |
388 | }
389 |
390 | }
391 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Aim And Shoot",
3 | "short_name": "aimAndShoot",
4 | "description": "A Neural Evolution Game Experiment",
5 | "start_url": "index.html",
6 | "display": "standalone",
7 | "orientation": "landscape",
8 | "background_color": "#FFF",
9 | "theme_color": "#FFF",
10 | "icons": [
11 | {
12 | "src": "favicon.png",
13 | "sizes": "256x256",
14 | "type": "image/png",
15 | "density": 1
16 | },
17 | {
18 | "src": "favicon.png",
19 | "sizes": "512x512",
20 | "type": "image/png",
21 | "density": 1
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/sounds/shoot.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/victorqribeiro/aimAndShoot/988d141892711b805f608a178bc940f6e9bb4323/sounds/shoot.mp3
--------------------------------------------------------------------------------
/sw.js:
--------------------------------------------------------------------------------
1 | const filesToCache = [
2 | './',
3 | './index.html',
4 | './js/main.js',
5 | './js/Matrix.js',
6 | './js/Dejavu.js',
7 | './js/Genetics.js',
8 | './js/Player.js',
9 | './js/Bullet.js',
10 | './css/main.css',
11 | './sounds/shoot.mp3',
12 | './favicon.png',
13 | './artwork.png',
14 | './manifest.json'
15 | ];
16 |
17 | const staticCacheName = 'aimAndShoot-v1';
18 |
19 | self.addEventListener('install', event => {
20 | event.waitUntil(
21 | caches.open(staticCacheName)
22 | .then(cache => {
23 | return cache.addAll(filesToCache);
24 | })
25 | );
26 | });
27 |
28 | self.addEventListener('fetch', event => {
29 | event.respondWith(
30 | caches.match(event.request)
31 | .then(response => {
32 | if (response) {
33 | return response;
34 | }
35 |
36 | return fetch(event.request)
37 |
38 | .then(response => {
39 | return caches.open(staticCacheName).then(cache => {
40 | cache.put(event.request.url, response.clone());
41 | return response;
42 | });
43 | });
44 |
45 | }).catch(error => {})
46 | );
47 | });
48 |
49 | self.addEventListener('activate', event => {
50 |
51 | const cacheWhitelist = [staticCacheName];
52 |
53 | event.waitUntil(
54 | caches.keys().then(cacheNames => {
55 | return Promise.all(
56 | cacheNames.map(cacheName => {
57 | if (cacheWhitelist.indexOf(cacheName) === -1) {
58 | return caches.delete(cacheName);
59 | }
60 | })
61 | );
62 | })
63 | );
64 | });
65 |
--------------------------------------------------------------------------------