commit f79d283c9674100e375cee5e8e5d1d36739a342e Author: QkoSad Date: Mon Nov 7 19:26:26 2022 +0200 self driving car 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 0000000..d969908 Binary files /dev/null and b/car.png differ 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