├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── bundle-bare.js ├── bundle-main.js ├── bundle.js ├── display ├── ancestry │ ├── display.js │ └── style.css └── draw-ghost.js ├── graphs.html ├── index.html ├── lib ├── box2d.js └── seedrandom.js ├── package-lock.json ├── package.json ├── src ├── bare.js ├── car-schema │ ├── car-constants.json │ ├── construct.js │ ├── def-to-car.js │ ├── progress.js │ └── run.js ├── debug.js ├── draw │ ├── draw-car-stats.js │ ├── draw-car.js │ ├── draw-circle.js │ ├── draw-floor.js │ ├── draw-virtual-poly.js │ ├── plot-graphs.js │ └── scatter-plot.js ├── even-distribution.js ├── generation-config │ ├── generateRandom.js │ ├── inbreeding-coefficient.js │ ├── index.js │ ├── pickParent.js │ └── selectFromAllParents.js ├── ghost │ ├── car-to-ghost.js │ └── index.js ├── index.js ├── machine-learning │ ├── create-instance.js │ ├── genetic-algorithm │ │ └── manage-round.js │ ├── random.js │ └── simulated-annealing │ │ └── manage-round.js └── world │ ├── run.js │ └── setup-scene.js └── styles.css /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { }, 3 | "plugins": [], 4 | "extends": [ 5 | "eslint:recommended" 6 | ], 7 | "globals": { 8 | "Promise": true, 9 | "console": true, 10 | "require": true, 11 | "Set": true, 12 | "Map": true, 13 | "setTimeout": true, 14 | "clearTimeout": true, 15 | "setInterval": true, 16 | "clearInterval": true, 17 | "setImmediate": true, 18 | "__dirname": true, 19 | "global": true, 20 | "module": true 21 | }, 22 | "parserOptions": { 23 | "ecmaVersion": 5, 24 | "sourceType": "module", 25 | "impliedStrict": true 26 | }, 27 | "rules": { 28 | "no-use-before-define": [2, { "functions": false }], 29 | "no-mixed-spaces-and-tabs": 0, 30 | "no-console": 0 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | 3 | ################# 4 | ## Eclipse 5 | ################# 6 | 7 | *.pydevproject 8 | .project 9 | .metadata 10 | bin/ 11 | tmp/ 12 | *.tmp 13 | *.bak 14 | *.swp 15 | *~.nib 16 | local.properties 17 | .classpath 18 | .settings/ 19 | .loadpath 20 | main.js 21 | index-test.html 22 | 23 | # External tool builders 24 | .externalToolBuilders/ 25 | 26 | # Locally stored "Eclipse launch configurations" 27 | *.launch 28 | 29 | # CDT-specific 30 | .cproject 31 | 32 | # PDT-specific 33 | .buildpath 34 | 35 | 36 | ################# 37 | ## Visual Studio 38 | ################# 39 | 40 | ## Ignore Visual Studio temporary files, build results, and 41 | ## files generated by popular Visual Studio add-ons. 42 | 43 | # User-specific files 44 | *.suo 45 | *.user 46 | *.sln.docstates 47 | 48 | # Build results 49 | 50 | [Dd]ebug/ 51 | [Rr]elease/ 52 | x64/ 53 | [Bb]in/ 54 | [Oo]bj/ 55 | 56 | # MSTest test Results 57 | [Tt]est[Rr]esult*/ 58 | [Bb]uild[Ll]og.* 59 | 60 | *_i.c 61 | *_p.c 62 | *.ilk 63 | *.meta 64 | *.obj 65 | *.pch 66 | *.pdb 67 | *.pgc 68 | *.pgd 69 | *.rsp 70 | *.sbr 71 | *.tlb 72 | *.tli 73 | *.tlh 74 | *.tmp 75 | *.tmp_proj 76 | *.log 77 | *.vspscc 78 | *.vssscc 79 | .builds 80 | *.pidb 81 | *.log 82 | *.scc 83 | 84 | # Visual C++ cache files 85 | ipch/ 86 | *.aps 87 | *.ncb 88 | *.opensdf 89 | *.sdf 90 | *.cachefile 91 | 92 | # Visual Studio profiler 93 | *.psess 94 | *.vsp 95 | *.vspx 96 | 97 | # Guidance Automation Toolkit 98 | *.gpState 99 | 100 | # ReSharper is a .NET coding add-in 101 | _ReSharper*/ 102 | *.[Rr]e[Ss]harper 103 | 104 | # TeamCity is a build add-in 105 | _TeamCity* 106 | 107 | # DotCover is a Code Coverage Tool 108 | *.dotCover 109 | 110 | # NCrunch 111 | *.ncrunch* 112 | .*crunch*.local.xml 113 | 114 | # Installshield output folder 115 | [Ee]xpress/ 116 | 117 | # DocProject is a documentation generator add-in 118 | DocProject/buildhelp/ 119 | DocProject/Help/*.HxT 120 | DocProject/Help/*.HxC 121 | DocProject/Help/*.hhc 122 | DocProject/Help/*.hhk 123 | DocProject/Help/*.hhp 124 | DocProject/Help/Html2 125 | DocProject/Help/html 126 | 127 | # Click-Once directory 128 | publish/ 129 | 130 | # Publish Web Output 131 | *.Publish.xml 132 | *.pubxml 133 | 134 | # NuGet Packages Directory 135 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 136 | #packages/ 137 | 138 | # Windows Azure Build Output 139 | csx 140 | *.build.csdef 141 | 142 | # Windows Store app package directory 143 | AppPackages/ 144 | 145 | # Others 146 | sql/ 147 | *.Cache 148 | ClientBin/ 149 | [Ss]tyle[Cc]op.* 150 | ~$* 151 | *~ 152 | *.dbmdl 153 | *.[Pp]ublish.xml 154 | *.pfx 155 | *.publishsettings 156 | 157 | # RIA/Silverlight projects 158 | Generated_Code/ 159 | 160 | # Backup & report files from converting an old project file to a newer 161 | # Visual Studio version. Backup files are not needed, because we have git ;-) 162 | _UpgradeReport_Files/ 163 | Backup*/ 164 | UpgradeLog*.XML 165 | UpgradeLog*.htm 166 | 167 | # SQL Server files 168 | App_Data/*.mdf 169 | App_Data/*.ldf 170 | 171 | ############# 172 | ## Windows detritus 173 | ############# 174 | 175 | # Windows image file caches 176 | Thumbs.db 177 | ehthumbs.db 178 | 179 | # Folder config file 180 | Desktop.ini 181 | 182 | # Recycle Bin used on file shares 183 | $RECYCLE.BIN/ 184 | 185 | # Mac crap 186 | .DS_Store 187 | 188 | 189 | ############# 190 | ## Python 191 | ############# 192 | 193 | *.py[co] 194 | 195 | # Packages 196 | *.egg 197 | *.egg-info 198 | dist/ 199 | build/ 200 | eggs/ 201 | parts/ 202 | var/ 203 | sdist/ 204 | develop-eggs/ 205 | .installed.cfg 206 | 207 | # Installer logs 208 | pip-log.txt 209 | 210 | # Unit test / coverage reports 211 | .coverage 212 | .tox 213 | 214 | #Translations 215 | *.mo 216 | 217 | #Mr Developer 218 | .mr.developer.cfg 219 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Rafael Matsunaga 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgement in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HTML5 Genetic Cars 2 | ================== 3 | 4 | A genetic algorithm car evolver in HTML5 canvas. 5 | 6 | This is inspired by BoxCar2D, and uses the same physics engine, Box2D, but it's written from scratch. 7 | 8 | This is the code as published on http://rednuht.org/genetic_cars_2/ 9 | 10 | The current module-based format required npm and browserify. 11 | 12 | Build with: 13 | 14 | npm run-script build 15 | -------------------------------------------------------------------------------- /bundle-main.js: -------------------------------------------------------------------------------- 1 | (function(){function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 10 | ${renderWheels(frame.wheels)} 11 | ${renderChassis(frame.chassis)} 12 | 13 | ` 14 | 15 | function renderWheels(wheels){ 16 | return wheels.map(function(wheelList){ 17 | return wheelList.map(function(wheel){ 18 | var fillStyle = "#eee"; 19 | var strokeStyle = "#aaa"; 20 | var center = wheel.pos, radius = wheel.rad, angle = wheel.ang; 21 | return ` 22 | 30 | 38 | `; 39 | }).join("\n"); 40 | }).join("\n") 41 | } 42 | 43 | function renderChassis(chassis){ 44 | var strokeStyle = "#aaa"; 45 | var fillStyle = "#eee"; 46 | 47 | return chassis.map(function(polyList){ 48 | return ` 49 | 60 | `; 61 | }).join("\n"); 62 | } 63 | 64 | } 65 | 66 | module.exports = function(ctx, zoom, frame){ 67 | // wheel style 68 | ctx.fillStyle = "#eee"; 69 | ctx.strokeStyle = "#aaa"; 70 | ctx.lineWidth = 1 / zoom; 71 | 72 | for (var i = 0; i < frame.wheels.length; i++) { 73 | for (var w in frame.wheels[i]) { 74 | ghost_draw_circle(ctx, frame.wheels[i][w].pos, frame.wheels[i][w].rad, frame.wheels[i][w].ang); 75 | } 76 | } 77 | 78 | // chassis style 79 | ctx.strokeStyle = "#aaa"; 80 | ctx.fillStyle = "#eee"; 81 | ctx.lineWidth = 1 / zoom; 82 | ctx.beginPath(); 83 | for (var c in frame.chassis) 84 | ghost_draw_poly(ctx, frame.chassis[c].vtx, frame.chassis[c].num); 85 | ctx.fill(); 86 | ctx.stroke(); 87 | } 88 | 89 | 90 | function ghost_draw_poly(ctx, vtx, n_vtx) { 91 | ctx.moveTo(vtx[0].x, vtx[0].y); 92 | for (var i = 1; i < n_vtx; i++) { 93 | ctx.lineTo(vtx[i].x, vtx[i].y); 94 | } 95 | ctx.lineTo(vtx[0].x, vtx[0].y); 96 | } 97 | 98 | function ghost_draw_circle(ctx, center, radius, angle) { 99 | ctx.beginPath(); 100 | ctx.arc(center.x, center.y, radius, 0, 2 * Math.PI, true); 101 | 102 | ctx.moveTo(center.x, center.y); 103 | ctx.lineTo(center.x + radius * Math.cos(angle), center.y + radius * Math.sin(angle)); 104 | 105 | ctx.fill(); 106 | ctx.stroke(); 107 | } 108 | -------------------------------------------------------------------------------- /graphs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTML5 Genetic Algorithm 2D Car Thingy - Chrome recommended 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 |
29 |
30 |
31 | 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTML5 Genetic Algorithm 2D Car Thingy - Chrome recommended 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 |
17 |
18 | 19 |
20 | 21 |
250
22 |
187
23 |
125
24 |
62
25 |
0
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 | 41 |
42 | 43 |
44 |
45 | 46 |
47 | 48 |
49 | 50 |
51 | 52 |
53 |
54 | 55 | 56 | 58 | 59 |
60 |
61 | 62 |
63 | 64 | 65 | 66 | 67 | 68 |
69 | 70 |
71 |
72 | 73 | 74 |
75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 108 | 109 | 110 | 111 | 128 | 129 | 130 | 131 | 137 | 138 | 139 | 140 | 152 | 153 | 154 | 155 | 170 | 171 |
Generation
Cars alive
Distance
Height
Mutation rate: 92 | 107 |
Mutation size: 112 | 127 |
Floor: 132 | 136 |
Gravity: 141 | 151 |
Elite clones: 156 | 169 |
172 | 173 |
174 | 175 | 176 |
177 |
178 |
179 | 180 |
181 |
182 | 183 | 184 |
185 |

But what is it?

186 |

The program uses a simple genetic algorithm to evolve random two-wheeled shapes into cars over generations. Loosely 187 | based on BoxCar2D, but 188 | written from scratch, only using the same physics engine (box2d).
189 | seedrandom.js written by David Bau. (thanks!)

190 | 191 | 192 |

Controls

193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 215 | 216 | 217 | 218 | 221 | 222 | 223 | 224 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 237 | 238 |
Save PopulationSaves current population locally.
Restore Saved PopulationRestore a previously saved population.
SupriseToggles drawing, makes the simulation faster.
New PopulationKeeps the generated track and restarts the whole car population.
Create new world with seedThe same seed always creates the same track, so you can agree on a seed with your friends 213 | and compete. :) 214 |
Mutation rateThe chance that each gene in each individual will mutate to a random value when a new generation is 219 | born. 220 |
Mutation sizeThe range each gene can mutate into. Lower numbers mean the gene will have values closer to the 225 | original. 226 |
Elite clonesThe top n cars that will be copied over to the next generation.
View top replayPause the current simulation and show the top performing car. Click a second time to resume 235 | simulation. 236 |
239 | 240 |

Graph

241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 |
RedTop score in each generation
GreenAverage of the top 10 cars in each generation
BlueAverage of the entire generation
255 | 256 |

Genome

257 |

258 | The genome consists of: 259 |

260 |
    261 |
  • Shape (8 genes, 1 per vertex)
  • 262 |
  • Wheel size (2 genes, 1 per wheel)
  • 263 |
  • Wheel position (2 genes, 1 per wheel)
  • 264 |
  • Wheel density (2 genes, 1 per wheel) darker wheels mean denser wheels
  • 265 |
  • Chassis density (1 gene) darker body means denser chassis
  • 266 |
267 | 268 |

Blurb

269 |

This is not as deterministic as it should be, so your best car may not perform as well as it once did. The terrain 270 | gets more complex with distance.
271 | I'm not in the mood to deal with checking if all scripts have loaded before running, so refresh the page if things 272 | seem whack.

273 | 274 |

GitHub

275 |

The code is now on a GitHub repository. Feel free to 276 | contribute!

277 | 278 |

Originally written by this guy, now with contributions from patient people at GitHub.

279 |
280 | 281 |
282 | 283 |
284 |
285 |
286 |
287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | -------------------------------------------------------------------------------- /lib/seedrandom.js: -------------------------------------------------------------------------------- 1 | // seedrandom.js 2 | // Author: David Bau 12/25/2010 3 | // 4 | // Defines a method Math.seedrandom() that, when called, substitutes 5 | // an explicitly seeded RC4-based algorithm for Math.random(). Also 6 | // supports automatic seeding from local or network sources of entropy. 7 | // 8 | // Usage: 9 | // 10 | // 11 | // 12 | // Math.seedrandom('yipee'); Sets Math.random to a function that is 13 | // initialized using the given explicit seed. 14 | // 15 | // Math.seedrandom(); Sets Math.random to a function that is 16 | // seeded using the current time, dom state, 17 | // and other accumulated local entropy. 18 | // The generated seed string is returned. 19 | // 20 | // Math.seedrandom('yowza', true); 21 | // Seeds using the given explicit seed mixed 22 | // together with accumulated entropy. 23 | // 24 | // 25 | // Seeds using physical random bits downloaded 26 | // from random.org. 27 | // 28 | // Seeds using urandom bits from call.jsonlib.com, 30 | // which is faster than random.org. 31 | // 32 | // Examples: 33 | // 34 | // Math.seedrandom("hello"); // Use "hello" as the seed. 35 | // document.write(Math.random()); // Always 0.5463663768140734 36 | // document.write(Math.random()); // Always 0.43973793770592234 37 | // var rng1 = Math.random; // Remember the current prng. 38 | // 39 | // var autoseed = Math.seedrandom(); // New prng with an automatic seed. 40 | // document.write(Math.random()); // Pretty much unpredictable. 41 | // 42 | // Math.random = rng1; // Continue "hello" prng sequence. 43 | // document.write(Math.random()); // Always 0.554769432473455 44 | // 45 | // Math.seedrandom(autoseed); // Restart at the previous seed. 46 | // document.write(Math.random()); // Repeat the 'unpredictable' value. 47 | // 48 | // Notes: 49 | // 50 | // Each time seedrandom('arg') is called, entropy from the passed seed 51 | // is accumulated in a pool to help generate future seeds for the 52 | // zero-argument form of Math.seedrandom, so entropy can be injected over 53 | // time by calling seedrandom with explicit data repeatedly. 54 | // 55 | // On speed - This javascript implementation of Math.random() is about 56 | // 3-10x slower than the built-in Math.random() because it is not native 57 | // code, but this is typically fast enough anyway. Seeding is more expensive, 58 | // especially if you use auto-seeding. Some details (timings on Chrome 4): 59 | // 60 | // Our Math.random() - avg less than 0.002 milliseconds per call 61 | // seedrandom('explicit') - avg less than 0.5 milliseconds per call 62 | // seedrandom('explicit', true) - avg less than 2 milliseconds per call 63 | // seedrandom() - avg about 38 milliseconds per call 64 | // 65 | // LICENSE (BSD): 66 | // 67 | // Copyright 2010 David Bau, all rights reserved. 68 | // 69 | // Redistribution and use in source and binary forms, with or without 70 | // modification, are permitted provided that the following conditions are met: 71 | // 72 | // 1. Redistributions of source code must retain the above copyright 73 | // notice, this list of conditions and the following disclaimer. 74 | // 75 | // 2. Redistributions in binary form must reproduce the above copyright 76 | // notice, this list of conditions and the following disclaimer in the 77 | // documentation and/or other materials provided with the distribution. 78 | // 79 | // 3. Neither the name of this module nor the names of its contributors may 80 | // be used to endorse or promote products derived from this software 81 | // without specific prior written permission. 82 | // 83 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 84 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 85 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 86 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 87 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 88 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 89 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 90 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 91 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 92 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 93 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 94 | // 95 | /** 96 | * All code is in an anonymous closure to keep the global namespace clean. 97 | * 98 | * @param {number=} overflow 99 | * @param {number=} startdenom 100 | */ 101 | (function (pool, math, width, chunks, significance, overflow, startdenom) { 102 | 103 | 104 | // 105 | // seedrandom() 106 | // This is the seedrandom function described above. 107 | // 108 | math['seedrandom'] = function seedrandom(seed, use_entropy) { 109 | var key = []; 110 | var arc4; 111 | 112 | // Flatten the seed string or build one from local entropy if needed. 113 | seed = mixkey(flatten( 114 | use_entropy ? [seed, pool] : 115 | arguments.length ? seed : 116 | [new Date().getTime(), pool, window], 3), key); 117 | 118 | // Use the seed to initialize an ARC4 generator. 119 | arc4 = new ARC4(key); 120 | 121 | // Mix the randomness into accumulated entropy. 122 | mixkey(arc4.S, pool); 123 | 124 | // Override Math.random 125 | 126 | // This function returns a random double in [0, 1) that contains 127 | // randomness in every bit of the mantissa of the IEEE 754 value. 128 | 129 | math['random'] = function random() { // Closure to return a random double: 130 | var n = arc4.g(chunks); // Start with a numerator n < 2 ^ 48 131 | var d = startdenom; // and denominator d = 2 ^ 48. 132 | var x = 0; // and no 'extra last byte'. 133 | while (n < significance) { // Fill up all significant digits by 134 | n = (n + x) * width; // shifting numerator and 135 | d *= width; // denominator and generating a 136 | x = arc4.g(1); // new least-significant-byte. 137 | } 138 | while (n >= overflow) { // To avoid rounding up, before adding 139 | n /= 2; // last byte, shift everything 140 | d /= 2; // right using integer math until 141 | x >>>= 1; // we have exactly the desired bits. 142 | } 143 | return (n + x) / d; // Form the number within [0, 1). 144 | }; 145 | 146 | // Return the seed that was used 147 | return seed; 148 | }; 149 | 150 | // 151 | // ARC4 152 | // 153 | // An ARC4 implementation. The constructor takes a key in the form of 154 | // an array of at most (width) integers that should be 0 <= x < (width). 155 | // 156 | // The g(count) method returns a pseudorandom integer that concatenates 157 | // the next (count) outputs from ARC4. Its return value is a number x 158 | // that is in the range 0 <= x < (width ^ count). 159 | // 160 | /** @constructor */ 161 | function ARC4(key) { 162 | var t, u, me = this, keylen = key.length; 163 | var i = 0, j = me.i = me.j = me.m = 0; 164 | me.S = []; 165 | me.c = []; 166 | 167 | // The empty key [] is treated as [0]. 168 | if (!keylen) { key = [keylen++]; } 169 | 170 | // Set up S using the standard key scheduling algorithm. 171 | while (i < width) { me.S[i] = i++; } 172 | for (i = 0; i < width; i++) { 173 | t = me.S[i]; 174 | j = lowbits(j + t + key[i % keylen]); 175 | u = me.S[j]; 176 | me.S[i] = u; 177 | me.S[j] = t; 178 | } 179 | 180 | // The "g" method returns the next (count) outputs as one number. 181 | me.g = function getnext(count) { 182 | var s = me.S; 183 | var i = lowbits(me.i + 1); var t = s[i]; 184 | var j = lowbits(me.j + t); var u = s[j]; 185 | s[i] = u; 186 | s[j] = t; 187 | var r = s[lowbits(t + u)]; 188 | while (--count) { 189 | i = lowbits(i + 1); t = s[i]; 190 | j = lowbits(j + t); u = s[j]; 191 | s[i] = u; 192 | s[j] = t; 193 | r = r * width + s[lowbits(t + u)]; 194 | } 195 | me.i = i; 196 | me.j = j; 197 | return r; 198 | }; 199 | // For robust unpredictability discard an initial batch of values. 200 | // See http://www.rsa.com/rsalabs/node.asp?id=2009 201 | me.g(width); 202 | } 203 | 204 | // 205 | // flatten() 206 | // Converts an object tree to nested arrays of strings. 207 | // 208 | /** @param {Object=} result 209 | * @param {string=} prop */ 210 | function flatten(obj, depth, result, prop) { 211 | result = []; 212 | if (depth && typeof(obj) == 'object') { 213 | for (prop in obj) { 214 | if (prop.indexOf('S') < 5) { // Avoid FF3 bug (local/sessionStorage) 215 | try { result.push(flatten(obj[prop], depth - 1)); } catch (e) {} 216 | } 217 | } 218 | } 219 | return result.length ? result : '' + obj; 220 | } 221 | 222 | // 223 | // mixkey() 224 | // Mixes a string seed into a key that is an array of integers, and 225 | // returns a shortened string seed that is equivalent to the result key. 226 | // 227 | /** @param {number=} smear 228 | * @param {number=} j */ 229 | function mixkey(seed, key, smear, j) { 230 | seed += ''; // Ensure the seed is a string 231 | smear = 0; 232 | for (j = 0; j < seed.length; j++) { 233 | key[lowbits(j)] = 234 | lowbits((smear ^= key[lowbits(j)] * 19) + seed.charCodeAt(j)); 235 | } 236 | seed = ''; 237 | for (j in key) { seed += String.fromCharCode(key[j]); } 238 | return seed; 239 | } 240 | 241 | // 242 | // lowbits() 243 | // A quick "n mod width" for width a power of 2. 244 | // 245 | function lowbits(n) { return n & (width - 1); } 246 | 247 | // 248 | // The following constants are related to IEEE 754 limits. 249 | // 250 | startdenom = math.pow(width, chunks); 251 | significance = math.pow(2, significance); 252 | overflow = significance * 2; 253 | 254 | // 255 | // When seedrandom.js is loaded, we immediately mix a few bits 256 | // from the built-in RNG into the entropy pool. Because we do 257 | // not want to intefere with determinstic PRNG state later, 258 | // seedrandom will not call math.random on its own again after 259 | // initialization. 260 | // 261 | mixkey(math.random(), pool); 262 | 263 | // End anonymous scope, and pass initial values. 264 | })( 265 | [], // pool: entropy pool starts empty 266 | Math, // math: package containing random, pow, and seedrandom 267 | 256, // width: each RC4 output is 0 <= x < 256 268 | 6, // chunks: at least six RC4 outputs for each double 269 | 52 // significance: there are 52 significant digits in a double 270 | ); 271 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "genetic-cars-html5", 3 | "version": "1.0.0", 4 | "description": "HTML5 Genetic Cars ==================", 5 | "main": "cawro.js", 6 | "watch": { 7 | "build": "{src,test}/**/*.js" 8 | }, 9 | "scripts": { 10 | "watch": "npm-watch", 11 | "watch-wait": " npm run build && wait-run -p './src/**/*' npm run build-main", 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "build": "npm run build-main && npm run build-graph", 14 | "build-main": "browserify --debug ./src/index.js -o ./bundle.js", 15 | "build-graph": "browserify --debug ./src/bare.js -o ./bundle-bare.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/formula1/HTML5_Genetic_Cars.git" 20 | }, 21 | "author": "", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/formula1/HTML5_Genetic_Cars/issues" 25 | }, 26 | "homepage": "https://github.com/formula1/HTML5_Genetic_Cars#readme", 27 | "devDependencies": { 28 | "browserify": "^14.3.0", 29 | "npm-watch": "^0.3.0" 30 | }, 31 | "dependencies": { 32 | "mersenne-twister": "^1.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/bare.js: -------------------------------------------------------------------------------- 1 | /* globals document confirm btoa */ 2 | /* globals b2Vec2 */ 3 | // Global Vars 4 | 5 | var worldRun = require("./world/run.js"); 6 | 7 | var graph_fns = require("./draw/plot-graphs.js"); 8 | var plot_graphs = graph_fns.plotGraphs; 9 | 10 | 11 | // ======= WORLD STATE ====== 12 | 13 | var $graphList = document.querySelector("#graph-list"); 14 | var $graphTemplate = document.querySelector("#graph-template"); 15 | 16 | function stringToHTML(s){ 17 | var temp = document.createElement('div'); 18 | temp.innerHTML = s; 19 | return temp.children[0]; 20 | } 21 | 22 | var states, runners, results, graphState = {}; 23 | 24 | function updateUI(key, scores){ 25 | var $graph = $graphList.querySelector("#graph-" + key); 26 | var $newGraph = stringToHTML($graphTemplate.innerHTML); 27 | $newGraph.id = "graph-" + key; 28 | if($graph){ 29 | $graphList.replaceChild($graph, $newGraph); 30 | } else { 31 | $graphList.appendChild($newGraph); 32 | } 33 | console.log($newGraph); 34 | var scatterPlotElem = $newGraph.querySelector(".scatterplot"); 35 | scatterPlotElem.id = "graph-" + key + "-scatter"; 36 | graphState[key] = plot_graphs( 37 | $newGraph.querySelector(".graphcanvas"), 38 | $newGraph.querySelector(".topscores"), 39 | scatterPlotElem, 40 | graphState[key], 41 | scores, 42 | {} 43 | ); 44 | } 45 | 46 | var generationConfig = require("./generation-config"); 47 | 48 | var box2dfps = 60; 49 | var max_car_health = box2dfps * 10; 50 | 51 | var world_def = { 52 | gravity: new b2Vec2(0.0, -9.81), 53 | doSleep: true, 54 | floorseed: btoa(Math.seedrandom()), 55 | tileDimensions: new b2Vec2(1.5, 0.15), 56 | maxFloorTiles: 200, 57 | mutable_floor: false, 58 | box2dfps: box2dfps, 59 | motorSpeed: 20, 60 | max_car_health: max_car_health, 61 | schema: generationConfig.constants.schema 62 | } 63 | 64 | var manageRound = { 65 | genetic: require("./machine-learning/genetic-algorithm/manage-round.js"), 66 | annealing: require("./machine-learning/simulated-annealing/manage-round.js"), 67 | }; 68 | 69 | var createListeners = function(key){ 70 | return { 71 | preCarStep: function(){}, 72 | carStep: function(){}, 73 | carDeath: function(carInfo){ 74 | carInfo.score.i = states[key].counter; 75 | }, 76 | generationEnd: function(results){ 77 | handleRoundEnd(key, results); 78 | } 79 | } 80 | } 81 | 82 | function generationZero(){ 83 | var obj = Object.keys(manageRound).reduce(function(obj, key){ 84 | obj.states[key] = manageRound[key].generationZero(generationConfig()); 85 | obj.runners[key] = worldRun( 86 | world_def, obj.states[key].generation, createListeners(key) 87 | ); 88 | obj.results[key] = []; 89 | graphState[key] = {} 90 | return obj; 91 | }, {states: {}, runners: {}, results: {}}); 92 | states = obj.states; 93 | runners = obj.runners; 94 | results = obj.results; 95 | } 96 | 97 | function handleRoundEnd(key, scores){ 98 | var previousCounter = states[key].counter; 99 | states[key] = manageRound[key].nextGeneration( 100 | states[key], scores, generationConfig() 101 | ); 102 | runners[key] = worldRun( 103 | world_def, states[key].generation, createListeners(key) 104 | ); 105 | if(states[key].counter === previousCounter){ 106 | console.log(results); 107 | results[key] = results[key].concat(scores); 108 | } else { 109 | handleGenerationEnd(key); 110 | results[key] = []; 111 | } 112 | } 113 | 114 | function runRound(){ 115 | var toRun = new Map(); 116 | Object.keys(states).forEach(function(key){ toRun.set(key, states[key].counter) }); 117 | console.log(toRun); 118 | while(toRun.size){ 119 | console.log("running"); 120 | Array.from(toRun.keys()).forEach(function(key){ 121 | if(states[key].counter === toRun.get(key)){ 122 | runners[key].step(); 123 | } else { 124 | toRun.delete(key); 125 | } 126 | }); 127 | } 128 | } 129 | 130 | function handleGenerationEnd(key){ 131 | var scores = results[key]; 132 | scores.sort(function (a, b) { 133 | if (a.score.v > b.score.v) { 134 | return -1 135 | } else { 136 | return 1 137 | } 138 | }) 139 | updateUI(key, scores); 140 | results[key] = []; 141 | } 142 | 143 | function cw_resetPopulationUI() { 144 | $graphList.innerHTML = ""; 145 | } 146 | 147 | function cw_resetWorld() { 148 | cw_resetPopulationUI(); 149 | Math.seedrandom(); 150 | generationZero(); 151 | } 152 | 153 | document.querySelector("#new-population").addEventListener("click", function(){ 154 | cw_resetPopulationUI() 155 | generationZero(); 156 | }) 157 | 158 | 159 | document.querySelector("#confirm-reset").addEventListener("click", function(){ 160 | cw_confirmResetWorld() 161 | }) 162 | 163 | document.querySelector("#fast-forward").addEventListener("click", function(){ 164 | runRound(); 165 | }) 166 | 167 | function cw_confirmResetWorld() { 168 | if (confirm('Really reset world?')) { 169 | cw_resetWorld(); 170 | } else { 171 | return false; 172 | } 173 | } 174 | 175 | cw_resetWorld(); 176 | -------------------------------------------------------------------------------- /src/car-schema/car-constants.json: -------------------------------------------------------------------------------- 1 | { 2 | "wheelCount": 2, 3 | "wheelMinRadius": 0.2, 4 | "wheelRadiusRange": 0.5, 5 | "wheelMinDensity": 40, 6 | "wheelDensityRange": 100, 7 | "chassisDensityRange": 300, 8 | "chassisMinDensity": 30, 9 | "chassisMinAxis": 0.1, 10 | "chassisAxisRange": 1.1 11 | } 12 | -------------------------------------------------------------------------------- /src/car-schema/construct.js: -------------------------------------------------------------------------------- 1 | var carConstants = require("./car-constants.json"); 2 | 3 | module.exports = { 4 | worldDef: worldDef, 5 | carConstants: getCarConstants, 6 | generateSchema: generateSchema 7 | } 8 | 9 | function worldDef(){ 10 | var box2dfps = 60; 11 | return { 12 | gravity: { y: 0 }, 13 | doSleep: true, 14 | floorseed: "abc", 15 | maxFloorTiles: 200, 16 | mutable_floor: false, 17 | motorSpeed: 20, 18 | box2dfps: box2dfps, 19 | max_car_health: box2dfps * 10, 20 | tileDimensions: { 21 | width: 1.5, 22 | height: 0.15 23 | } 24 | }; 25 | } 26 | 27 | function getCarConstants(){ 28 | return carConstants; 29 | } 30 | 31 | function generateSchema(values){ 32 | return { 33 | wheel_radius: { 34 | type: "float", 35 | length: values.wheelCount, 36 | min: values.wheelMinRadius, 37 | range: values.wheelRadiusRange, 38 | factor: 1, 39 | }, 40 | wheel_density: { 41 | type: "float", 42 | length: values.wheelCount, 43 | min: values.wheelMinDensity, 44 | range: values.wheelDensityRange, 45 | factor: 1, 46 | }, 47 | chassis_density: { 48 | type: "float", 49 | length: 1, 50 | min: values.chassisDensityRange, 51 | range: values.chassisMinDensity, 52 | factor: 1, 53 | }, 54 | vertex_list: { 55 | type: "float", 56 | length: 12, 57 | min: values.chassisMinAxis, 58 | range: values.chassisAxisRange, 59 | factor: 1, 60 | }, 61 | wheel_vertex: { 62 | type: "shuffle", 63 | length: 8, 64 | limit: values.wheelCount, 65 | factor: 1, 66 | }, 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /src/car-schema/def-to-car.js: -------------------------------------------------------------------------------- 1 | /* 2 | globals b2RevoluteJointDef b2Vec2 b2BodyDef b2Body b2FixtureDef b2PolygonShape b2CircleShape 3 | */ 4 | 5 | var createInstance = require("../machine-learning/create-instance"); 6 | 7 | module.exports = defToCar; 8 | 9 | function defToCar(normal_def, world, constants){ 10 | var car_def = createInstance.applyTypes(constants.schema, normal_def) 11 | var instance = {}; 12 | instance.chassis = createChassis( 13 | world, car_def.vertex_list, car_def.chassis_density 14 | ); 15 | var i; 16 | 17 | var wheelCount = car_def.wheel_radius.length; 18 | 19 | instance.wheels = []; 20 | for (i = 0; i < wheelCount; i++) { 21 | instance.wheels[i] = createWheel( 22 | world, 23 | car_def.wheel_radius[i], 24 | car_def.wheel_density[i] 25 | ); 26 | } 27 | 28 | var carmass = instance.chassis.GetMass(); 29 | for (i = 0; i < wheelCount; i++) { 30 | carmass += instance.wheels[i].GetMass(); 31 | } 32 | 33 | var joint_def = new b2RevoluteJointDef(); 34 | 35 | for (i = 0; i < wheelCount; i++) { 36 | var torque = carmass * -constants.gravity.y / car_def.wheel_radius[i]; 37 | 38 | var randvertex = instance.chassis.vertex_list[car_def.wheel_vertex[i]]; 39 | joint_def.localAnchorA.Set(randvertex.x, randvertex.y); 40 | joint_def.localAnchorB.Set(0, 0); 41 | joint_def.maxMotorTorque = torque; 42 | joint_def.motorSpeed = -constants.motorSpeed; 43 | joint_def.enableMotor = true; 44 | joint_def.bodyA = instance.chassis; 45 | joint_def.bodyB = instance.wheels[i]; 46 | world.CreateJoint(joint_def); 47 | } 48 | 49 | return instance; 50 | } 51 | 52 | function createChassis(world, vertexs, density) { 53 | 54 | var vertex_list = new Array(); 55 | vertex_list.push(new b2Vec2(vertexs[0], 0)); 56 | vertex_list.push(new b2Vec2(vertexs[1], vertexs[2])); 57 | vertex_list.push(new b2Vec2(0, vertexs[3])); 58 | vertex_list.push(new b2Vec2(-vertexs[4], vertexs[5])); 59 | vertex_list.push(new b2Vec2(-vertexs[6], 0)); 60 | vertex_list.push(new b2Vec2(-vertexs[7], -vertexs[8])); 61 | vertex_list.push(new b2Vec2(0, -vertexs[9])); 62 | vertex_list.push(new b2Vec2(vertexs[10], -vertexs[11])); 63 | 64 | var body_def = new b2BodyDef(); 65 | body_def.type = b2Body.b2_dynamicBody; 66 | body_def.position.Set(0.0, 4.0); 67 | 68 | var body = world.CreateBody(body_def); 69 | 70 | createChassisPart(body, vertex_list[0], vertex_list[1], density); 71 | createChassisPart(body, vertex_list[1], vertex_list[2], density); 72 | createChassisPart(body, vertex_list[2], vertex_list[3], density); 73 | createChassisPart(body, vertex_list[3], vertex_list[4], density); 74 | createChassisPart(body, vertex_list[4], vertex_list[5], density); 75 | createChassisPart(body, vertex_list[5], vertex_list[6], density); 76 | createChassisPart(body, vertex_list[6], vertex_list[7], density); 77 | createChassisPart(body, vertex_list[7], vertex_list[0], density); 78 | 79 | body.vertex_list = vertex_list; 80 | 81 | return body; 82 | } 83 | 84 | 85 | function createChassisPart(body, vertex1, vertex2, density) { 86 | var vertex_list = new Array(); 87 | vertex_list.push(vertex1); 88 | vertex_list.push(vertex2); 89 | vertex_list.push(b2Vec2.Make(0, 0)); 90 | var fix_def = new b2FixtureDef(); 91 | fix_def.shape = new b2PolygonShape(); 92 | fix_def.density = density; 93 | fix_def.friction = 10; 94 | fix_def.restitution = 0.2; 95 | fix_def.filter.groupIndex = -1; 96 | fix_def.shape.SetAsArray(vertex_list, 3); 97 | 98 | body.CreateFixture(fix_def); 99 | } 100 | 101 | function createWheel(world, radius, density) { 102 | var body_def = new b2BodyDef(); 103 | body_def.type = b2Body.b2_dynamicBody; 104 | body_def.position.Set(0, 0); 105 | 106 | var body = world.CreateBody(body_def); 107 | 108 | var fix_def = new b2FixtureDef(); 109 | fix_def.shape = new b2CircleShape(radius); 110 | fix_def.density = density; 111 | fix_def.friction = 1; 112 | fix_def.restitution = 0.2; 113 | fix_def.filter.groupIndex = -1; 114 | 115 | body.CreateFixture(fix_def); 116 | return body; 117 | } 118 | -------------------------------------------------------------------------------- /src/car-schema/progress.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red42/HTML5_Genetic_Cars/f3e9ce5233025f832fd09dafbae901e4485605ac/src/car-schema/progress.js -------------------------------------------------------------------------------- /src/car-schema/run.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = { 4 | getInitialState: getInitialState, 5 | updateState: updateState, 6 | getStatus: getStatus, 7 | calculateScore: calculateScore, 8 | }; 9 | 10 | function getInitialState(world_def){ 11 | return { 12 | frames: 0, 13 | health: world_def.max_car_health, 14 | maxPositiony: 0, 15 | minPositiony: 0, 16 | maxPositionx: 0, 17 | }; 18 | } 19 | 20 | function updateState(constants, worldConstruct, state){ 21 | if(state.health <= 0){ 22 | throw new Error("Already Dead"); 23 | } 24 | if(state.maxPositionx > constants.finishLine){ 25 | throw new Error("already Finished"); 26 | } 27 | 28 | // console.log(state); 29 | // check health 30 | var position = worldConstruct.chassis.GetPosition(); 31 | // check if car reached end of the path 32 | var nextState = { 33 | frames: state.frames + 1, 34 | maxPositionx: position.x > state.maxPositionx ? position.x : state.maxPositionx, 35 | maxPositiony: position.y > state.maxPositiony ? position.y : state.maxPositiony, 36 | minPositiony: position.y < state.minPositiony ? position.y : state.minPositiony 37 | }; 38 | 39 | if (position.x > constants.finishLine) { 40 | return nextState; 41 | } 42 | 43 | if (position.x > state.maxPositionx + 0.02) { 44 | nextState.health = constants.max_car_health; 45 | return nextState; 46 | } 47 | nextState.health = state.health - 1; 48 | if (Math.abs(worldConstruct.chassis.GetLinearVelocity().x) < 0.001) { 49 | nextState.health -= 5; 50 | } 51 | return nextState; 52 | } 53 | 54 | function getStatus(state, constants){ 55 | if(hasFailed(state, constants)) return -1; 56 | if(hasSuccess(state, constants)) return 1; 57 | return 0; 58 | } 59 | 60 | function hasFailed(state /*, constants */){ 61 | return state.health <= 0; 62 | } 63 | function hasSuccess(state, constants){ 64 | return state.maxPositionx > constants.finishLine; 65 | } 66 | 67 | function calculateScore(state, constants){ 68 | var avgspeed = (state.maxPositionx / state.frames) * constants.box2dfps; 69 | var position = state.maxPositionx; 70 | var score = position + avgspeed; 71 | return { 72 | v: score, 73 | s: avgspeed, 74 | x: position, 75 | y: state.maxPositiony, 76 | y2: state.minPositiony 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/debug.js: -------------------------------------------------------------------------------- 1 | var debugbox = document.getElementById("debug"); 2 | 3 | module.exports = debug; 4 | 5 | function debug(str, clear) { 6 | if (clear) { 7 | debugbox.innerHTML = ""; 8 | } 9 | debugbox.innerHTML += str + "
"; 10 | } 11 | -------------------------------------------------------------------------------- /src/draw/draw-car-stats.js: -------------------------------------------------------------------------------- 1 | /* globals document */ 2 | 3 | var run = require("../car-schema/run"); 4 | 5 | /* ========================================================================= */ 6 | /* === Car ================================================================= */ 7 | var cw_Car = function () { 8 | this.__constructor.apply(this, arguments); 9 | } 10 | 11 | cw_Car.prototype.__constructor = function (car) { 12 | this.car = car; 13 | this.car_def = car.def; 14 | var car_def = this.car_def; 15 | 16 | this.frames = 0; 17 | this.alive = true; 18 | this.is_elite = car.def.is_elite; 19 | this.healthBar = document.getElementById("health" + car_def.index).style; 20 | this.healthBarText = document.getElementById("health" + car_def.index).nextSibling.nextSibling; 21 | this.healthBarText.innerHTML = car_def.index; 22 | this.minimapmarker = document.getElementById("bar" + car_def.index); 23 | 24 | if (this.is_elite) { 25 | this.healthBar.backgroundColor = "#3F72AF"; 26 | this.minimapmarker.style.borderLeft = "1px solid #3F72AF"; 27 | this.minimapmarker.innerHTML = car_def.index; 28 | } else { 29 | this.healthBar.backgroundColor = "#F7C873"; 30 | this.minimapmarker.style.borderLeft = "1px solid #F7C873"; 31 | this.minimapmarker.innerHTML = car_def.index; 32 | } 33 | 34 | } 35 | 36 | cw_Car.prototype.getPosition = function () { 37 | return this.car.car.chassis.GetPosition(); 38 | } 39 | 40 | cw_Car.prototype.kill = function (currentRunner, constants) { 41 | this.minimapmarker.style.borderLeft = "1px solid #3F72AF"; 42 | var finishLine = currentRunner.scene.finishLine 43 | var max_car_health = constants.max_car_health; 44 | var status = run.getStatus(this.car.state, { 45 | finishLine: finishLine, 46 | max_car_health: max_car_health, 47 | }) 48 | switch(status){ 49 | case 1: { 50 | this.healthBar.width = "0"; 51 | break 52 | } 53 | case -1: { 54 | this.healthBarText.innerHTML = "†"; 55 | this.healthBar.width = "0"; 56 | break 57 | } 58 | } 59 | this.alive = false; 60 | 61 | } 62 | 63 | module.exports = cw_Car; 64 | -------------------------------------------------------------------------------- /src/draw/draw-car.js: -------------------------------------------------------------------------------- 1 | 2 | var cw_drawVirtualPoly = require("./draw-virtual-poly"); 3 | var cw_drawCircle = require("./draw-circle"); 4 | 5 | module.exports = function(car_constants, myCar, camera, ctx){ 6 | var camera_x = camera.pos.x; 7 | var zoom = camera.zoom; 8 | 9 | var wheelMinDensity = car_constants.wheelMinDensity 10 | var wheelDensityRange = car_constants.wheelDensityRange 11 | 12 | if (!myCar.alive) { 13 | return; 14 | } 15 | var myCarPos = myCar.getPosition(); 16 | 17 | if (myCarPos.x < (camera_x - 5)) { 18 | // too far behind, don't draw 19 | return; 20 | } 21 | 22 | ctx.strokeStyle = "#444"; 23 | ctx.lineWidth = 1 / zoom; 24 | 25 | var wheels = myCar.car.car.wheels; 26 | 27 | for (var i = 0; i < wheels.length; i++) { 28 | var b = wheels[i]; 29 | for (var f = b.GetFixtureList(); f; f = f.m_next) { 30 | var s = f.GetShape(); 31 | var color = Math.round(255 - (255 * (f.m_density - wheelMinDensity)) / wheelDensityRange).toString(); 32 | var rgbcolor = "rgb(" + color + "," + color + "," + color + ")"; 33 | cw_drawCircle(ctx, b, s.m_p, s.m_radius, b.m_sweep.a, rgbcolor); 34 | } 35 | } 36 | 37 | if (myCar.is_elite) { 38 | ctx.strokeStyle = "#3F72AF"; 39 | ctx.fillStyle = "#DBE2EF"; 40 | } else { 41 | ctx.strokeStyle = "#F7C873"; 42 | ctx.fillStyle = "#FAEBCD"; 43 | } 44 | ctx.beginPath(); 45 | 46 | var chassis = myCar.car.car.chassis; 47 | 48 | for (f = chassis.GetFixtureList(); f; f = f.m_next) { 49 | var cs = f.GetShape(); 50 | cw_drawVirtualPoly(ctx, chassis, cs.m_vertices, cs.m_vertexCount); 51 | } 52 | ctx.fill(); 53 | ctx.stroke(); 54 | } 55 | -------------------------------------------------------------------------------- /src/draw/draw-circle.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = cw_drawCircle; 3 | 4 | function cw_drawCircle(ctx, body, center, radius, angle, color) { 5 | var p = body.GetWorldPoint(center); 6 | ctx.fillStyle = color; 7 | 8 | ctx.beginPath(); 9 | ctx.arc(p.x, p.y, radius, 0, 2 * Math.PI, true); 10 | 11 | ctx.moveTo(p.x, p.y); 12 | ctx.lineTo(p.x + radius * Math.cos(angle), p.y + radius * Math.sin(angle)); 13 | 14 | ctx.fill(); 15 | ctx.stroke(); 16 | } 17 | -------------------------------------------------------------------------------- /src/draw/draw-floor.js: -------------------------------------------------------------------------------- 1 | var cw_drawVirtualPoly = require("./draw-virtual-poly"); 2 | module.exports = function(ctx, camera, cw_floorTiles) { 3 | var camera_x = camera.pos.x; 4 | var zoom = camera.zoom; 5 | ctx.strokeStyle = "#000"; 6 | ctx.fillStyle = "#777"; 7 | ctx.lineWidth = 1 / zoom; 8 | ctx.beginPath(); 9 | 10 | var k; 11 | if(camera.pos.x - 10 > 0){ 12 | k = Math.floor((camera.pos.x - 10) / 1.5); 13 | } else { 14 | k = 0; 15 | } 16 | 17 | // console.log(k); 18 | 19 | outer_loop: 20 | for (k; k < cw_floorTiles.length; k++) { 21 | var b = cw_floorTiles[k]; 22 | for (var f = b.GetFixtureList(); f; f = f.m_next) { 23 | var s = f.GetShape(); 24 | var shapePosition = b.GetWorldPoint(s.m_vertices[0]).x; 25 | if ((shapePosition > (camera_x - 5)) && (shapePosition < (camera_x + 10))) { 26 | cw_drawVirtualPoly(ctx, b, s.m_vertices, s.m_vertexCount); 27 | } 28 | if (shapePosition > camera_x + 10) { 29 | break outer_loop; 30 | } 31 | } 32 | } 33 | ctx.fill(); 34 | ctx.stroke(); 35 | } 36 | -------------------------------------------------------------------------------- /src/draw/draw-virtual-poly.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = function(ctx, body, vtx, n_vtx) { 4 | // set strokestyle and fillstyle before call 5 | // call beginPath before call 6 | 7 | var p0 = body.GetWorldPoint(vtx[0]); 8 | ctx.moveTo(p0.x, p0.y); 9 | for (var i = 1; i < n_vtx; i++) { 10 | var p = body.GetWorldPoint(vtx[i]); 11 | ctx.lineTo(p.x, p.y); 12 | } 13 | ctx.lineTo(p0.x, p0.y); 14 | } 15 | -------------------------------------------------------------------------------- /src/draw/plot-graphs.js: -------------------------------------------------------------------------------- 1 | var scatterPlot = require("./scatter-plot"); 2 | 3 | module.exports = { 4 | plotGraphs: function(graphElem, topScoresElem, scatterPlotElem, lastState, scores, config) { 5 | lastState = lastState || {}; 6 | var generationSize = scores.length 7 | var graphcanvas = graphElem; 8 | var graphctx = graphcanvas.getContext("2d"); 9 | var graphwidth = 400; 10 | var graphheight = 250; 11 | var nextState = cw_storeGraphScores( 12 | lastState, scores, generationSize 13 | ); 14 | console.log(scores, nextState); 15 | cw_clearGraphics(graphcanvas, graphctx, graphwidth, graphheight); 16 | cw_plotAverage(nextState, graphctx); 17 | cw_plotElite(nextState, graphctx); 18 | cw_plotTop(nextState, graphctx); 19 | cw_listTopScores(topScoresElem, nextState); 20 | nextState.scatterGraph = drawAllResults( 21 | scatterPlotElem, config, nextState, lastState.scatterGraph 22 | ); 23 | return nextState; 24 | }, 25 | clearGraphics: function(graphElem) { 26 | var graphcanvas = graphElem; 27 | var graphctx = graphcanvas.getContext("2d"); 28 | var graphwidth = 400; 29 | var graphheight = 250; 30 | cw_clearGraphics(graphcanvas, graphctx, graphwidth, graphheight); 31 | } 32 | }; 33 | 34 | 35 | function cw_storeGraphScores(lastState, cw_carScores, generationSize) { 36 | console.log(cw_carScores); 37 | return { 38 | cw_topScores: (lastState.cw_topScores || []) 39 | .concat([cw_carScores[0].score]), 40 | cw_graphAverage: (lastState.cw_graphAverage || []).concat([ 41 | cw_average(cw_carScores, generationSize) 42 | ]), 43 | cw_graphElite: (lastState.cw_graphElite || []).concat([ 44 | cw_eliteaverage(cw_carScores, generationSize) 45 | ]), 46 | cw_graphTop: (lastState.cw_graphTop || []).concat([ 47 | cw_carScores[0].score.v 48 | ]), 49 | allResults: (lastState.allResults || []).concat(cw_carScores), 50 | } 51 | } 52 | 53 | function cw_plotTop(state, graphctx) { 54 | var cw_graphTop = state.cw_graphTop; 55 | var graphsize = cw_graphTop.length; 56 | graphctx.strokeStyle = "#C83B3B"; 57 | graphctx.beginPath(); 58 | graphctx.moveTo(0, 0); 59 | for (var k = 0; k < graphsize; k++) { 60 | graphctx.lineTo(400 * (k + 1) / graphsize, cw_graphTop[k]); 61 | } 62 | graphctx.stroke(); 63 | } 64 | 65 | function cw_plotElite(state, graphctx) { 66 | var cw_graphElite = state.cw_graphElite; 67 | var graphsize = cw_graphElite.length; 68 | graphctx.strokeStyle = "#7BC74D"; 69 | graphctx.beginPath(); 70 | graphctx.moveTo(0, 0); 71 | for (var k = 0; k < graphsize; k++) { 72 | graphctx.lineTo(400 * (k + 1) / graphsize, cw_graphElite[k]); 73 | } 74 | graphctx.stroke(); 75 | } 76 | 77 | function cw_plotAverage(state, graphctx) { 78 | var cw_graphAverage = state.cw_graphAverage; 79 | var graphsize = cw_graphAverage.length; 80 | graphctx.strokeStyle = "#3F72AF"; 81 | graphctx.beginPath(); 82 | graphctx.moveTo(0, 0); 83 | for (var k = 0; k < graphsize; k++) { 84 | graphctx.lineTo(400 * (k + 1) / graphsize, cw_graphAverage[k]); 85 | } 86 | graphctx.stroke(); 87 | } 88 | 89 | 90 | function cw_eliteaverage(scores, generationSize) { 91 | var sum = 0; 92 | for (var k = 0; k < Math.floor(generationSize / 2); k++) { 93 | sum += scores[k].score.v; 94 | } 95 | return sum / Math.floor(generationSize / 2); 96 | } 97 | 98 | function cw_average(scores, generationSize) { 99 | var sum = 0; 100 | for (var k = 0; k < generationSize; k++) { 101 | sum += scores[k].score.v; 102 | } 103 | return sum / generationSize; 104 | } 105 | 106 | function cw_clearGraphics(graphcanvas, graphctx, graphwidth, graphheight) { 107 | graphcanvas.width = graphcanvas.width; 108 | graphctx.translate(0, graphheight); 109 | graphctx.scale(1, -1); 110 | graphctx.lineWidth = 1; 111 | graphctx.strokeStyle = "#3F72AF"; 112 | graphctx.beginPath(); 113 | graphctx.moveTo(0, graphheight / 2); 114 | graphctx.lineTo(graphwidth, graphheight / 2); 115 | graphctx.moveTo(0, graphheight / 4); 116 | graphctx.lineTo(graphwidth, graphheight / 4); 117 | graphctx.moveTo(0, graphheight * 3 / 4); 118 | graphctx.lineTo(graphwidth, graphheight * 3 / 4); 119 | graphctx.stroke(); 120 | } 121 | 122 | function cw_listTopScores(elem, state) { 123 | var cw_topScores = state.cw_topScores; 124 | var ts = elem; 125 | ts.innerHTML = "Top Scores:
"; 126 | cw_topScores.sort(function (a, b) { 127 | if (a.v > b.v) { 128 | return -1 129 | } else { 130 | return 1 131 | } 132 | }); 133 | 134 | for (var k = 0; k < Math.min(10, cw_topScores.length); k++) { 135 | var topScore = cw_topScores[k]; 136 | // console.log(topScore); 137 | var n = "#" + (k + 1) + ":"; 138 | var score = Math.round(topScore.v * 100) / 100; 139 | var distance = "d:" + Math.round(topScore.x * 100) / 100; 140 | var yrange = "h:" + Math.round(topScore.y2 * 100) / 100 + "/" + Math.round(topScore.y * 100) / 100 + "m"; 141 | var gen = "(Gen " + cw_topScores[k].i + ")" 142 | 143 | ts.innerHTML += [n, score, distance, yrange, gen].join(" ") + "
"; 144 | } 145 | } 146 | 147 | function drawAllResults(scatterPlotElem, config, allResults, previousGraph){ 148 | if(!scatterPlotElem) return; 149 | return scatterPlot(scatterPlotElem, allResults, config.propertyMap, previousGraph) 150 | } 151 | -------------------------------------------------------------------------------- /src/draw/scatter-plot.js: -------------------------------------------------------------------------------- 1 | /* globals vis Highcharts */ 2 | 3 | // Called when the Visualization API is loaded. 4 | 5 | module.exports = highCharts; 6 | function highCharts(elem, scores){ 7 | var keys = Object.keys(scores[0].def); 8 | keys = keys.reduce(function(curArray, key){ 9 | var l = scores[0].def[key].length; 10 | var subArray = []; 11 | for(var i = 0; i < l; i++){ 12 | subArray.push(key + "." + i); 13 | } 14 | return curArray.concat(subArray); 15 | }, []); 16 | function retrieveValue(obj, path){ 17 | return path.split(".").reduce(function(curValue, key){ 18 | return curValue[key]; 19 | }, obj); 20 | } 21 | 22 | var dataObj = Object.keys(scores).reduce(function(kv, score){ 23 | keys.forEach(function(key){ 24 | kv[key].data.push([ 25 | retrieveValue(score.def, key), score.score.v 26 | ]) 27 | }) 28 | return kv; 29 | }, keys.reduce(function(kv, key){ 30 | kv[key] = { 31 | name: key, 32 | data: [], 33 | } 34 | return kv; 35 | }, {})) 36 | Highcharts.chart(elem.id, { 37 | chart: { 38 | type: 'scatter', 39 | zoomType: 'xy' 40 | }, 41 | title: { 42 | text: 'Property Value to Score' 43 | }, 44 | xAxis: { 45 | title: { 46 | enabled: true, 47 | text: 'Normalized' 48 | }, 49 | startOnTick: true, 50 | endOnTick: true, 51 | showLastLabel: true 52 | }, 53 | yAxis: { 54 | title: { 55 | text: 'Score' 56 | } 57 | }, 58 | legend: { 59 | layout: 'vertical', 60 | align: 'left', 61 | verticalAlign: 'top', 62 | x: 100, 63 | y: 70, 64 | floating: true, 65 | backgroundColor: (Highcharts.theme && Highcharts.theme.legendBackgroundColor) || '#FFFFFF', 66 | borderWidth: 1 67 | }, 68 | plotOptions: { 69 | scatter: { 70 | marker: { 71 | radius: 5, 72 | states: { 73 | hover: { 74 | enabled: true, 75 | lineColor: 'rgb(100,100,100)' 76 | } 77 | } 78 | }, 79 | states: { 80 | hover: { 81 | marker: { 82 | enabled: false 83 | } 84 | } 85 | }, 86 | tooltip: { 87 | headerFormat: '{series.name}
', 88 | pointFormat: '{point.x}, {point.y}' 89 | } 90 | } 91 | }, 92 | series: keys.map(function(key){ 93 | return dataObj[key]; 94 | }) 95 | }); 96 | } 97 | 98 | function visChart(elem, scores, propertyMap, graph) { 99 | 100 | // Create and populate a data table. 101 | var data = new vis.DataSet(); 102 | scores.forEach(function(scoreInfo){ 103 | data.add({ 104 | x: getProperty(scoreInfo, propertyMap.x), 105 | y: getProperty(scoreInfo, propertyMap.x), 106 | z: getProperty(scoreInfo, propertyMap.z), 107 | style: getProperty(scoreInfo, propertyMap.z), 108 | // extra: def.ancestry 109 | }); 110 | }); 111 | 112 | function getProperty(info, key){ 113 | if(key === "score"){ 114 | return info.score.v 115 | } else { 116 | return info.def[key]; 117 | } 118 | } 119 | 120 | // specify options 121 | var options = { 122 | width: '600px', 123 | height: '600px', 124 | style: 'dot-size', 125 | showPerspective: true, 126 | showLegend: true, 127 | showGrid: true, 128 | showShadow: false, 129 | 130 | // Option tooltip can be true, false, or a function returning a string with HTML contents 131 | tooltip: function (point) { 132 | // parameter point contains properties x, y, z, and data 133 | // data is the original object passed to the point constructor 134 | return 'score: ' + point.z + '
'; // + point.data.extra; 135 | }, 136 | 137 | // Tooltip default styling can be overridden 138 | tooltipStyle: { 139 | content: { 140 | background : 'rgba(255, 255, 255, 0.7)', 141 | padding : '10px', 142 | borderRadius : '10px' 143 | }, 144 | line: { 145 | borderLeft : '1px dotted rgba(0, 0, 0, 0.5)' 146 | }, 147 | dot: { 148 | border : '5px solid rgba(0, 0, 0, 0.5)' 149 | } 150 | }, 151 | 152 | keepAspectRatio: true, 153 | verticalRatio: 0.5 154 | }; 155 | 156 | var camera = graph ? graph.getCameraPosition() : null; 157 | 158 | // create our graph 159 | var container = elem; 160 | graph = new vis.Graph3d(container, data, options); 161 | 162 | if (camera) graph.setCameraPosition(camera); // restore camera position 163 | return graph; 164 | } 165 | -------------------------------------------------------------------------------- /src/even-distribution.js: -------------------------------------------------------------------------------- 1 | var random = require("./random.js"); 2 | 3 | module.exports = { 4 | createGenerationZero(schema, generator){ 5 | return Object.keys(schema).reduce(function(instance, key){ 6 | var schemaProp = schema[key]; 7 | var values; 8 | switch(schemaProp.type){ 9 | case "shuffle" : 10 | values = random.shuffleIntegers(schemaProp, generator); break; 11 | case "float" : 12 | values = random.createFloats(schemaProp, generator); break; 13 | case "integer": 14 | values = random.createIntegers(schemaProp, generator); break; 15 | default: 16 | throw new Error(`Unknown type ${schemaProp.type} of schema for key ${key}`); 17 | } 18 | instance[key] = values; 19 | return instance; 20 | }, {}); 21 | }, 22 | createCrossBreed(schema, parents, parentChooser){ 23 | return Object.keys(schema).reduce(function(crossDef, key){ 24 | var schemaDef = schema[key]; 25 | var values = []; 26 | for(var i = 0, l = schemaDef.length; i < l; i++){ 27 | var p = parentChooser(key, parents); 28 | values.push(parents[p][key][i]); 29 | } 30 | crossDef[key] = values; 31 | return crossDef; 32 | }, {}); 33 | }, 34 | createMutatedClone(schema, generator, parent, factor){ 35 | return Object.keys(schema).reduce(function(clone, key){ 36 | var schemaProp = schema[key]; 37 | var values; 38 | console.log(key, parent[key]); 39 | switch(schemaProp.type){ 40 | case "shuffle" : values = random.mutateShuffle( 41 | schemaProp, generator, parent[key], factor 42 | ); break; 43 | case "float" : values = random.mutateFloats( 44 | schemaProp, generator, parent[key], factor 45 | ); break; 46 | case "integer": values = random.mutateIntegers( 47 | schemaProp, generator, parent[key], factor 48 | ); break; 49 | default: 50 | throw new Error(`Unknown type ${schemaProp.type} of schema for key ${key}`); 51 | } 52 | clone[key] = values; 53 | return clone; 54 | }, {}); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/generation-config/generateRandom.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = generateRandom; 3 | function generateRandom(){ 4 | return Math.random(); 5 | } 6 | -------------------------------------------------------------------------------- /src/generation-config/inbreeding-coefficient.js: -------------------------------------------------------------------------------- 1 | // http://sunmingtao.blogspot.com/2016/11/inbreeding-coefficient.html 2 | module.exports = getInbreedingCoefficient; 3 | 4 | function getInbreedingCoefficient(child){ 5 | var nameIndex = new Map(); 6 | var flagged = new Set(); 7 | var convergencePoints = new Set(); 8 | createAncestryMap(child, []); 9 | 10 | var storedCoefficients = new Map(); 11 | 12 | return Array.from(convergencePoints.values()).reduce(function(sum, point){ 13 | var iCo = getCoefficient(point); 14 | return sum + iCo; 15 | }, 0); 16 | 17 | function createAncestryMap(initNode){ 18 | var itemsInQueue = [{ node: initNode, path: [] }]; 19 | do{ 20 | var item = itemsInQueue.shift(); 21 | var node = item.node; 22 | var path = item.path; 23 | if(processItem(node, path)){ 24 | var nextPath = [ node.id ].concat(path); 25 | itemsInQueue = itemsInQueue.concat(node.ancestry.map(function(parent){ 26 | return { 27 | node: parent, 28 | path: nextPath 29 | }; 30 | })); 31 | } 32 | }while(itemsInQueue.length); 33 | 34 | 35 | function processItem(node, path){ 36 | var newAncestor = !nameIndex.has(node.id); 37 | if(newAncestor){ 38 | nameIndex.set(node.id, { 39 | parents: (node.ancestry || []).map(function(parent){ 40 | return parent.id; 41 | }), 42 | id: node.id, 43 | children: [], 44 | convergences: [], 45 | }); 46 | } else { 47 | 48 | flagged.add(node.id) 49 | nameIndex.get(node.id).children.forEach(function(childIdentifier){ 50 | var offsets = findConvergence(childIdentifier.path, path); 51 | if(!offsets){ 52 | return; 53 | } 54 | var childID = path[offsets[1]]; 55 | convergencePoints.add(childID); 56 | nameIndex.get(childID).convergences.push({ 57 | parent: node.id, 58 | offsets: offsets, 59 | }); 60 | }); 61 | } 62 | 63 | if(path.length){ 64 | nameIndex.get(node.id).children.push({ 65 | child: path[0], 66 | path: path 67 | }); 68 | } 69 | 70 | if(!newAncestor){ 71 | return; 72 | } 73 | if(!node.ancestry){ 74 | return; 75 | } 76 | return true; 77 | } 78 | } 79 | 80 | function getCoefficient(id){ 81 | if(storedCoefficients.has(id)){ 82 | return storedCoefficients.get(id); 83 | } 84 | var node = nameIndex.get(id); 85 | var val = node.convergences.reduce(function(sum, point){ 86 | return sum + Math.pow(1 / 2, point.offsets.reduce(function(sum, value){ 87 | return sum + value; 88 | }, 1)) * (1 + getCoefficient(point.parent)); 89 | }, 0); 90 | storedCoefficients.set(id, val); 91 | 92 | return val; 93 | 94 | } 95 | function findConvergence(listA, listB){ 96 | var ci, cj, li, lj; 97 | outerloop: 98 | for(ci = 0, li = listA.length; ci < li; ci++){ 99 | for(cj = 0, lj = listB.length; cj < lj; cj++){ 100 | if(listA[ci] === listB[cj]){ 101 | break outerloop; 102 | } 103 | } 104 | } 105 | if(ci === li){ 106 | return false; 107 | } 108 | return [ci, cj]; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/generation-config/index.js: -------------------------------------------------------------------------------- 1 | var carConstruct = require("../car-schema/construct.js"); 2 | 3 | var carConstants = carConstruct.carConstants(); 4 | 5 | var schema = carConstruct.generateSchema(carConstants); 6 | var pickParent = require("./pickParent"); 7 | var selectFromAllParents = require("./selectFromAllParents"); 8 | const constants = { 9 | generationSize: 20, 10 | schema: schema, 11 | championLength: 1, 12 | mutation_range: 1, 13 | gen_mutation: 0.05, 14 | }; 15 | module.exports = function(){ 16 | var currentChoices = new Map(); 17 | return Object.assign( 18 | {}, 19 | constants, 20 | { 21 | selectFromAllParents: selectFromAllParents, 22 | generateRandom: require("./generateRandom"), 23 | pickParent: pickParent.bind(void 0, currentChoices), 24 | } 25 | ); 26 | } 27 | module.exports.constants = constants 28 | -------------------------------------------------------------------------------- /src/generation-config/pickParent.js: -------------------------------------------------------------------------------- 1 | var nAttributes = 15; 2 | module.exports = pickParent; 3 | 4 | function pickParent(currentChoices, chooseId, key /* , parents */){ 5 | if(!currentChoices.has(chooseId)){ 6 | currentChoices.set(chooseId, initializePick()) 7 | } 8 | // console.log(chooseId); 9 | var state = currentChoices.get(chooseId); 10 | // console.log(state.curparent); 11 | state.i++ 12 | if(["wheel_radius", "wheel_vertex", "wheel_density"].indexOf(key) > -1){ 13 | state.curparent = cw_chooseParent(state); 14 | return state.curparent; 15 | } 16 | state.curparent = cw_chooseParent(state); 17 | return state.curparent; 18 | 19 | function cw_chooseParent(state) { 20 | var curparent = state.curparent; 21 | var attributeIndex = state.i; 22 | var swapPoint1 = state.swapPoint1 23 | var swapPoint2 = state.swapPoint2 24 | // console.log(swapPoint1, swapPoint2, attributeIndex) 25 | if ((swapPoint1 == attributeIndex) || (swapPoint2 == attributeIndex)) { 26 | return curparent == 1 ? 0 : 1 27 | } 28 | return curparent 29 | } 30 | 31 | function initializePick(){ 32 | var curparent = 0; 33 | 34 | var swapPoint1 = Math.floor(Math.random() * (nAttributes)); 35 | var swapPoint2 = swapPoint1; 36 | while (swapPoint2 == swapPoint1) { 37 | swapPoint2 = Math.floor(Math.random() * (nAttributes)); 38 | } 39 | var i = 0; 40 | return { 41 | curparent: curparent, 42 | i: i, 43 | swapPoint1: swapPoint1, 44 | swapPoint2: swapPoint2 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/generation-config/selectFromAllParents.js: -------------------------------------------------------------------------------- 1 | var getInbreedingCoefficient = require("./inbreeding-coefficient"); 2 | 3 | module.exports = simpleSelect; 4 | 5 | function simpleSelect(parents){ 6 | var totalParents = parents.length 7 | var r = Math.random(); 8 | if (r == 0) 9 | return 0; 10 | return Math.floor(-Math.log(r) * totalParents) % totalParents; 11 | } 12 | 13 | function selectFromAllParents(parents, parentList, previousParentIndex) { 14 | var previousParent = parents[previousParentIndex]; 15 | var validParents = parents.filter(function(parent, i){ 16 | if(previousParentIndex === i){ 17 | return false; 18 | } 19 | if(!previousParent){ 20 | return true; 21 | } 22 | var child = { 23 | id: Math.random().toString(32), 24 | ancestry: [previousParent, parent].map(function(p){ 25 | return { 26 | id: p.def.id, 27 | ancestry: p.def.ancestry 28 | } 29 | }) 30 | } 31 | var iCo = getInbreedingCoefficient(child); 32 | console.log("inbreeding coefficient", iCo) 33 | if(iCo > 0.25){ 34 | return false; 35 | } 36 | return true; 37 | }) 38 | if(validParents.length === 0){ 39 | return Math.floor(Math.random() * parents.length) 40 | } 41 | var totalScore = validParents.reduce(function(sum, parent){ 42 | return sum + parent.score.v; 43 | }, 0); 44 | var r = totalScore * Math.random(); 45 | for(var i = 0; i < validParents.length; i++){ 46 | var score = validParents[i].score.v; 47 | if(r > score){ 48 | r = r - score; 49 | } else { 50 | break; 51 | } 52 | } 53 | return i; 54 | } 55 | -------------------------------------------------------------------------------- /src/ghost/car-to-ghost.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(car) { 3 | var out = { 4 | chassis: ghost_get_chassis(car.chassis), 5 | wheels: [], 6 | pos: {x: car.chassis.GetPosition().x, y: car.chassis.GetPosition().y} 7 | }; 8 | 9 | for (var i = 0; i < car.wheels.length; i++) { 10 | out.wheels[i] = ghost_get_wheel(car.wheels[i]); 11 | } 12 | 13 | return out; 14 | } 15 | 16 | function ghost_get_chassis(c) { 17 | var gc = []; 18 | 19 | for (var f = c.GetFixtureList(); f; f = f.m_next) { 20 | var s = f.GetShape(); 21 | 22 | var p = { 23 | vtx: [], 24 | num: 0 25 | } 26 | 27 | p.num = s.m_vertexCount; 28 | 29 | for (var i = 0; i < s.m_vertexCount; i++) { 30 | p.vtx.push(c.GetWorldPoint(s.m_vertices[i])); 31 | } 32 | 33 | gc.push(p); 34 | } 35 | 36 | return gc; 37 | } 38 | 39 | function ghost_get_wheel(w) { 40 | var gw = []; 41 | 42 | for (var f = w.GetFixtureList(); f; f = f.m_next) { 43 | var s = f.GetShape(); 44 | 45 | var c = { 46 | pos: w.GetWorldPoint(s.m_p), 47 | rad: s.m_radius, 48 | ang: w.m_sweep.a 49 | } 50 | 51 | gw.push(c); 52 | } 53 | 54 | return gw; 55 | } 56 | -------------------------------------------------------------------------------- /src/ghost/index.js: -------------------------------------------------------------------------------- 1 | 2 | var ghost_get_frame = require("./car-to-ghost.js"); 3 | 4 | var enable_ghost = true; 5 | 6 | module.exports = { 7 | ghost_create_replay: ghost_create_replay, 8 | ghost_create_ghost: ghost_create_ghost, 9 | ghost_pause: ghost_pause, 10 | ghost_resume: ghost_resume, 11 | ghost_get_position: ghost_get_position, 12 | ghost_compare_to_replay: ghost_compare_to_replay, 13 | ghost_move_frame: ghost_move_frame, 14 | ghost_add_replay_frame: ghost_add_replay_frame, 15 | ghost_draw_frame: ghost_draw_frame, 16 | ghost_reset_ghost: ghost_reset_ghost 17 | } 18 | 19 | function ghost_create_replay() { 20 | if (!enable_ghost) 21 | return null; 22 | 23 | return { 24 | num_frames: 0, 25 | frames: [], 26 | } 27 | } 28 | 29 | function ghost_create_ghost() { 30 | if (!enable_ghost) 31 | return null; 32 | 33 | return { 34 | replay: null, 35 | frame: 0, 36 | dist: -100 37 | } 38 | } 39 | 40 | function ghost_reset_ghost(ghost) { 41 | if (!enable_ghost) 42 | return; 43 | if (ghost == null) 44 | return; 45 | ghost.frame = 0; 46 | } 47 | 48 | function ghost_pause(ghost) { 49 | if (ghost != null) 50 | ghost.old_frame = ghost.frame; 51 | ghost_reset_ghost(ghost); 52 | } 53 | 54 | function ghost_resume(ghost) { 55 | if (ghost != null) 56 | ghost.frame = ghost.old_frame; 57 | } 58 | 59 | function ghost_get_position(ghost) { 60 | if (!enable_ghost) 61 | return; 62 | if (ghost == null) 63 | return; 64 | if (ghost.frame < 0) 65 | return; 66 | if (ghost.replay == null) 67 | return; 68 | var frame = ghost.replay.frames[ghost.frame]; 69 | return frame.pos; 70 | } 71 | 72 | function ghost_compare_to_replay(replay, ghost, max) { 73 | if (!enable_ghost) 74 | return; 75 | if (ghost == null) 76 | return; 77 | if (replay == null) 78 | return; 79 | 80 | if (ghost.dist < max) { 81 | ghost.replay = replay; 82 | ghost.dist = max; 83 | ghost.frame = 0; 84 | } 85 | } 86 | 87 | function ghost_move_frame(ghost) { 88 | if (!enable_ghost) 89 | return; 90 | if (ghost == null) 91 | return; 92 | if (ghost.replay == null) 93 | return; 94 | ghost.frame++; 95 | if (ghost.frame >= ghost.replay.num_frames) 96 | ghost.frame = ghost.replay.num_frames - 1; 97 | } 98 | 99 | function ghost_add_replay_frame(replay, car) { 100 | if (!enable_ghost) 101 | return; 102 | if (replay == null) 103 | return; 104 | 105 | var frame = ghost_get_frame(car); 106 | replay.frames.push(frame); 107 | replay.num_frames++; 108 | } 109 | 110 | function ghost_draw_frame(ctx, ghost, camera) { 111 | var zoom = camera.zoom; 112 | if (!enable_ghost) 113 | return; 114 | if (ghost == null) 115 | return; 116 | if (ghost.frame < 0) 117 | return; 118 | if (ghost.replay == null) 119 | return; 120 | 121 | var frame = ghost.replay.frames[ghost.frame]; 122 | 123 | // wheel style 124 | ctx.fillStyle = "#eee"; 125 | ctx.strokeStyle = "#aaa"; 126 | ctx.lineWidth = 1 / zoom; 127 | 128 | for (var i = 0; i < frame.wheels.length; i++) { 129 | for (var w in frame.wheels[i]) { 130 | ghost_draw_circle(ctx, frame.wheels[i][w].pos, frame.wheels[i][w].rad, frame.wheels[i][w].ang); 131 | } 132 | } 133 | 134 | // chassis style 135 | ctx.strokeStyle = "#aaa"; 136 | ctx.fillStyle = "#eee"; 137 | ctx.lineWidth = 1 / zoom; 138 | ctx.beginPath(); 139 | for (var c in frame.chassis) 140 | ghost_draw_poly(ctx, frame.chassis[c].vtx, frame.chassis[c].num); 141 | ctx.fill(); 142 | ctx.stroke(); 143 | } 144 | 145 | function ghost_draw_poly(ctx, vtx, n_vtx) { 146 | ctx.moveTo(vtx[0].x, vtx[0].y); 147 | for (var i = 1; i < n_vtx; i++) { 148 | ctx.lineTo(vtx[i].x, vtx[i].y); 149 | } 150 | ctx.lineTo(vtx[0].x, vtx[0].y); 151 | } 152 | 153 | function ghost_draw_circle(ctx, center, radius, angle) { 154 | ctx.beginPath(); 155 | ctx.arc(center.x, center.y, radius, 0, 2 * Math.PI, true); 156 | 157 | ctx.moveTo(center.x, center.y); 158 | ctx.lineTo(center.x + radius * Math.cos(angle), center.y + radius * Math.sin(angle)); 159 | 160 | ctx.fill(); 161 | ctx.stroke(); 162 | } 163 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* globals document performance localStorage alert confirm btoa HTMLDivElement */ 2 | /* globals b2Vec2 */ 3 | // Global Vars 4 | 5 | var worldRun = require("./world/run.js"); 6 | var carConstruct = require("./car-schema/construct.js"); 7 | 8 | var manageRound = require("./machine-learning/genetic-algorithm/manage-round.js"); 9 | 10 | var ghost_fns = require("./ghost/index.js"); 11 | 12 | var drawCar = require("./draw/draw-car.js"); 13 | var graph_fns = require("./draw/plot-graphs.js"); 14 | var plot_graphs = graph_fns.plotGraphs; 15 | var cw_clearGraphics = graph_fns.clearGraphics; 16 | var cw_drawFloor = require("./draw/draw-floor.js"); 17 | 18 | var ghost_draw_frame = ghost_fns.ghost_draw_frame; 19 | var ghost_create_ghost = ghost_fns.ghost_create_ghost; 20 | var ghost_add_replay_frame = ghost_fns.ghost_add_replay_frame; 21 | var ghost_compare_to_replay = ghost_fns.ghost_compare_to_replay; 22 | var ghost_get_position = ghost_fns.ghost_get_position; 23 | var ghost_move_frame = ghost_fns.ghost_move_frame; 24 | var ghost_reset_ghost = ghost_fns.ghost_reset_ghost 25 | var ghost_pause = ghost_fns.ghost_pause; 26 | var ghost_resume = ghost_fns.ghost_resume; 27 | var ghost_create_replay = ghost_fns.ghost_create_replay; 28 | 29 | var cw_Car = require("./draw/draw-car-stats.js"); 30 | var ghost; 31 | var carMap = new Map(); 32 | 33 | var doDraw = true; 34 | var cw_paused = false; 35 | 36 | var box2dfps = 60; 37 | var screenfps = 60; 38 | var skipTicks = Math.round(1000 / box2dfps); 39 | var maxFrameSkip = skipTicks * 2; 40 | 41 | var canvas = document.getElementById("mainbox"); 42 | var ctx = canvas.getContext("2d"); 43 | 44 | var camera = { 45 | speed: 0.05, 46 | pos: { 47 | x: 0, y: 0 48 | }, 49 | target: -1, 50 | zoom: 70 51 | } 52 | 53 | var minimapcamera = document.getElementById("minimapcamera").style; 54 | var minimapholder = document.querySelector("#minimapholder"); 55 | 56 | var minimapcanvas = document.getElementById("minimap"); 57 | var minimapctx = minimapcanvas.getContext("2d"); 58 | var minimapscale = 3; 59 | var minimapfogdistance = 0; 60 | var fogdistance = document.getElementById("minimapfog").style; 61 | 62 | 63 | var carConstants = carConstruct.carConstants(); 64 | 65 | 66 | var max_car_health = box2dfps * 10; 67 | 68 | var cw_ghostReplayInterval = null; 69 | 70 | var distanceMeter = document.getElementById("distancemeter"); 71 | var heightMeter = document.getElementById("heightmeter"); 72 | 73 | var leaderPosition = { 74 | x: 0, y: 0 75 | } 76 | 77 | minimapcamera.width = 12 * minimapscale + "px"; 78 | minimapcamera.height = 6 * minimapscale + "px"; 79 | 80 | 81 | // ======= WORLD STATE ====== 82 | var generationConfig = require("./generation-config"); 83 | 84 | 85 | var world_def = { 86 | gravity: new b2Vec2(0.0, -9.81), 87 | doSleep: true, 88 | floorseed: btoa(Math.seedrandom()), 89 | tileDimensions: new b2Vec2(1.5, 0.15), 90 | maxFloorTiles: 200, 91 | mutable_floor: false, 92 | box2dfps: box2dfps, 93 | motorSpeed: 20, 94 | max_car_health: max_car_health, 95 | schema: generationConfig.constants.schema 96 | } 97 | 98 | var cw_deadCars; 99 | var graphState = { 100 | cw_topScores: [], 101 | cw_graphAverage: [], 102 | cw_graphElite: [], 103 | cw_graphTop: [], 104 | }; 105 | 106 | function resetGraphState(){ 107 | graphState = { 108 | cw_topScores: [], 109 | cw_graphAverage: [], 110 | cw_graphElite: [], 111 | cw_graphTop: [], 112 | }; 113 | } 114 | 115 | 116 | 117 | // ========================== 118 | 119 | var generationState; 120 | 121 | // ======== Activity State ==== 122 | var currentRunner; 123 | var loops = 0; 124 | var nextGameTick = (new Date).getTime(); 125 | 126 | function showDistance(distance, height) { 127 | distanceMeter.innerHTML = distance + " meters
"; 128 | heightMeter.innerHTML = height + " meters"; 129 | if (distance > minimapfogdistance) { 130 | fogdistance.width = 800 - Math.round(distance + 15) * minimapscale + "px"; 131 | minimapfogdistance = distance; 132 | } 133 | } 134 | 135 | 136 | 137 | /* === END Car ============================================================= */ 138 | /* ========================================================================= */ 139 | 140 | 141 | /* ========================================================================= */ 142 | /* ==== Generation ========================================================= */ 143 | 144 | function cw_generationZero() { 145 | 146 | generationState = manageRound.generationZero(generationConfig()); 147 | } 148 | 149 | function resetCarUI(){ 150 | cw_deadCars = 0; 151 | leaderPosition = { 152 | x: 0, y: 0 153 | }; 154 | document.getElementById("generation").innerHTML = generationState.counter.toString(); 155 | document.getElementById("cars").innerHTML = ""; 156 | document.getElementById("population").innerHTML = generationConfig.constants.generationSize.toString(); 157 | } 158 | 159 | /* ==== END Genration ====================================================== */ 160 | /* ========================================================================= */ 161 | 162 | /* ========================================================================= */ 163 | /* ==== Drawing ============================================================ */ 164 | 165 | function cw_drawScreen() { 166 | var floorTiles = currentRunner.scene.floorTiles; 167 | ctx.clearRect(0, 0, canvas.width, canvas.height); 168 | ctx.save(); 169 | cw_setCameraPosition(); 170 | var camera_x = camera.pos.x; 171 | var camera_y = camera.pos.y; 172 | var zoom = camera.zoom; 173 | ctx.translate(200 - (camera_x * zoom), 200 + (camera_y * zoom)); 174 | ctx.scale(zoom, -zoom); 175 | cw_drawFloor(ctx, camera, floorTiles); 176 | ghost_draw_frame(ctx, ghost, camera); 177 | cw_drawCars(); 178 | ctx.restore(); 179 | } 180 | 181 | function cw_minimapCamera(/* x, y*/) { 182 | var camera_x = camera.pos.x 183 | var camera_y = camera.pos.y 184 | minimapcamera.left = Math.round((2 + camera_x) * minimapscale) + "px"; 185 | minimapcamera.top = Math.round((31 - camera_y) * minimapscale) + "px"; 186 | } 187 | 188 | function cw_setCameraTarget(k) { 189 | camera.target = k; 190 | } 191 | 192 | function cw_setCameraPosition() { 193 | var cameraTargetPosition 194 | if (camera.target !== -1) { 195 | cameraTargetPosition = carMap.get(camera.target).getPosition(); 196 | } else { 197 | cameraTargetPosition = leaderPosition; 198 | } 199 | var diff_y = camera.pos.y - cameraTargetPosition.y; 200 | var diff_x = camera.pos.x - cameraTargetPosition.x; 201 | camera.pos.y -= camera.speed * diff_y; 202 | camera.pos.x -= camera.speed * diff_x; 203 | cw_minimapCamera(camera.pos.x, camera.pos.y); 204 | } 205 | 206 | function cw_drawGhostReplay() { 207 | var floorTiles = currentRunner.scene.floorTiles; 208 | var carPosition = ghost_get_position(ghost); 209 | camera.pos.x = carPosition.x; 210 | camera.pos.y = carPosition.y; 211 | cw_minimapCamera(camera.pos.x, camera.pos.y); 212 | showDistance( 213 | Math.round(carPosition.x * 100) / 100, 214 | Math.round(carPosition.y * 100) / 100 215 | ); 216 | ctx.clearRect(0, 0, canvas.width, canvas.height); 217 | ctx.save(); 218 | ctx.translate( 219 | 200 - (carPosition.x * camera.zoom), 220 | 200 + (carPosition.y * camera.zoom) 221 | ); 222 | ctx.scale(camera.zoom, -camera.zoom); 223 | ghost_draw_frame(ctx, ghost); 224 | ghost_move_frame(ghost); 225 | cw_drawFloor(ctx, camera, floorTiles); 226 | ctx.restore(); 227 | } 228 | 229 | 230 | function cw_drawCars() { 231 | var cw_carArray = Array.from(carMap.values()); 232 | for (var k = (cw_carArray.length - 1); k >= 0; k--) { 233 | var myCar = cw_carArray[k]; 234 | drawCar(carConstants, myCar, camera, ctx) 235 | } 236 | } 237 | 238 | function toggleDisplay() { 239 | canvas.width = canvas.width; 240 | if (doDraw) { 241 | doDraw = false; 242 | cw_stopSimulation(); 243 | cw_runningInterval = setInterval(function () { 244 | var time = performance.now() + (1000 / screenfps); 245 | while (time > performance.now()) { 246 | simulationStep(); 247 | } 248 | }, 1); 249 | } else { 250 | doDraw = true; 251 | clearInterval(cw_runningInterval); 252 | cw_startSimulation(); 253 | } 254 | } 255 | 256 | function cw_drawMiniMap() { 257 | var floorTiles = currentRunner.scene.floorTiles; 258 | var last_tile = null; 259 | var tile_position = new b2Vec2(-5, 0); 260 | minimapfogdistance = 0; 261 | fogdistance.width = "800px"; 262 | minimapcanvas.width = minimapcanvas.width; 263 | minimapctx.strokeStyle = "#3F72AF"; 264 | minimapctx.beginPath(); 265 | minimapctx.moveTo(0, 35 * minimapscale); 266 | for (var k = 0; k < floorTiles.length; k++) { 267 | last_tile = floorTiles[k]; 268 | var last_fixture = last_tile.GetFixtureList(); 269 | var last_world_coords = last_tile.GetWorldPoint(last_fixture.GetShape().m_vertices[3]); 270 | tile_position = last_world_coords; 271 | minimapctx.lineTo((tile_position.x + 5) * minimapscale, (-tile_position.y + 35) * minimapscale); 272 | } 273 | minimapctx.stroke(); 274 | } 275 | 276 | /* ==== END Drawing ======================================================== */ 277 | /* ========================================================================= */ 278 | var uiListeners = { 279 | preCarStep: function(){ 280 | ghost_move_frame(ghost); 281 | }, 282 | carStep(car){ 283 | updateCarUI(car); 284 | }, 285 | carDeath(carInfo){ 286 | 287 | var k = carInfo.index; 288 | 289 | var car = carInfo.car, score = carInfo.score; 290 | carMap.get(carInfo).kill(currentRunner, world_def); 291 | 292 | // refocus camera to leader on death 293 | if (camera.target == carInfo) { 294 | cw_setCameraTarget(-1); 295 | } 296 | // console.log(score); 297 | carMap.delete(carInfo); 298 | ghost_compare_to_replay(car.replay, ghost, score.v); 299 | score.i = generationState.counter; 300 | 301 | cw_deadCars++; 302 | var generationSize = generationConfig.constants.generationSize; 303 | document.getElementById("population").innerHTML = (generationSize - cw_deadCars).toString(); 304 | 305 | // console.log(leaderPosition.leader, k) 306 | if (leaderPosition.leader == k) { 307 | // leader is dead, find new leader 308 | cw_findLeader(); 309 | } 310 | }, 311 | generationEnd(results){ 312 | cleanupRound(results); 313 | return cw_newRound(results); 314 | } 315 | } 316 | 317 | function simulationStep() { 318 | currentRunner.step(); 319 | showDistance( 320 | Math.round(leaderPosition.x * 100) / 100, 321 | Math.round(leaderPosition.y * 100) / 100 322 | ); 323 | } 324 | 325 | function gameLoop() { 326 | loops = 0; 327 | while (!cw_paused && (new Date).getTime() > nextGameTick && loops < maxFrameSkip) { 328 | nextGameTick += skipTicks; 329 | loops++; 330 | } 331 | simulationStep(); 332 | cw_drawScreen(); 333 | 334 | if(!cw_paused) window.requestAnimationFrame(gameLoop); 335 | } 336 | 337 | function updateCarUI(carInfo){ 338 | var k = carInfo.index; 339 | var car = carMap.get(carInfo); 340 | var position = car.getPosition(); 341 | 342 | ghost_add_replay_frame(car.replay, car.car.car); 343 | car.minimapmarker.style.left = Math.round((position.x + 5) * minimapscale) + "px"; 344 | car.healthBar.width = Math.round((car.car.state.health / max_car_health) * 100) + "%"; 345 | if (position.x > leaderPosition.x) { 346 | leaderPosition = position; 347 | leaderPosition.leader = k; 348 | // console.log("new leader: ", k); 349 | } 350 | } 351 | 352 | function cw_findLeader() { 353 | var lead = 0; 354 | var cw_carArray = Array.from(carMap.values()); 355 | for (var k = 0; k < cw_carArray.length; k++) { 356 | if (!cw_carArray[k].alive) { 357 | continue; 358 | } 359 | var position = cw_carArray[k].getPosition(); 360 | if (position.x > lead) { 361 | leaderPosition = position; 362 | leaderPosition.leader = k; 363 | } 364 | } 365 | } 366 | 367 | function fastForward(){ 368 | var gen = generationState.counter; 369 | while(gen === generationState.counter){ 370 | currentRunner.step(); 371 | } 372 | } 373 | 374 | function cleanupRound(results){ 375 | 376 | results.sort(function (a, b) { 377 | if (a.score.v > b.score.v) { 378 | return -1 379 | } else { 380 | return 1 381 | } 382 | }) 383 | graphState = plot_graphs( 384 | document.getElementById("graphcanvas"), 385 | document.getElementById("topscores"), 386 | null, 387 | graphState, 388 | results 389 | ); 390 | } 391 | 392 | function cw_newRound(results) { 393 | camera.pos.x = camera.pos.y = 0; 394 | cw_setCameraTarget(-1); 395 | 396 | generationState = manageRound.nextGeneration( 397 | generationState, results, generationConfig() 398 | ); 399 | if (world_def.mutable_floor) { 400 | // GHOST DISABLED 401 | ghost = null; 402 | world_def.floorseed = btoa(Math.seedrandom()); 403 | } else { 404 | // RE-ENABLE GHOST 405 | ghost_reset_ghost(ghost); 406 | } 407 | currentRunner = worldRun(world_def, generationState.generation, uiListeners); 408 | setupCarUI(); 409 | cw_drawMiniMap(); 410 | resetCarUI(); 411 | } 412 | 413 | function cw_startSimulation() { 414 | cw_paused = false; 415 | window.requestAnimationFrame(gameLoop); 416 | } 417 | 418 | function cw_stopSimulation() { 419 | cw_paused = true; 420 | } 421 | 422 | function cw_clearPopulationWorld() { 423 | carMap.forEach(function(car){ 424 | car.kill(currentRunner, world_def); 425 | }); 426 | } 427 | 428 | function cw_resetPopulationUI() { 429 | document.getElementById("generation").innerHTML = ""; 430 | document.getElementById("cars").innerHTML = ""; 431 | document.getElementById("topscores").innerHTML = ""; 432 | cw_clearGraphics(document.getElementById("graphcanvas")); 433 | resetGraphState(); 434 | } 435 | 436 | function cw_resetWorld() { 437 | doDraw = true; 438 | cw_stopSimulation(); 439 | world_def.floorseed = document.getElementById("newseed").value; 440 | cw_clearPopulationWorld(); 441 | cw_resetPopulationUI(); 442 | 443 | Math.seedrandom(); 444 | cw_generationZero(); 445 | currentRunner = worldRun( 446 | world_def, generationState.generation, uiListeners 447 | ); 448 | 449 | ghost = ghost_create_ghost(); 450 | resetCarUI(); 451 | setupCarUI() 452 | cw_drawMiniMap(); 453 | 454 | cw_startSimulation(); 455 | } 456 | 457 | function setupCarUI(){ 458 | currentRunner.cars.map(function(carInfo){ 459 | var car = new cw_Car(carInfo, carMap); 460 | carMap.set(carInfo, car); 461 | car.replay = ghost_create_replay(); 462 | ghost_add_replay_frame(car.replay, car.car.car); 463 | }) 464 | } 465 | 466 | 467 | document.querySelector("#fast-forward").addEventListener("click", function(){ 468 | fastForward() 469 | }); 470 | 471 | document.querySelector("#save-progress").addEventListener("click", function(){ 472 | saveProgress() 473 | }); 474 | 475 | document.querySelector("#restore-progress").addEventListener("click", function(){ 476 | restoreProgress() 477 | }); 478 | 479 | document.querySelector("#toggle-display").addEventListener("click", function(){ 480 | toggleDisplay() 481 | }) 482 | 483 | document.querySelector("#new-population").addEventListener("click", function(){ 484 | cw_resetPopulationUI() 485 | cw_generationZero(); 486 | ghost = ghost_create_ghost(); 487 | resetCarUI(); 488 | }) 489 | 490 | function saveProgress() { 491 | localStorage.cw_savedGeneration = JSON.stringify(generationState.generation); 492 | localStorage.cw_genCounter = generationState.counter; 493 | localStorage.cw_ghost = JSON.stringify(ghost); 494 | localStorage.cw_topScores = JSON.stringify(graphState.cw_topScores); 495 | localStorage.cw_floorSeed = world_def.floorseed; 496 | } 497 | 498 | function restoreProgress() { 499 | if (typeof localStorage.cw_savedGeneration == 'undefined' || localStorage.cw_savedGeneration == null) { 500 | alert("No saved progress found"); 501 | return; 502 | } 503 | cw_stopSimulation(); 504 | generationState.generation = JSON.parse(localStorage.cw_savedGeneration); 505 | generationState.counter = localStorage.cw_genCounter; 506 | ghost = JSON.parse(localStorage.cw_ghost); 507 | graphState.cw_topScores = JSON.parse(localStorage.cw_topScores); 508 | world_def.floorseed = localStorage.cw_floorSeed; 509 | document.getElementById("newseed").value = world_def.floorseed; 510 | 511 | currentRunner = worldRun(world_def, generationState.generation, uiListeners); 512 | cw_drawMiniMap(); 513 | Math.seedrandom(); 514 | 515 | resetCarUI(); 516 | cw_startSimulation(); 517 | } 518 | 519 | document.querySelector("#confirm-reset").addEventListener("click", function(){ 520 | cw_confirmResetWorld() 521 | }) 522 | 523 | function cw_confirmResetWorld() { 524 | if (confirm('Really reset world?')) { 525 | cw_resetWorld(); 526 | } else { 527 | return false; 528 | } 529 | } 530 | 531 | // ghost replay stuff 532 | 533 | 534 | function cw_pauseSimulation() { 535 | cw_paused = true; 536 | ghost_pause(ghost); 537 | } 538 | 539 | function cw_resumeSimulation() { 540 | cw_paused = false; 541 | ghost_resume(ghost); 542 | window.requestAnimationFrame(gameLoop); 543 | } 544 | 545 | function cw_startGhostReplay() { 546 | if (!doDraw) { 547 | toggleDisplay(); 548 | } 549 | cw_pauseSimulation(); 550 | cw_ghostReplayInterval = setInterval(cw_drawGhostReplay, Math.round(1000 / screenfps)); 551 | } 552 | 553 | function cw_stopGhostReplay() { 554 | clearInterval(cw_ghostReplayInterval); 555 | cw_ghostReplayInterval = null; 556 | cw_findLeader(); 557 | camera.pos.x = leaderPosition.x; 558 | camera.pos.y = leaderPosition.y; 559 | cw_resumeSimulation(); 560 | } 561 | 562 | document.querySelector("#toggle-ghost").addEventListener("click", function(e){ 563 | cw_toggleGhostReplay(e.target) 564 | }) 565 | 566 | function cw_toggleGhostReplay(button) { 567 | if (cw_ghostReplayInterval == null) { 568 | cw_startGhostReplay(); 569 | button.value = "Resume simulation"; 570 | } else { 571 | cw_stopGhostReplay(); 572 | button.value = "View top replay"; 573 | } 574 | } 575 | // ghost replay stuff END 576 | 577 | // initial stuff, only called once (hopefully) 578 | function cw_init() { 579 | // clone silver dot and health bar 580 | var mmm = document.getElementsByName('minimapmarker')[0]; 581 | var hbar = document.getElementsByName('healthbar')[0]; 582 | var generationSize = generationConfig.constants.generationSize; 583 | 584 | for (var k = 0; k < generationSize; k++) { 585 | 586 | // minimap markers 587 | var newbar = mmm.cloneNode(true); 588 | newbar.id = "bar" + k; 589 | newbar.style.paddingTop = k * 9 + "px"; 590 | minimapholder.appendChild(newbar); 591 | 592 | // health bars 593 | var newhealth = hbar.cloneNode(true); 594 | newhealth.getElementsByTagName("DIV")[0].id = "health" + k; 595 | newhealth.car_index = k; 596 | document.getElementById("health").appendChild(newhealth); 597 | } 598 | mmm.parentNode.removeChild(mmm); 599 | hbar.parentNode.removeChild(hbar); 600 | world_def.floorseed = btoa(Math.seedrandom()); 601 | cw_generationZero(); 602 | ghost = ghost_create_ghost(); 603 | resetCarUI(); 604 | currentRunner = worldRun(world_def, generationState.generation, uiListeners); 605 | setupCarUI(); 606 | cw_drawMiniMap(); 607 | window.requestAnimationFrame(gameLoop); 608 | 609 | } 610 | 611 | function relMouseCoords(event) { 612 | var totalOffsetX = 0; 613 | var totalOffsetY = 0; 614 | var canvasX = 0; 615 | var canvasY = 0; 616 | var currentElement = this; 617 | 618 | do { 619 | totalOffsetX += currentElement.offsetLeft - currentElement.scrollLeft; 620 | totalOffsetY += currentElement.offsetTop - currentElement.scrollTop; 621 | currentElement = currentElement.offsetParent 622 | } 623 | while (currentElement); 624 | 625 | canvasX = event.pageX - totalOffsetX; 626 | canvasY = event.pageY - totalOffsetY; 627 | 628 | return {x: canvasX, y: canvasY} 629 | } 630 | HTMLDivElement.prototype.relMouseCoords = relMouseCoords; 631 | minimapholder.onclick = function (event) { 632 | var coords = minimapholder.relMouseCoords(event); 633 | var cw_carArray = Array.from(carMap.values()); 634 | var closest = { 635 | value: cw_carArray[0].car, 636 | dist: Math.abs(((cw_carArray[0].getPosition().x + 6) * minimapscale) - coords.x), 637 | x: cw_carArray[0].getPosition().x 638 | } 639 | 640 | var maxX = 0; 641 | for (var i = 0; i < cw_carArray.length; i++) { 642 | var pos = cw_carArray[i].getPosition(); 643 | var dist = Math.abs(((pos.x + 6) * minimapscale) - coords.x); 644 | if (dist < closest.dist) { 645 | closest.value = cw_carArray.car; 646 | closest.dist = dist; 647 | closest.x = pos.x; 648 | } 649 | maxX = Math.max(pos.x, maxX); 650 | } 651 | 652 | if (closest.x == maxX) { // focus on leader again 653 | cw_setCameraTarget(-1); 654 | } else { 655 | cw_setCameraTarget(closest.value); 656 | } 657 | } 658 | 659 | 660 | document.querySelector("#mutationrate").addEventListener("change", function(e){ 661 | var elem = e.target 662 | cw_setMutation(elem.options[elem.selectedIndex].value) 663 | }) 664 | 665 | document.querySelector("#mutationsize").addEventListener("change", function(e){ 666 | var elem = e.target 667 | cw_setMutationRange(elem.options[elem.selectedIndex].value) 668 | }) 669 | 670 | document.querySelector("#floor").addEventListener("change", function(e){ 671 | var elem = e.target 672 | cw_setMutableFloor(elem.options[elem.selectedIndex].value) 673 | }); 674 | 675 | document.querySelector("#gravity").addEventListener("change", function(e){ 676 | var elem = e.target 677 | cw_setGravity(elem.options[elem.selectedIndex].value) 678 | }) 679 | 680 | document.querySelector("#elitesize").addEventListener("change", function(e){ 681 | var elem = e.target 682 | cw_setEliteSize(elem.options[elem.selectedIndex].value) 683 | }) 684 | 685 | function cw_setMutation(mutation) { 686 | generationConfig.constants.gen_mutation = parseFloat(mutation); 687 | } 688 | 689 | function cw_setMutationRange(range) { 690 | generationConfig.constants.mutation_range = parseFloat(range); 691 | } 692 | 693 | function cw_setMutableFloor(choice) { 694 | world_def.mutable_floor = (choice == 1); 695 | } 696 | 697 | function cw_setGravity(choice) { 698 | world_def.gravity = new b2Vec2(0.0, -parseFloat(choice)); 699 | var world = currentRunner.scene.world 700 | // CHECK GRAVITY CHANGES 701 | if (world.GetGravity().y != world_def.gravity.y) { 702 | world.SetGravity(world_def.gravity); 703 | } 704 | } 705 | 706 | function cw_setEliteSize(clones) { 707 | generationConfig.constants.championLength = parseInt(clones, 10); 708 | } 709 | 710 | cw_init(); 711 | -------------------------------------------------------------------------------- /src/machine-learning/create-instance.js: -------------------------------------------------------------------------------- 1 | var random = require("./random.js"); 2 | 3 | module.exports = { 4 | createGenerationZero(schema, generator){ 5 | return Object.keys(schema).reduce(function(instance, key){ 6 | var schemaProp = schema[key]; 7 | var values = random.createNormals(schemaProp, generator); 8 | instance[key] = values; 9 | return instance; 10 | }, { id: Math.random().toString(32) }); 11 | }, 12 | createCrossBreed(schema, parents, parentChooser){ 13 | var id = Math.random().toString(32); 14 | return Object.keys(schema).reduce(function(crossDef, key){ 15 | var schemaDef = schema[key]; 16 | var values = []; 17 | for(var i = 0, l = schemaDef.length; i < l; i++){ 18 | var p = parentChooser(id, key, parents); 19 | values.push(parents[p][key][i]); 20 | } 21 | crossDef[key] = values; 22 | return crossDef; 23 | }, { 24 | id: id, 25 | ancestry: parents.map(function(parent){ 26 | return { 27 | id: parent.id, 28 | ancestry: parent.ancestry, 29 | }; 30 | }) 31 | }); 32 | }, 33 | createMutatedClone(schema, generator, parent, factor, chanceToMutate){ 34 | return Object.keys(schema).reduce(function(clone, key){ 35 | var schemaProp = schema[key]; 36 | var originalValues = parent[key]; 37 | var values = random.mutateNormals( 38 | schemaProp, generator, originalValues, factor, chanceToMutate 39 | ); 40 | clone[key] = values; 41 | return clone; 42 | }, { 43 | id: parent.id, 44 | ancestry: parent.ancestry 45 | }); 46 | }, 47 | applyTypes(schema, parent){ 48 | return Object.keys(schema).reduce(function(clone, key){ 49 | var schemaProp = schema[key]; 50 | var originalValues = parent[key]; 51 | var values; 52 | switch(schemaProp.type){ 53 | case "shuffle" : 54 | values = random.mapToShuffle(schemaProp, originalValues); break; 55 | case "float" : 56 | values = random.mapToFloat(schemaProp, originalValues); break; 57 | case "integer": 58 | values = random.mapToInteger(schemaProp, originalValues); break; 59 | default: 60 | throw new Error(`Unknown type ${schemaProp.type} of schema for key ${key}`); 61 | } 62 | clone[key] = values; 63 | return clone; 64 | }, { 65 | id: parent.id, 66 | ancestry: parent.ancestry 67 | }); 68 | }, 69 | } 70 | -------------------------------------------------------------------------------- /src/machine-learning/genetic-algorithm/manage-round.js: -------------------------------------------------------------------------------- 1 | var create = require("../create-instance"); 2 | 3 | module.exports = { 4 | generationZero: generationZero, 5 | nextGeneration: nextGeneration 6 | } 7 | 8 | function generationZero(config){ 9 | var generationSize = config.generationSize, 10 | schema = config.schema; 11 | var cw_carGeneration = []; 12 | for (var k = 0; k < generationSize; k++) { 13 | var def = create.createGenerationZero(schema, function(){ 14 | return Math.random() 15 | }); 16 | def.index = k; 17 | cw_carGeneration.push(def); 18 | } 19 | return { 20 | counter: 0, 21 | generation: cw_carGeneration, 22 | }; 23 | } 24 | 25 | function nextGeneration( 26 | previousState, 27 | scores, 28 | config 29 | ){ 30 | var champion_length = config.championLength, 31 | generationSize = config.generationSize, 32 | selectFromAllParents = config.selectFromAllParents; 33 | 34 | var newGeneration = new Array(); 35 | var newborn; 36 | for (var k = 0; k < champion_length; k++) {`` 37 | scores[k].def.is_elite = true; 38 | scores[k].def.index = k; 39 | newGeneration.push(scores[k].def); 40 | } 41 | var parentList = []; 42 | for (k = champion_length; k < generationSize; k++) { 43 | var parent1 = selectFromAllParents(scores, parentList); 44 | var parent2 = parent1; 45 | while (parent2 == parent1) { 46 | parent2 = selectFromAllParents(scores, parentList, parent1); 47 | } 48 | var pair = [parent1, parent2] 49 | parentList.push(pair); 50 | newborn = makeChild(config, 51 | pair.map(function(parent) { return scores[parent].def; }) 52 | ); 53 | newborn = mutate(config, newborn); 54 | newborn.is_elite = false; 55 | newborn.index = k; 56 | newGeneration.push(newborn); 57 | } 58 | 59 | return { 60 | counter: previousState.counter + 1, 61 | generation: newGeneration, 62 | }; 63 | } 64 | 65 | 66 | function makeChild(config, parents){ 67 | var schema = config.schema, 68 | pickParent = config.pickParent; 69 | return create.createCrossBreed(schema, parents, pickParent) 70 | } 71 | 72 | 73 | function mutate(config, parent){ 74 | var schema = config.schema, 75 | mutation_range = config.mutation_range, 76 | gen_mutation = config.gen_mutation, 77 | generateRandom = config.generateRandom; 78 | return create.createMutatedClone( 79 | schema, 80 | generateRandom, 81 | parent, 82 | Math.max(mutation_range), 83 | gen_mutation 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /src/machine-learning/random.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const random = { 4 | shuffleIntegers(prop, generator){ 5 | return random.mapToShuffle(prop, random.createNormals({ 6 | length: prop.length || 10, 7 | inclusive: true, 8 | }, generator)); 9 | }, 10 | createIntegers(prop, generator){ 11 | return random.mapToInteger(prop, random.createNormals({ 12 | length: prop.length, 13 | inclusive: true, 14 | }, generator)); 15 | }, 16 | createFloats(prop, generator){ 17 | return random.mapToFloat(prop, random.createNormals({ 18 | length: prop.length, 19 | inclusive: true, 20 | }, generator)); 21 | }, 22 | createNormals(prop, generator){ 23 | var l = prop.length; 24 | var values = []; 25 | for(var i = 0; i < l; i++){ 26 | values.push( 27 | createNormal(prop, generator) 28 | ); 29 | } 30 | return values; 31 | }, 32 | mutateShuffle( 33 | prop, generator, originalValues, mutation_range, chanceToMutate 34 | ){ 35 | return random.mapToShuffle(prop, random.mutateNormals( 36 | prop, generator, originalValues, mutation_range, chanceToMutate 37 | )); 38 | }, 39 | mutateIntegers(prop, generator, originalValues, mutation_range, chanceToMutate){ 40 | return random.mapToInteger(prop, random.mutateNormals( 41 | prop, generator, originalValues, mutation_range, chanceToMutate 42 | )); 43 | }, 44 | mutateFloats(prop, generator, originalValues, mutation_range, chanceToMutate){ 45 | return random.mapToFloat(prop, random.mutateNormals( 46 | prop, generator, originalValues, mutation_range, chanceToMutate 47 | )); 48 | }, 49 | mapToShuffle(prop, normals){ 50 | var offset = prop.offset || 0; 51 | var limit = prop.limit || prop.length; 52 | var sorted = normals.slice().sort(function(a, b){ 53 | return a - b; 54 | }); 55 | return normals.map(function(val){ 56 | return sorted.indexOf(val); 57 | }).map(function(i){ 58 | return i + offset; 59 | }).slice(0, limit); 60 | }, 61 | mapToInteger(prop, normals){ 62 | prop = { 63 | min: prop.min || 0, 64 | range: prop.range || 10, 65 | length: prop.length 66 | } 67 | return random.mapToFloat(prop, normals).map(function(float){ 68 | return Math.round(float); 69 | }); 70 | }, 71 | mapToFloat(prop, normals){ 72 | prop = { 73 | min: prop.min || 0, 74 | range: prop.range || 1 75 | } 76 | return normals.map(function(normal){ 77 | var min = prop.min; 78 | var range = prop.range; 79 | return min + normal * range 80 | }) 81 | }, 82 | mutateNormals(prop, generator, originalValues, mutation_range, chanceToMutate){ 83 | var factor = (prop.factor || 1) * mutation_range 84 | return originalValues.map(function(originalValue){ 85 | if(generator() > chanceToMutate){ 86 | return originalValue; 87 | } 88 | return mutateNormal( 89 | prop, generator, originalValue, factor 90 | ); 91 | }); 92 | } 93 | }; 94 | 95 | module.exports = random; 96 | 97 | function mutateNormal(prop, generator, originalValue, mutation_range){ 98 | if(mutation_range > 1){ 99 | throw new Error("Cannot mutate beyond bounds"); 100 | } 101 | var newMin = originalValue - 0.5; 102 | if (newMin < 0) newMin = 0; 103 | if (newMin + mutation_range > 1) 104 | newMin = 1 - mutation_range; 105 | var rangeValue = createNormal({ 106 | inclusive: true, 107 | }, generator); 108 | return newMin + rangeValue * mutation_range; 109 | } 110 | 111 | function createNormal(prop, generator){ 112 | if(!prop.inclusive){ 113 | return generator(); 114 | } else { 115 | return generator() < 0.5 ? 116 | generator() : 117 | 1 - generator(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/machine-learning/simulated-annealing/manage-round.js: -------------------------------------------------------------------------------- 1 | var create = require("../create-instance"); 2 | 3 | module.exports = { 4 | generationZero: generationZero, 5 | nextGeneration: nextGeneration, 6 | } 7 | 8 | function generationZero(config){ 9 | var oldStructure = create.createGenerationZero( 10 | config.schema, config.generateRandom 11 | ); 12 | var newStructure = createStructure(config, 1, oldStructure); 13 | 14 | var k = 0; 15 | 16 | return { 17 | counter: 0, 18 | k: k, 19 | generation: [newStructure, oldStructure] 20 | } 21 | } 22 | 23 | function nextGeneration(previousState, scores, config){ 24 | var nextState = { 25 | k: (previousState.k + 1)%config.generationSize, 26 | counter: previousState.counter + (previousState.k === config.generationSize ? 1 : 0) 27 | }; 28 | // gradually get closer to zero temperature (but never hit it) 29 | var oldDef = previousState.curDef || previousState.generation[1]; 30 | var oldScore = previousState.score || scores[1].score.v; 31 | 32 | var newDef = previousState.generation[0]; 33 | var newScore = scores[0].score.v; 34 | 35 | 36 | var temp = Math.pow(Math.E, -nextState.counter / config.generationSize); 37 | 38 | var scoreDiff = newScore - oldScore; 39 | // If the next point is higher, change location 40 | if(scoreDiff > 0){ 41 | nextState.curDef = newDef; 42 | nextState.score = newScore; 43 | // Else we want to increase likelyhood of changing location as we get 44 | } else if(Math.random() > Math.exp(-scoreDiff/(nextState.k * temp))){ 45 | nextState.curDef = newDef; 46 | nextState.score = newScore; 47 | } else { 48 | nextState.curDef = oldDef; 49 | nextState.score = oldScore; 50 | } 51 | 52 | console.log(previousState, nextState); 53 | 54 | nextState.generation = [createStructure(config, temp, nextState.curDef)]; 55 | 56 | return nextState; 57 | } 58 | 59 | 60 | function createStructure(config, mutation_range, parent){ 61 | var schema = config.schema, 62 | gen_mutation = 1, 63 | generateRandom = config.generateRandom; 64 | return create.createMutatedClone( 65 | schema, 66 | generateRandom, 67 | parent, 68 | mutation_range, 69 | gen_mutation 70 | ) 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/world/run.js: -------------------------------------------------------------------------------- 1 | /* globals btoa */ 2 | var setupScene = require("./setup-scene"); 3 | var carRun = require("../car-schema/run"); 4 | var defToCar = require("../car-schema/def-to-car"); 5 | 6 | module.exports = runDefs; 7 | function runDefs(world_def, defs, listeners) { 8 | if (world_def.mutable_floor) { 9 | // GHOST DISABLED 10 | world_def.floorseed = btoa(Math.seedrandom()); 11 | } 12 | 13 | var scene = setupScene(world_def); 14 | scene.world.Step(1 / world_def.box2dfps, 20, 20); 15 | console.log("about to build cars"); 16 | var cars = defs.map((def, i) => { 17 | return { 18 | index: i, 19 | def: def, 20 | car: defToCar(def, scene.world, world_def), 21 | state: carRun.getInitialState(world_def) 22 | }; 23 | }); 24 | var alivecars = cars; 25 | return { 26 | scene: scene, 27 | cars: cars, 28 | step: function () { 29 | if (alivecars.length === 0) { 30 | throw new Error("no more cars"); 31 | } 32 | scene.world.Step(1 / world_def.box2dfps, 20, 20); 33 | listeners.preCarStep(); 34 | alivecars = alivecars.filter(function (car) { 35 | car.state = carRun.updateState( 36 | world_def, car.car, car.state 37 | ); 38 | var status = carRun.getStatus(car.state, world_def); 39 | listeners.carStep(car); 40 | if (status === 0) { 41 | return true; 42 | } 43 | car.score = carRun.calculateScore(car.state, world_def); 44 | listeners.carDeath(car); 45 | 46 | var world = scene.world; 47 | var worldCar = car.car; 48 | world.DestroyBody(worldCar.chassis); 49 | 50 | for (var w = 0; w < worldCar.wheels.length; w++) { 51 | world.DestroyBody(worldCar.wheels[w]); 52 | } 53 | 54 | return false; 55 | }) 56 | if (alivecars.length === 0) { 57 | listeners.generationEnd(cars); 58 | } 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/world/setup-scene.js: -------------------------------------------------------------------------------- 1 | /* globals b2World b2Vec2 b2BodyDef b2FixtureDef b2PolygonShape */ 2 | 3 | /* 4 | 5 | world_def = { 6 | gravity: {x, y}, 7 | doSleep: boolean, 8 | floorseed: string, 9 | tileDimensions, 10 | maxFloorTiles, 11 | mutable_floor: boolean 12 | } 13 | 14 | */ 15 | 16 | module.exports = function(world_def){ 17 | 18 | var world = new b2World(world_def.gravity, world_def.doSleep); 19 | var floorTiles = cw_createFloor( 20 | world, 21 | world_def.floorseed, 22 | world_def.tileDimensions, 23 | world_def.maxFloorTiles, 24 | world_def.mutable_floor 25 | ); 26 | 27 | var last_tile = floorTiles[ 28 | floorTiles.length - 1 29 | ]; 30 | var last_fixture = last_tile.GetFixtureList(); 31 | var tile_position = last_tile.GetWorldPoint( 32 | last_fixture.GetShape().m_vertices[3] 33 | ); 34 | world.finishLine = tile_position.x; 35 | return { 36 | world: world, 37 | floorTiles: floorTiles, 38 | finishLine: tile_position.x 39 | }; 40 | } 41 | 42 | function cw_createFloor(world, floorseed, dimensions, maxFloorTiles, mutable_floor) { 43 | var last_tile = null; 44 | var tile_position = new b2Vec2(-5, 0); 45 | var cw_floorTiles = []; 46 | Math.seedrandom(floorseed); 47 | for (var k = 0; k < maxFloorTiles; k++) { 48 | if (!mutable_floor) { 49 | // keep old impossible tracks if not using mutable floors 50 | last_tile = cw_createFloorTile( 51 | world, dimensions, tile_position, (Math.random() * 3 - 1.5) * 1.5 * k / maxFloorTiles 52 | ); 53 | } else { 54 | // if path is mutable over races, create smoother tracks 55 | last_tile = cw_createFloorTile( 56 | world, dimensions, tile_position, (Math.random() * 3 - 1.5) * 1.2 * k / maxFloorTiles 57 | ); 58 | } 59 | cw_floorTiles.push(last_tile); 60 | var last_fixture = last_tile.GetFixtureList(); 61 | tile_position = last_tile.GetWorldPoint(last_fixture.GetShape().m_vertices[3]); 62 | } 63 | return cw_floorTiles; 64 | } 65 | 66 | 67 | function cw_createFloorTile(world, dim, position, angle) { 68 | var body_def = new b2BodyDef(); 69 | 70 | body_def.position.Set(position.x, position.y); 71 | var body = world.CreateBody(body_def); 72 | var fix_def = new b2FixtureDef(); 73 | fix_def.shape = new b2PolygonShape(); 74 | fix_def.friction = 0.5; 75 | 76 | var coords = new Array(); 77 | coords.push(new b2Vec2(0, 0)); 78 | coords.push(new b2Vec2(0, -dim.y)); 79 | coords.push(new b2Vec2(dim.x, -dim.y)); 80 | coords.push(new b2Vec2(dim.x, 0)); 81 | 82 | var center = new b2Vec2(0, 0); 83 | 84 | var newcoords = cw_rotateFloorTile(coords, center, angle); 85 | 86 | fix_def.shape.SetAsArray(newcoords); 87 | 88 | body.CreateFixture(fix_def); 89 | return body; 90 | } 91 | 92 | function cw_rotateFloorTile(coords, center, angle) { 93 | return coords.map(function(coord){ 94 | return { 95 | x: Math.cos(angle) * (coord.x - center.x) - Math.sin(angle) * (coord.y - center.y) + center.x, 96 | y: Math.sin(angle) * (coord.x - center.x) + Math.cos(angle) * (coord.y - center.y) + center.y, 97 | }; 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 15px; 3 | font-family: sans-serif; 4 | color: #112D4E; 5 | line-height: 1.4; 6 | } 7 | 8 | html * { 9 | box-sizing: border-box; 10 | } 11 | 12 | body { 13 | padding: 20px; 14 | margin: 0; 15 | min-width: 1200px; 16 | width: 100%; 17 | } 18 | 19 | .clearfix:after { 20 | content: "."; 21 | clear: both; 22 | display: block; 23 | visibility: hidden; 24 | height: 0; 25 | } 26 | 27 | .float-left { 28 | float: left; 29 | } 30 | 31 | #mainbox { 32 | width: 800px; 33 | height: 400px; 34 | border: 1px solid #BE4747; 35 | } 36 | 37 | #div { 38 | width: 800px; 39 | height: 400px; 40 | border: 1px solid #112D4E; 41 | } 42 | 43 | #data { 44 | width: 400px; 45 | padding: 0 20px 20px; 46 | } 47 | 48 | #data table { 49 | width: 100%; 50 | } 51 | 52 | [type="button"] { 53 | border: none; 54 | background: #3F72AF; 55 | color: #F9F7F7; 56 | margin: 0 0 5px; 57 | padding: 5px 10px; 58 | cursor: pointer; 59 | display: inline-block; 60 | } 61 | 62 | .buttons [type="button"] { 63 | min-width: 49%; 64 | } 65 | 66 | #graphholder { 67 | position: relative; 68 | padding-right: 40px; 69 | } 70 | 71 | #graphholder .scale { 72 | position: absolute; 73 | left: 405px; 74 | font-size: 9px; 75 | } 76 | 77 | #s0 { 78 | top: 240px; 79 | } 80 | 81 | #s25 { 82 | top: 187px; 83 | } 84 | 85 | #s50 { 86 | top: 125px; 87 | } 88 | 89 | #s75 { 90 | top: 62px; 91 | } 92 | 93 | #s100 { 94 | top: 0px; 95 | } 96 | 97 | #graphcanvas { 98 | border: 1px solid #112D4E; 99 | } 100 | 101 | #topscoreholder { 102 | font-size: .9rem; 103 | } 104 | 105 | #minimapholder { 106 | position: relative; 107 | border: 1px solid #112D4E; 108 | width: 800px; 109 | height: 200px; 110 | overflow: hidden; 111 | } 112 | 113 | .minimapmarker { 114 | position: absolute; 115 | left: 0; 116 | top: 0; 117 | width: 1px; 118 | height: 200px; 119 | z-index: 5; 120 | border-left: 1px solid #112D4E; 121 | font-size: 9px; 122 | padding-left: 2px; 123 | } 124 | 125 | .silverdot { 126 | position: absolute; 127 | left: 0; 128 | top: 0; 129 | width: 1px; 130 | height: 200px; 131 | z-index: 4; 132 | } 133 | 134 | #minimapfog { 135 | width: 798px; 136 | height: 198px; 137 | position: absolute; 138 | top: 1px; 139 | right: 1px; 140 | z-index: 2; 141 | background-color: #fff; 142 | } 143 | 144 | #minimapcamera { 145 | position: absolute; 146 | top: 0px; 147 | left: 1px; 148 | height: 199px; 149 | width: 50px; 150 | z-index: 3; 151 | border: 1px dashed #999; 152 | } 153 | 154 | .healthbar { 155 | cursor: pointer; 156 | position: relative; 157 | border: 1px solid #112D4E; 158 | width: 100px; 159 | height: 18px; 160 | padding: 2px; 161 | margin-left: 25px; 162 | } 163 | 164 | .healthbar .health { 165 | height: 100%; 166 | width: 100%; 167 | background-color: #BE4747; 168 | } 169 | 170 | .healthbar .healthtext { 171 | position: absolute; 172 | top: 0; 173 | left: -20px; 174 | font-size: .8rem; 175 | } 176 | 177 | #cars { 178 | margin-top: 20px; 179 | } 180 | 181 | #explanation { 182 | font-size: .8rem; 183 | margin-top: 50px; 184 | color: #444; 185 | } 186 | 187 | table { 188 | border-collapse:collapse; 189 | margin-bottom: 5px; 190 | } 191 | 192 | table td, 193 | table th { 194 | padding: 0 5px; 195 | text-align: left; 196 | border-bottom: 1px solid #CCC; 197 | } 198 | --------------------------------------------------------------------------------