Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 24 additions & 20 deletions classes/Car.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,23 @@ import { Ray } from './Ray.js';
import { NeuralNetwork } from './NeuralNetwork.js';

export class Car {
constructor(x, width, height, canvasWidth, speed, generation = 1) {
constructor(x, y, width, height, canvasWidth, canvasHeight, speed, generation = 1) {
this.x = x;
this.y = 800 * 0.8; // Will be set in draw() based on canvas height
this.y = y;
// Width and height of the car
this.width = width;
this.height = height;

this.canvasWidth = canvasWidth;

// Penalty multiplier applied when the car strays from the road center
this.centerPenaltyMultiplier = 0.1; // Math.random() * 0.5 + 0.5;
this.canvasHeight = canvasHeight;
// Penalty multiplier when the forward ray detects a nearby obstacle
this.proximityPenaltyMultiplier = 0.8;

// Current angle of the car, affects how quickly the car shifts left or right
this.angle = 0;
// Maximum angle change per update
this.maxSteer = 0.02;
// Affects how quickly the car shifts left or right along with angle
// Affects how quickly the car moves along with angle
this.speed = speed;
this.generation = generation;
// Whether the car has hit an obstacle or not
Expand Down Expand Up @@ -102,20 +100,26 @@ export class Car {
// 4. Predict steering using NN
const steerDelta = this.brain.predict(inputs) * this.maxSteer; // output [-maxSteer, maxSteer]

// 5. Update car angle and position, with clamping
const newAngle = this.angle + steerDelta;
// Clamp angle between -π/2 and π/2 (90 degrees)
this.angle = Math.max(-Math.PI/2, Math.min(Math.PI/2, newAngle));
this.x += Math.sin(this.angle) * this.speed;
// 5. Update car angle and position
this.angle += steerDelta;
if (this.angle > Math.PI) this.angle -= 2 * Math.PI;
if (this.angle < -Math.PI) this.angle += 2 * Math.PI;

// 6. Update fitness (distance traveled) and apply penalty for
// drifting away from the road center
this.fitness += this.speed;
this.x += Math.sin(this.angle) * this.speed;
this.y -= Math.cos(this.angle) * this.speed;

// Keep within bounds
if (
this.x - this.width / 2 < 0 ||
this.x + this.width / 2 > this.canvasWidth ||
this.y - this.height / 2 < 0 ||
this.y + this.height / 2 > this.canvasHeight
) {
this.alive = false;
}

const centerX = this.canvasWidth / 2;
const distanceFromCenter = Math.abs(this.x - centerX);
const normalized = distanceFromCenter / centerX; // 0 at center, 1 at edge
this.fitness -= normalized * this.centerPenaltyMultiplier * this.speed;
// 6. Update fitness (time survived)
this.fitness += 1;

// Penalize if the forward-facing ray detects an obstacle too close
const middleIndex = Math.floor(this.rays.length / 2);
Expand All @@ -135,8 +139,6 @@ export class Car {
draw(ctx) {
if (!this.alive) return;

this.y = ctx.canvas.height * 0.8; // Update the stored y position

// Draw rays first (in world space, before car transformations)
for (const ray of this.rays) {
ray.draw(ctx);
Expand Down Expand Up @@ -195,9 +197,11 @@ export class Car {
clone() {
const clone = new Car(
this.canvasWidth / 2,
this.canvasHeight / 2,
this.width,
this.height,
this.canvasWidth,
this.canvasHeight,
this.speed,
this.generation
);
Expand Down
10 changes: 6 additions & 4 deletions classes/Obstacle.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
export class Obstacle {
constructor(x, y, width, height, speed) {
constructor(x, y, width, height, vx = 0, vy = 0) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.speed = speed; // downward speed
this.vx = vx;
this.vy = vy;
}

update() {
this.y += this.speed;
this.x += this.vx;
this.y += this.vy;
}

draw(ctx) {
Expand Down
53 changes: 14 additions & 39 deletions helpers/ObstacleManager.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,38 @@
import { Obstacle } from "../classes/Obstacle.js";

export class ObstacleManager {
constructor(canvas, obstacleCount = 5, speed) {
constructor(canvas, obstacleCount = 5) {
this.canvas = canvas;
this.count = obstacleCount;
this.speed = speed;
// Use square obstacles inside the arena
this.width = 40;
this.height = 15;
this.height = 40;
this.obstacles = [];

this.initializeObstacles();
}

initializeObstacles() {
this.obstacles = [];
// Add moving obstacles

for (let i = 0; i < this.count; i++) {
const x = this.getRandomX();
const y = -i * 200; // Space them out vertically above the screen
this.obstacles.push(new Obstacle(x, y, this.width, this.height, this.speed));
const x = Math.random() * (this.canvas.width - this.width) + this.width / 2;
const y = Math.random() * (this.canvas.height - this.height) + this.height / 2;
this.obstacles.push(new Obstacle(x, y, this.width, this.height));
}

// Manually add sides of the road as obstacles
this.obstacles.push(new Obstacle(0, this.canvas.height / 2, 10, this.canvas.height, 0)); // left wall
this.obstacles.push(new Obstacle(this.canvas.width, this.canvas.height / 2, 10, this.canvas.height, 0)); // right wall
// Arena walls
this.obstacles.push(new Obstacle(this.canvas.width / 2, 5, this.canvas.width, 10)); // top
this.obstacles.push(new Obstacle(this.canvas.width / 2, this.canvas.height - 5, this.canvas.width, 10)); // bottom
this.obstacles.push(new Obstacle(5, this.canvas.height / 2, 10, this.canvas.height)); // left
this.obstacles.push(new Obstacle(this.canvas.width - 5, this.canvas.height / 2, 10, this.canvas.height)); // right
}

getRandomX() {
// Allow obstacles to spawn partially outside the road bounds so that
// "side hugging" cars cannot exploit a gap next to the walls. An
// obstacle's center can be anywhere from -width/2 to
// canvas.width + width/2 which means it may overlap the wall by up to
// half of its width but will never be completely outside the canvas.
const minX = -this.width / 2;
const maxX = this.canvas.width + this.width / 2;
return minX + Math.random() * (maxX - minX);
}

updateAll(cars) {
for (const obs of this.obstacles) {
obs.update();

if (obs.y - obs.height / 2 > this.canvas.height) {
obs.y = -this.height;
obs.x = this.getRandomX();
}

for (const car of cars) {
if (car.alive && obs.collidesWith(car)) {
car.alive = false;
Expand All @@ -64,20 +51,8 @@ export class ObstacleManager {
return this.obstacles;
}

// Reset all moving obstacles to starting positions
// Reset obstacles for new generation
reset() {
// Keep the last two obstacles (walls) and reset the moving ones
const walls = this.obstacles.slice(-2);
this.obstacles = [];

// Reinitialize moving obstacles
for (let i = 0; i < this.count; i++) {
const x = this.getRandomX();
const y = -i * 200; // Space them out vertically above the screen
this.obstacles.push(new Obstacle(x, y, this.width, this.height, this.speed));
}

// Add back the walls
this.obstacles.push(...walls);
this.initializeObstacles();
}
}
4 changes: 3 additions & 1 deletion helpers/SimulationManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ export class SimulationManager {
for (let i = 0; i < this.populationSize; i++) {
this.cars.push(new Car(
this.canvas.width / 2, // x
this.canvas.height / 2, // y
this.CAR_WIDTH, // width
this.CAR_HEIGHT, // height
this.canvas.width, // canvasWidth
this.canvas.height, // canvasHeight
this.speed, // speed
this.generation // generation
));
Expand Down Expand Up @@ -68,7 +70,7 @@ export class SimulationManager {

// Increment overall distance if at least one car is alive
if (this.cars.some(car => car.alive)) {
this.distanceTraveled += this.speed;
this.distanceTraveled += 1;
}

// Check if all cars are dead
Expand Down
17 changes: 4 additions & 13 deletions main.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,23 @@
import { SimulationManager } from './helpers/SimulationManager.js';
import { StatisticsManager } from './helpers/StatisticsManager.js';
import {
initRoadLines,
updateRoadLines,
drawRoadLines,
} from './helpers/roadLines.js';

import { ObstacleManager } from './helpers/ObstacleManager.js';

// Get canvas and context
const canvas = document.getElementById("canvas");
canvas.width = 600;
// Make the arena a slightly larger square
canvas.width = 800;
canvas.height = 800;
const ctx = canvas.getContext("2d");

// Initialize road lines
initRoadLines(canvas.height);

// Define constants
const SPEED = 1;
const NUM_OBSTACLES = 10;
const POPULATION_SIZE = 100;
const PARENT_COUNT = 10;
const MUTATION_RATE = 0.2;

const simManager = new SimulationManager(canvas, new ObstacleManager(canvas, NUM_OBSTACLES, SPEED),
const simManager = new SimulationManager(canvas, new ObstacleManager(canvas, NUM_OBSTACLES),
POPULATION_SIZE, PARENT_COUNT, SPEED, MUTATION_RATE);

// Initialize statistics manager
Expand All @@ -33,9 +27,6 @@ const statsManager = new StatisticsManager(simManager);
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);

updateRoadLines(SPEED, canvas.height);
drawRoadLines(ctx, canvas.width);

simManager.update();
simManager.draw(ctx);

Expand Down