From f79d283c9674100e375cee5e8e5d1d36739a342e Mon Sep 17 00:00:00 2001 From: QkoSad Date: Mon, 7 Nov 2022 19:26:26 +0200 Subject: [PATCH] self driving car --- car.js | 174 ++++++++++++++++++++++++++++++++++++++++++++++ car.png | Bin 0 -> 6964 bytes controls.js | 51 ++++++++++++++ index.html | 21 ++++++ main.js | 116 +++++++++++++++++++++++++++++++ network.js | 75 ++++++++++++++++++++ road.js | 59 ++++++++++++++++ sensor.js | 95 +++++++++++++++++++++++++ style.css | 28 ++++++++ utils.js | 31 +++++++++ visualizer.js | 110 +++++++++++++++++++++++++++++ visualizer_old.js | 67 ++++++++++++++++++ 12 files changed, 827 insertions(+) create mode 100644 car.js create mode 100644 car.png create mode 100644 controls.js create mode 100644 index.html create mode 100644 main.js create mode 100644 network.js create mode 100644 road.js create mode 100644 sensor.js create mode 100644 style.css create mode 100644 utils.js create mode 100644 visualizer.js create mode 100644 visualizer_old.js diff --git a/car.js b/car.js new file mode 100644 index 0000000..cd10ed0 --- /dev/null +++ b/car.js @@ -0,0 +1,174 @@ +class Car { + constructor( + x, + y, + controlType, + color = "blue", + maxSpeed = 4, + width = 30, + height = 50 + ) { + 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.useBrain = controlType == "AI"; + if (controlType != "DUMMY") { + this.sensor = new Sensor(this); + this.brain = new NeuralNetwork([ + this.sensor.rayCount, + 6, + 4, + ]); + } + this.controls = new Controls(controlType); + this.img = new Image(); + this.img.src = "car.png"; + this.mask = document.createElement("canvas"); + this.mask.width = width; + this.mask.height = height; + + const maskCtx = this.mask.getContext("2d"); + this.img.onload = () => { + maskCtx.fillStyle = color; + maskCtx.rect(0, 0, this.width, this.height); + maskCtx.fill(); + + maskCtx.globalCompositeOperation = "destination-atop"; + maskCtx.drawImage( + this.img, + 0, + 0, + this.width, + this.height + ); + }; + } + update(roadBorders, traffic) { + if (!this.damaged) { + this.#move(); + this.polygon = this.#createPolygon(); + this.damaged = this.#assessDamage(roadBorders, traffic); + } + if (this.sensor) { + this.sensor.update(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 (polyIntersect(this.polygon, roadBorders[i])) + return true; + } + for (let i = 0; i < traffic.length; i++) { + if (polyIntersect(this.polygon, traffic[i].polygon)) { + 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; + else if (this.controls.reverse) this.speed -= this.acceleration; + if (this.speed > this.maxSpeed) this.speed = this.maxSpeed; + else 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; + 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; + } + this.x -= Math.sin(this.angle) * this.speed; + this.y -= Math.cos(this.angle) * this.speed; + } + draw(ctx, color, drawSensor = false) { + if (this.sensor && drawSensor) this.sensor.draw(ctx); + ctx.save(); + ctx.translate(this.x, this.y); + ctx.rotate(-this.angle); + if (!this.damaged) { + ctx.drawImage( + this.mask, + -this.width / 2, + -this.height / 2, + this.width, + this.height + ); + } + ctx.globalCompositeOperation = "multiply"; + ctx.drawImage( + this.img, + -this.width / 2, + -this.height / 2, + this.width, + this.height + ); + ctx.restore(); + } +} + +function polyIntersect(poly1, poly2) { + for (let i = 0; i < poly1.length; i++) { + for (let j = 0; j < poly2.length; j++) { + const touch = getIntersection( + poly1[i], + poly1[(i + 1) % poly1.length], + poly2[j], + poly2[(j + 1) % poly2.length] + ); + if (touch) return true; + } + } + return false; +} diff --git a/car.png b/car.png new file mode 100644 index 0000000000000000000000000000000000000000..d969908cc3e90b514170aadaa7b353e86aacce67 GIT binary patch literal 6964 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 literal 0 HcmV?d00001 diff --git a/controls.js b/controls.js new file mode 100644 index 0000000..33c3f51 --- /dev/null +++ b/controls.js @@ -0,0 +1,51 @@ +class Controls { + constructor(type) { + this.forward = false; + this.reverse = false; + this.left = false; + this.right = 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; + } + }; + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..029a99b --- /dev/null +++ b/index.html @@ -0,0 +1,21 @@ + + + Self-driving car - No libraries + + + + +
+ + +
+ + + + + + + + + + diff --git a/main.js b/main.js new file mode 100644 index 0000000..db1080b --- /dev/null +++ b/main.js @@ -0,0 +1,116 @@ +const carCanvas = document.getElementById("carCanvas"); +carCanvas.width = 200; + +const networkCanvas = document.getElementById("networkCanvas"); +networkCanvas.width = 0; + +const carCtx = carCanvas.getContext("2d"); +const networkCtx = networkCanvas.getContext("2d"); + +const road = new Road(carCanvas.width / 2, carCanvas.width * 0.9, 5); +const N = 200; +const cars = generateCars(N); +const traffic = []; +let minY = 500; +let carsCount = 3; +for (let i = 0; i < carsCount; i++) { + traffic.push( + new Car( + road.getLaneCenter(Math.random() * 5), + Math.random() * (-1300 + 700) - 700, + "DUMMY", + getRandomColor(), + Math.random() * 2.9 + ) + ); +} +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.2); + } + } +} +animate(); + +function save() { + localStorage.setItem("bestBrain", JSON.stringify(bestCar.brain)); + localStorage.setItem("bestScore", JSON.stringify(bestCar.y)); +} +function discard() { + localStorage.removeItem("bestBrain"); + localStorage.removeItem("bestScore"); +} +function generateCars(N) { + const cars = []; + for (let i = 0; i <= N; i++) { + cars.push(new Car(road.getLaneCenter(1), -1, "AI")); + } + return cars; +} +setInterval(() => { + if (carsCount < 15) { + traffic.push( + new Car( + road.getLaneCenter(Math.random() * 3), + bestCar.y - 800, + "DUMMY", + getRandomColor(), + Math.random() * 2.9 + ) + ); + carsCount += 1; + } + if (bestCar.y < localStorage.getItem("bestScore")) save(); + if (minY > 0) minY = -minY; + minY = minY * 1.5; +}, 5000); +function animate() { + for (let i = 0; i < traffic.length; i++) { + if ( + traffic[i].y > bestCar.y + 350 || + traffic[i].y < bestCar.y - 1400 + ) { + traffic.splice( + i, + 1, + new Car( + road.getLaneCenter(Math.random() * 3), + bestCar.y - 800, + "DUMMY", + getRandomColor(), + Math.random() * 2.9 + ) + ); + } + traffic[i].update(road.borders, []); + } + let restart = true; + for (let i = 0; i < cars.length; i++) { + cars[i].update(road.borders, traffic); + restart = restart && cars[i].damaged; + if (minY < cars[i].y) cars[i].damaged = true; + } + if (restart) location.reload(); + bestCar = cars.find((c) => c.y == Math.min(...cars.map((c) => c.y))); + carCanvas.height = window.innerHeight; + networkCanvas.height = window.innerHeight; + + carCtx.save(); + carCtx.translate(0, -bestCar.y + carCanvas.height * 0.7); + road.draw(carCtx); + for (let i = 0; i < traffic.length; i++) { + traffic[i].draw(carCtx, "red"); + } + carCtx.globalAlpha = 0.2; + for (let i = 0; i < cars.length; i++) { + cars[i].draw(carCtx, "blue"); + } + carCtx.globalAlpha = 1; + bestCar.draw(carCtx, "blue", true); + carCtx.restore(); + // Visualizer.drawNetwork(networkCtx, bestCar.brain); + requestAnimationFrame(animate); +} diff --git a/network.js b/network.js new file mode 100644 index 0000000..27316cc --- /dev/null +++ b/network.js @@ -0,0 +1,75 @@ +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.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) { + for (let i = 0; i < level.inputs.length; i++) { + level.inputs[i] = givenInputs[i]; + } + 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; + } +} diff --git a/road.js b/road.js new file mode 100644 index 0000000..94de08d --- /dev/null +++ b/road.js @@ -0,0 +1,59 @@ +class Road { + constructor(x, width, laneCount = 5) { + this.x = x; + this.width = width; + this.laneCount = laneCount; + + this.left = x - width / 2; + this.right = x + width / 2; + + const infinity = 1000000; + this.top = -infinity; + this.bottom = infinity; + + const topLeft = { x: this.left, y: this.top }; + const topRight = { x: this.right, y: this.top }; + const bottomLeft = { x: this.left, y: this.bottom }; + const bottomRight = { x: this.right, y: this.bottom }; + this.borders = [ + [topLeft, bottomLeft], + [topRight, bottomRight], + ]; + } + + getLaneCenter(laneIndex) { + const laneWidth = this.width / this.laneCount; + return ( + this.left + + laneWidth / 2 + + Math.min(laneIndex, this.laneCount - 1) * laneWidth + ); + } + + draw(ctx) { + ctx.lineWidth = 5; + ctx.strokeStyle = "white"; + + for (let i = 1; i <= this.laneCount - 1; i++) { + const x = lerp( + this.left, + this.right, + i / this.laneCount + ); + + ctx.setLineDash([20, 20]); + ctx.beginPath(); + ctx.moveTo(x, this.top); + ctx.lineTo(x, this.bottom); + ctx.stroke(); + } + + ctx.setLineDash([]); + this.borders.forEach((border) => { + ctx.beginPath(); + ctx.moveTo(border[0].x, border[0].y); + ctx.lineTo(border[1].x, border[1].y); + ctx.stroke(); + }); + } +} diff --git a/sensor.js b/sensor.js new file mode 100644 index 0000000..98ee650 --- /dev/null +++ b/sensor.js @@ -0,0 +1,95 @@ +class Sensor { + constructor(car) { + this.car = car; + this.rayCount = 5; + this.rayLength = 150; + this.raySpread = Math.PI / 2; + + this.rays = []; + this.readings = []; + } + update(roadBorders, traffic) { + this.#castRays(); + this.readings = []; + for (let i = 0; i < this.rays.length; i++) { + this.readings.push( + this.#getReading( + this.rays[i], + roadBorders, + traffic + ) + ); + } + } + #getReading(ray, roadBorders, traffic) { + let touches = []; + for (let i = 0; i < roadBorders.length; i++) { + const touch = getIntersection( + ray[0], + ray[1], + roadBorders[i][0], + roadBorders[i][1] + ); + if (touch) touches.push(touch); + } + for (let i = 0; i < traffic.length; i++) { + const poly = traffic[i].polygon; + for (let j = 0; j < poly.length; j++) { + const value = getIntersection( + ray[0], + ray[1], + poly[j], + poly[(j + 1) % poly.length] + ); + if (value) touches.push(value); + } + } + if (touches.length == 0) return null; + const offsets = touches.map((e) => e.offset); + const minOffset = Math.min(...offsets); + return touches.find((e) => e.offset == minOffset); + } + #castRays() { + this.rays = []; + for (let i = 0; i < this.rayCount; i++) { + const rayAngle = + lerp( + this.raySpread / 2, + -this.raySpread / 2, + this.rayCount == 1 + ? 0.5 + : i / (this.rayCount - 1) + ) + this.car.angle; + const start = { x: this.car.x, y: this.car.y }; + const end = { + x: + this.car.x - + Math.sin(rayAngle) * this.rayLength, + y: + this.car.y - + Math.cos(rayAngle) * this.rayLength, + }; + this.rays.push([start, end]); + } + } + draw(ctx) { + for (let i = 0; i < this.rayCount; i++) { + let end = this.rays[i][1]; + if (this.readings[i]) end = this.readings[i]; + + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.strokeStyle = "yellow"; + ctx.moveTo(this.rays[i][0].x, this.rays[i][0].y); + ctx.lineTo(end.x, end.y); + ctx.stroke(); + + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.strokeStyle = "black"; + ctx.moveTo(this.rays[i][1].x, this.rays[i][1].y); + ctx.lineTo(end.x, end.y); + ctx.stroke(); + } + } +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..154ca32 --- /dev/null +++ b/style.css @@ -0,0 +1,28 @@ +body { + margin: 0; + background: darkgray; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; +} +#verticalButtons { + display: flex; + flex-direction: column; +} +button { + border: none; + border-radius: 5px; + margin: 2px; + padding: 5px 5px 7px 5px; + cursor: pointer; +} +button:hover { + background: blue; +} +#carCanvas { + background: lightgray; +} +#networkCanvas { + background: black; +} diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..22ddca6 --- /dev/null +++ b/utils.js @@ -0,0 +1,31 @@ +function lerp(A, B, t) { + return A + (B - A) * t; +} +function getIntersection(A, B, C, D) { + const tTop = (D.x - C.x) * (A.y - C.y) - (D.y - C.y) * (A.x - C.x); + const uTop = (C.y - A.y) * (A.x - B.x) - (C.x - A.x) * (A.y - B.y); + const bottom = (D.y - C.y) * (B.x - A.x) - (D.x - C.x) * (B.y - A.y); + if (bottom != 0) { + const t = tTop / bottom; + const u = uTop / bottom; + if (t >= 0 && t <= 1 && u >= 0 && u <= 1) { + return { + x: lerp(A.x, B.x, t), + y: lerp(A.y, B.y, t), + offset: t, + }; + } + } + return null; +} +function getRGBA(value) { + const alpha = Math.abs(value); + const R = value < 0 ? 0 : 255; + const G = R; + const B = value > 0 ? 0 : 255; + return "rgba(" + R + "," + G + "," + B + "," + alpha + ")"; +} +function getRandomColor() { + const hue = 290 + Math.random() * 260; + return "hsl(" + hue + ",100%, 60%)"; +} diff --git a/visualizer.js b/visualizer.js new file mode 100644 index 0000000..9d359cf --- /dev/null +++ b/visualizer.js @@ -0,0 +1,110 @@ +class Visualizer{ + static drawNetwork(ctx,network){ + const margin=50; + const left=margin; + const top=margin; + const width=ctx.canvas.width-margin*2; + const height=ctx.canvas.height-margin*2; + + const levelHeight=height/network.levels.length; + + for(let i=network.levels.length-1;i>=0;i--){ + const levelTop=top+ + lerp( + height-levelHeight, + 0, + network.levels.length==1 + ?0.5 + :i/(network.levels.length-1) + ); + + ctx.setLineDash([7,3]); + Visualizer.drawLevel(ctx,network.levels[i], + left,levelTop, + width,levelHeight, + i==network.levels.length-1 + ?['🠉','🠈','🠊','🠋'] + :[] + ); + } + } + + static drawLevel(ctx,level,left,top,width,height,outputLabels){ + const right=left+width; + const bottom=top+height; + + const {inputs,outputs,weights,biases}=level; + + for(let i=0;i