forked from gniziemazity/self-driving-car
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
70b48f3
commit 161916a
Showing
11 changed files
with
627 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
class Car{ | ||
constructor(x,y,width,height,controlType,maxSpeed=3){ | ||
this.x=x; | ||
this.y=y; | ||
this.width=width; | ||
this.height=height; | ||
|
||
this.speed=0; | ||
this.acceleration=0.2; | ||
this.maxSpeed=maxSpeed; | ||
this.friction=0.05; | ||
this.angle=0; | ||
|
||
this.damaged=false; | ||
|
||
this.useBrain=controlType=="AI"; | ||
|
||
if(controlType!="DUMMY"){ | ||
this.sensor=new Sensor(); | ||
this.brain=new NeuralNetwork( | ||
[this.sensor.rayCount,4] | ||
); | ||
} | ||
this.controls=new Controls(controlType); | ||
} | ||
|
||
update(roadBorders,traffic){ | ||
if(!this.damaged){ | ||
this.#move(); | ||
this.polygon=this.#createPolygon(); | ||
this.damaged=this.#assessDamage(roadBorders,traffic); | ||
} | ||
if(this.sensor){ | ||
this.sensor.update(this.x,this.y,this.angle,roadBorders,traffic); | ||
const offsets=this.sensor.readings.map( | ||
s=>s==null?0:1-s.offset | ||
); | ||
const outputs=NeuralNetwork.feedForward(offsets,this.brain); | ||
if(this.useBrain){ | ||
this.controls.forward=outputs[0]; | ||
this.controls.left=outputs[1]; | ||
this.controls.right=outputs[2]; | ||
this.controls.reverse=outputs[3]; | ||
} | ||
} | ||
} | ||
|
||
#assessDamage(roadBorders,traffic){ | ||
for(let i=0;i<roadBorders.length;i++){ | ||
if(polysIntersect( | ||
[...this.polygon,this.polygon[0]], | ||
roadBorders[i]) | ||
){ | ||
return true; | ||
} | ||
} | ||
for(let i=0;i<traffic.length;i++){ | ||
const poly=traffic[i].polygon; | ||
if(polysIntersect( | ||
[...this.polygon,this.polygon[0]], | ||
[...poly,poly[0]]) | ||
){ | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
#createPolygon(){ | ||
const points=[]; | ||
const rad=Math.hypot(this.width,this.height)/2; | ||
const alpha=Math.atan2(this.width,this.height); | ||
points.push({ | ||
x:this.x-Math.sin(this.angle-alpha)*rad, | ||
y:this.y-Math.cos(this.angle-alpha)*rad | ||
}); | ||
points.push({ | ||
x:this.x-Math.sin(this.angle+alpha)*rad, | ||
y:this.y-Math.cos(this.angle+alpha)*rad | ||
}); | ||
points.push({ | ||
x:this.x-Math.sin(Math.PI+this.angle-alpha)*rad, | ||
y:this.y-Math.cos(Math.PI+this.angle-alpha)*rad | ||
}); | ||
points.push({ | ||
x:this.x-Math.sin(Math.PI+this.angle+alpha)*rad, | ||
y:this.y-Math.cos(Math.PI+this.angle+alpha)*rad | ||
}); | ||
return points; | ||
} | ||
|
||
#move(){ | ||
if(this.controls.forward){ | ||
this.speed+=this.acceleration; | ||
} | ||
if(this.controls.reverse){ | ||
this.speed-=this.acceleration; | ||
} | ||
|
||
if(this.speed!=0){ | ||
const flip=this.speed>0?1:-1; | ||
if(this.controls.left){ | ||
this.angle+=0.03*flip; | ||
} | ||
if(this.controls.right){ | ||
this.angle-=0.03*flip; | ||
} | ||
} | ||
|
||
if(this.speed>this.maxSpeed){ | ||
this.speed=this.maxSpeed; | ||
} | ||
if(this.speed<-this.maxSpeed/2){ | ||
this.speed=-this.maxSpeed/2; | ||
} | ||
|
||
if(this.speed>0){ | ||
this.speed-=this.friction; | ||
} | ||
if(this.speed<0){ | ||
this.speed+=this.friction; | ||
} | ||
if(Math.abs(this.speed)<this.friction){ | ||
this.speed=0; | ||
} | ||
|
||
this.x-=Math.sin(this.angle)*this.speed; | ||
this.y-=Math.cos(this.angle)*this.speed; | ||
} | ||
|
||
draw(ctx,drawSensor=false){ | ||
if(this.damaged){ | ||
ctx.fillStyle="gray"; | ||
}else{ | ||
ctx.fillStyle="black"; | ||
} | ||
ctx.beginPath(); | ||
ctx.moveTo(this.polygon[0].x,this.polygon[0].y); | ||
for(let i=1;i<this.polygon.length;i++){ | ||
ctx.lineTo(this.polygon[i].x,this.polygon[i].y); | ||
} | ||
ctx.fill(); | ||
if(this.sensor && drawSensor){ | ||
this.sensor.draw(ctx); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
class Controls{ | ||
constructor(type){ | ||
this.forward=false; | ||
this.left=false; | ||
this.right=false; | ||
this.reverse=false; | ||
|
||
switch(type){ | ||
case "KEYS": | ||
this.#addKeyboardListeners(); | ||
break; | ||
case "DUMMY": | ||
this.forward=true; | ||
break; | ||
} | ||
} | ||
|
||
#addKeyboardListeners(){ | ||
document.onkeydown=(event)=>{ | ||
switch(event.key){ | ||
case "ArrowLeft": | ||
this.left=true; | ||
break; | ||
case "ArrowRight": | ||
this.right=true; | ||
break; | ||
case "ArrowUp": | ||
this.forward=true; | ||
break; | ||
case "ArrowDown": | ||
this.reverse=true; | ||
break; | ||
} | ||
} | ||
document.onkeyup=(event)=>{ | ||
switch(event.key){ | ||
case "ArrowLeft": | ||
this.left=false; | ||
break; | ||
case "ArrowRight": | ||
this.right=false; | ||
break; | ||
case "ArrowUp": | ||
this.forward=false; | ||
break; | ||
case "ArrowDown": | ||
this.reverse=false; | ||
break; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>JS - Self-driving Car - LIVE</title> | ||
<link rel="stylesheet" href="style.css"> | ||
</head> | ||
<body> | ||
<canvas id="carCanvas"></canvas><canvas id="networkCanvas"></canvas> | ||
<script src="visualizer.js"></script> | ||
<script src="network.js"></script> | ||
<script src="sensor.js"></script> | ||
<script src="utils.js"></script> | ||
<script src="road.js"></script> | ||
<script src="controls.js"></script> | ||
<script src="car.js"></script> | ||
<script src="main.js"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
carCanvas.height=window.innerHeight; | ||
carCanvas.width=200; | ||
networkCanvas.height=window.innerHeight; | ||
networkCanvas.width=298; | ||
|
||
const carCtx=carCanvas.getContext("2d"); | ||
const networkCtx=networkCanvas.getContext("2d"); | ||
const road=new Road(carCanvas.width/2,carCanvas.width*0.9); | ||
const N=100; | ||
const cars=generateCars(N); | ||
const traffic=[ | ||
new Car(100,-100,30,50,"DUMMY",2) | ||
]; | ||
let bestCar=cars[0]; | ||
if(localStorage.getItem("bestBrain")){ | ||
for(let i=0;i<cars.length;i++){ | ||
cars[i].brain=JSON.parse( | ||
localStorage.getItem("bestBrain")); | ||
if(i>0){ | ||
NeuralNetwork.mutate(cars[i].brain,0.4); | ||
} | ||
} | ||
} | ||
|
||
animate(); | ||
|
||
function animate(){ | ||
for(let i=0;i<traffic.length;i++){ | ||
traffic[i].update([],[]); | ||
} | ||
for(let i=0;i<cars.length;i++){ | ||
cars[i].update(road.borders,traffic); | ||
} | ||
bestCar=cars.find( | ||
c=>c.y==Math.min( | ||
...cars.map(c=>c.y) | ||
)); | ||
|
||
carCanvas.height=window.innerHeight; | ||
networkCanvas.height=window.innerHeight; | ||
|
||
carCtx.translate(0,-bestCar.y+carCanvas.height*0.7); | ||
road.draw(carCtx); | ||
for(let i=0;i<traffic.length;i++){ | ||
traffic[i].draw(carCtx); | ||
} | ||
carCtx.globalAlpha=0.2; | ||
for(let i=0;i<cars.length;i++){ | ||
cars[i].draw(carCtx); | ||
} | ||
carCtx.globalAlpha=1; | ||
bestCar.draw(carCtx,true); | ||
|
||
Visualizer.drawNetwork(networkCtx,bestCar.brain); | ||
|
||
requestAnimationFrame(animate); | ||
} | ||
|
||
function generateCars(N){ | ||
const cars=[]; | ||
for(let i=1;i<=N;i++){ | ||
cars.push(new Car(100,100,30,50,"AI")); | ||
} | ||
return cars; | ||
} | ||
|
||
function save(){ | ||
localStorage.setItem("bestBrain", | ||
JSON.stringify(bestCar.brain)); | ||
} | ||
|
||
function discard(){ | ||
localStorage.removeItem("bestBrain"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
class NeuralNetwork{ | ||
constructor(neuronCounts){ | ||
this.levels=[]; | ||
for(let i=0;i<neuronCounts.length-1;i++){ | ||
this.levels.push(new Level( | ||
neuronCounts[i],neuronCounts[i+1] | ||
)); | ||
} | ||
} | ||
|
||
static feedForward(givenInputs,network){ | ||
let outputs=Level.feedForward( | ||
givenInputs,network.levels[0]); | ||
for(let i=1;i<network.levels.length;i++){ | ||
outputs=Level.feedForward( | ||
outputs,network.levels[i]); | ||
} | ||
return outputs; | ||
} | ||
|
||
static mutate(network,amount=1){ | ||
network.levels.forEach(level => { | ||
for(let i=0;i<level.biases.length;i++){ | ||
level.biases[i]=lerp( | ||
level.biases[i], | ||
Math.random()*2-1, | ||
amount | ||
) | ||
} | ||
for(let i=0;i<level.weights.length;i++){ | ||
for(let j=0;j<level.weights[i].length;j++){ | ||
level.weights[i][j]=lerp( | ||
level.weights[i][j], | ||
Math.random()*2-1, | ||
amount | ||
) | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
|
||
class Level{ | ||
constructor(inputCount,outputCount){ | ||
this.inputs=new Array(inputCount); | ||
this.outputs=new Array(outputCount); | ||
this.biases=new Array(outputCount); | ||
|
||
this.weights=[]; | ||
for(let i=0;i<inputCount;i++){ | ||
this.weights[i]=new Array(outputCount); | ||
} | ||
|
||
Level.#randomize(this); | ||
} | ||
|
||
static #randomize(level){ | ||
for(let i=0;i<level.inputs.length;i++){ | ||
for(let j=0;j<level.outputs.length;j++){ | ||
level.weights[i][j]=Math.random()*2-1; | ||
} | ||
} | ||
|
||
for(let i=0;i<level.biases.length;i++){ | ||
level.biases[i]=Math.random()*2-1; | ||
} | ||
} | ||
|
||
static feedForward(givenInputs,level){ | ||
level.inputs=[...givenInputs]; | ||
|
||
for(let i=0;i<level.outputs.length;i++){ | ||
let sum=0 | ||
for(let j=0;j<level.inputs.length;j++){ | ||
sum+=level.inputs[j]*level.weights[j][i]; | ||
} | ||
|
||
if(sum>level.biases[i]){ | ||
level.outputs[i]=1; | ||
}else{ | ||
level.outputs[i]=0; | ||
} | ||
} | ||
|
||
return level.outputs; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
Link to live stream: | ||
https://youtu.be/NUjN2Mln_Gg | ||
|
||
Instructions: | ||
|
||
Call the save() and discard() methods in the browser console when you want to store a car's brain in local storage. | ||
|
||
Play with the number of cars simulated in parallel N (line 9) and the mutation amount (line 0.4) | ||
|
||
Changing properties of the neural network or the sensor requires you to call discard() in the console so that the old ones don't come from local storage. |
Oops, something went wrong.