├── .gitignore ├── README.md ├── car.js ├── controls.js ├── index.html ├── main.js ├── network.js ├── road.js ├── sensor.js ├── settings.js ├── style.css ├── utils.js └── visualizer.js /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS system files 2 | .DS_Store 3 | 4 | # Node modules (if used in the future) 5 | node_modules/ 6 | npm-debug.log 7 | 8 | # VS Code settings 9 | .vscode/ 10 | 11 | # Logs 12 | logs/ 13 | *.log 14 | 15 | # Temporary files 16 | *.tmp 17 | *.swp 18 | 19 | # Build files (if any) 20 | build/ 21 | dist/ 22 | 23 | # Git ignore for local environment 24 | .env 25 | 26 | # Cache files 27 | .cache/ 28 | 29 | # Python virtual environment (if used) 30 | venv/ 31 | __pycache__/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Self-Driving Car Simulation (No Libraries) 2 | 3 | This project is a simple 2D simulation of a self-driving car environment, built purely with vanilla JavaScript, HTML, and CSS, without relying on any external game or physics libraries. 4 | 5 | ## 🌐 Live Demo 6 | 7 | Try the simulation here: [Self-Driving Car Demo](https://self-driving-car-vert.vercel.app/) 8 | 9 | ## 🚀 Project Status (Latest Update) 10 | 11 | * **Layout:** Three-column (Settings | Simulation | Network) centered layout. 12 | * **Settings Panel:** Improved UI with sliders for Car Count, Max Speed, Mutation Rate. Settings saved to `localStorage`, require reload (Space/Discard) to apply. 13 | * **Cars:** Stylized car shapes with body, roof, headlights, and taillights. 14 | * **Traffic:** Fixed, looping traffic patterns based on difficulty. 15 | * **AI:** Neural network with visualizer showing real-time decision making. 16 | * **Controls:** 17 | * Difficulty change reloads the page. 18 | * Spacebar reloads the page. 19 | * Save/Discard buttons manage the best car's brain in `localStorage`. 20 | 21 | ## Project Goal 22 | 23 | The main goal is to simulate cars that can perceive their environment and learn to navigate a multi-lane road with traffic using a simple neural network and evolutionary concepts. 24 | 25 | ## Current Features (Detailed - Reflecting Latest Status) 26 | 27 | * **Layout:** Three-column layout featuring: 28 | * **Settings Panel (Left):** Improved UI allows adjusting simulation parameters via interactive sliders (Car Count, Max Speed, Mutation Rate). Settings are saved to `localStorage` and applied on reload. 29 | * **Simulation Canvas (Center):** Displays the main car simulation, road, and traffic. 30 | * **Network Canvas (Right):** Visualizes the neural network of the best-performing car. 31 | * **Controls Bar (Bottom):** Contains buttons for saving/discarding the best brain and a dropdown for selecting difficulty. 32 | * **AI Cars:** 33 | * Number adjustable via Settings Panel. 34 | * Max speed adjustable via Settings Panel. 35 | * Controlled by a simple Neural Network. 36 | * Use sensors (ray casting) to perceive road borders and traffic. 37 | * Apply basic rules (avoid edges, brake for obstacles). 38 | * Blue, stylized car shape with lights and roof. 39 | * Sensors are only visually rendered for the current best car. 40 | * **Fixed Traffic:** 41 | * Uses predefined, looping patterns based on selected difficulty (1-5). 42 | * Cars are red, stylized shapes with lights. 43 | * **Neural Network:** 44 | * Simple feedforward network (`network.js`). 45 | * Visualized in real-time (`visualizer.js`). 46 | * Mutation rate adjustable via Settings Panel (`localStorage`). 47 | * **Persistence & Control:** 48 | * **Save:** Saves the brain of the best car to `localStorage`. 49 | * **Discard:** Removes the saved brain from `localStorage` and reloads the page (starts with random brains). 50 | * **Difficulty Selector:** Changing difficulty saves the selection and reloads the page. The simulation restarts with the new difficulty, using the saved brain if available. 51 | * **Spacebar:** Reloads the page, maintaining the current difficulty and loading the saved brain if available. 52 | * **Settings Panel:** Changes made here are saved to `localStorage` immediately but require a reload (Spacebar or Discard button) to take effect in the simulation. 53 | 54 | ## File Structure 55 | 56 | * `index.html`: Sets up the three canvases (`settingsCanvas`, `carCanvas`, `networkCanvas`), the controls bar, and includes all JavaScript files. 57 | * `style.css`: Styles the page layout (3-column flexbox), canvases, settings panel elements (drawn via JS), and controls bar. 58 | * `main.js`: Initializes settings and simulation, creates the car population based on settings, handles the main animation loop (updating, drawing), manages `localStorage` for difficulty and brain, and handles button/keyboard events. 59 | * `settings.js`: Defines the `Settings` class, responsible for drawing the settings panel UI (sliders), handling user interaction with settings, and saving/loading settings values to/from `localStorage`. 60 | * `car.js`: Defines the `Car` class (handles `AI` and `DUMMY` types), movement logic, AI rule application, collision detection (using a rectangular polygon), and drawing (stylized car shape with roof). 61 | * `network.js`: Defines the `Level` and `NeuralNetwork` classes, including feedforward and mutation logic. 62 | * `visualizer.js`: Contains the `Visualizer` class with static methods to draw the neural network. 63 | * `road.js`: Defines the `Road` class for geometry and drawing. 64 | * `sensor.js`: Defines the `Sensor` class for ray casting. 65 | * `controls.js`: Defines the `Controls` class (primarily stores AI decisions). 66 | * `utils.js`: Contains utility functions like `lerp`, `polysIntersect`. 67 | 68 | ## How to Run 69 | 70 | 1. Clone or download the repository: 71 | ``` 72 | git clone https://github.com/lburakakca/Self-driving-car.git 73 | ``` 74 | 2. Open the `index.html` file in a web browser. 75 | 3. Observe the simulation: Settings on the left, car simulation in the center, network visualization on the right. 76 | 4. Use the controls: 77 | * **Settings Panel:** Adjust sliders (requires reload/discard to apply). 78 | * **Difficulty Dropdown:** Select difficulty (reloads page). 79 | * `Save`: Saves the current best car's network. 80 | * `Discard`: Clears the saved brain and reloads. 81 | * `Spacebar`: Reloads the simulation (uses saved brain/settings if available). 82 | 83 | ## Next Steps (Potential) 84 | 85 | * Implement a proper genetic algorithm (selection, crossover) beyond just mutation. 86 | * Improve the fitness function. 87 | * Refine AI rules or network structure. 88 | * Add more adjustable settings (sensor range, angle, etc.). 89 | * Optimize performance. 90 | 91 | ## 🤝 Contributing 92 | 93 | Feel free to contribute to this project by submitting issues or pull requests. All contributions are welcome! 94 | 95 | ## 📜 License 96 | 97 | This project is open source and available under the MIT License. -------------------------------------------------------------------------------- /car.js: -------------------------------------------------------------------------------- 1 | class Car{ 2 | constructor(x,y,width,height, controlType="PLAYER", maxSpeed=4){ 3 | this.x=x; 4 | this.y=y; 5 | this.width=width; 6 | this.height=height; 7 | 8 | this.speed=0; 9 | this.acceleration=0.25; 10 | this.maxSpeed= (controlType === "DUMMY") ? maxSpeed * 0.6 : maxSpeed; 11 | this.friction=0.1; 12 | this.handling=0.04; 13 | this.angle=0; 14 | this.damaged=false; 15 | this.controlType = controlType; 16 | 17 | this.useBrain = (controlType == "AI"); 18 | 19 | this.brakeForce=0.15; 20 | 21 | if(controlType !== "DUMMY"){ 22 | this.sensor=new Sensor(this); 23 | this.brain= new NeuralNetwork( 24 | [this.sensor.rayCount, 6, 4] 25 | ); 26 | } 27 | this.controls=new Controls(controlType); 28 | } 29 | 30 | update(roadBorders, traffic){ 31 | if(!this.damaged){ 32 | if (this.controlType === "DUMMY") { 33 | this.#moveDummy(); 34 | } else { 35 | if(this.useBrain && this.sensor && this.brain){ 36 | // 1. Get sensor readings (inputs for the network) 37 | const sensorOffsets = this.sensor.readings.map( 38 | s => s==null?0:1-s.offset // Closer = higher value (0 to 1) 39 | ); 40 | 41 | // 2. Get raw outputs (recommendations) from the neural network 42 | const outputs = NeuralNetwork.feedForward(sensorOffsets, this.brain); 43 | console.log("Raw AI Outputs:", outputs); 44 | 45 | // 3. Apply rules based on sensor data and road position 46 | const currentLaneIndex = road.getLaneIndex(this.x); 47 | const laneWidth = road.laneWidth; 48 | const frontSensorThreshold = 0.7; // How close an object needs to be to react (1-offset) 49 | let obstacleInLane = false; 50 | 51 | // Check sensors for obstacles directly in front (within the same lane) 52 | for(let i=0; i < this.sensor.rayCount; i++){ 53 | const reading = this.sensor.readings[i]; 54 | if(reading && reading.offset < (1 - frontSensorThreshold)) { // If object is close 55 | // Calculate the x-coordinate of the detected point 56 | const angle = this.sensor.rays[i][1].angle + this.angle; // Consider car's angle 57 | const touchX = this.x - Math.sin(angle) * reading.offset * this.sensor.rayLength; // Approximate touch X 58 | const obstacleLaneIndex = road.getLaneIndex(touchX); 59 | 60 | if(obstacleLaneIndex === currentLaneIndex){ 61 | obstacleInLane = true; 62 | console.log(`Obstacle detected in lane ${currentLaneIndex} by sensor ${i}`); 63 | break; // Found an obstacle in our lane, no need to check further 64 | } 65 | } 66 | } 67 | 68 | // --- Apply Control Rules --- 69 | // Default: try to go forward, no turning/reversing 70 | this.controls.forward = true; 71 | this.controls.left = false; 72 | this.controls.right = false; 73 | this.controls.reverse = false; 74 | 75 | // Rule 1: Obstacle in lane? -> Don't go forward, maybe reverse/brake 76 | if(obstacleInLane){ 77 | this.controls.forward = false; 78 | // Optional: Engage reverse/brake based on network output or fixed rule 79 | // this.controls.reverse = outputs[3] > 0.5; // Use network's reverse suggestion? 80 | this.controls.reverse = true; // Simple braking action 81 | console.log("Rule Applied: Obstacle -> Brake"); 82 | } 83 | 84 | // Rule 2: Avoid Edges -> Override turn signals if on edge lanes 85 | if(currentLaneIndex === 0){ // On leftmost lane 86 | this.controls.left = false; // Don't allow turning left 87 | if(outputs[1] > 0.5) console.log("Rule Applied: Left Edge -> Ignoring Left Turn"); 88 | } else { 89 | // Allow left turn only if no obstacle and network suggests it 90 | this.controls.left = !obstacleInLane && outputs[1] > 0.5; 91 | } 92 | 93 | if(currentLaneIndex === road.laneCount - 1){ // On rightmost lane 94 | this.controls.right = false; // Don't allow turning right 95 | if(outputs[2] > 0.5) console.log("Rule Applied: Right Edge -> Ignoring Right Turn"); 96 | } else { 97 | // Allow right turn only if no obstacle and network suggests it 98 | this.controls.right = !obstacleInLane && outputs[2] > 0.5; 99 | } 100 | 101 | // Rule 3: Prioritize Forward (already default) 102 | // Only override forward if obstacle detected. 103 | // Turn signals are only active if no obstacle and not on edge lane and network suggests. 104 | 105 | console.log("Final Controls:", JSON.stringify(this.controls)); 106 | 107 | // (Keep the original move call outside the AI block) 108 | } 109 | // Move based on final controls (manual or AI + rules) 110 | this.#move(); 111 | } 112 | this.polygon=this.#createPolygon(); 113 | // Assess damage against borders and traffic (all types except dummy-dummy) 114 | this.damaged=this.#assessDamage(roadBorders, (this.controlType !== "DUMMY") ? traffic : []); 115 | } 116 | if(this.sensor){ 117 | this.sensor.update(roadBorders, traffic); 118 | } 119 | } 120 | 121 | #assessDamage(roadBorders, traffic){ 122 | for(let i=0;i 0){ 164 | this.speed-=this.brakeForce; 165 | }else{ 166 | this.speed-=this.acceleration; 167 | } 168 | } 169 | 170 | if(this.speed>this.maxSpeed){ 171 | this.speed=this.maxSpeed; 172 | } 173 | if(this.speed<-this.maxSpeed/2){ 174 | this.speed=-this.maxSpeed/2; 175 | } 176 | 177 | if(this.speed>0){ 178 | this.speed-=this.friction; 179 | } 180 | if(this.speed<0){ 181 | this.speed+=this.friction; 182 | } 183 | if(Math.abs(this.speed)0?1:-1; 189 | const turnSpeed = this.handling * (0.5 + Math.abs(this.speed/this.maxSpeed)); 190 | if(this.controls.left){ 191 | this.angle+=turnSpeed*flip; 192 | } 193 | if(this.controls.right){ 194 | this.angle-=turnSpeed*flip; 195 | } 196 | } 197 | 198 | this.x-=Math.sin(this.angle)*this.speed; 199 | this.y-=Math.cos(this.angle)*this.speed; 200 | } 201 | 202 | #moveDummy() { 203 | this.speed = this.maxSpeed; 204 | this.x-=Math.sin(this.angle)*this.speed; 205 | this.y-=Math.cos(this.angle)*this.speed; 206 | } 207 | 208 | draw(ctx, drawSensor = true){ 209 | // Polygon'u (çarpışma alanını) çiz - bu orijinal davranış 210 | if(this.damaged){ 211 | ctx.fillStyle="gray"; 212 | } else { 213 | ctx.fillStyle = (this.controlType === "DUMMY") ? "#E74C3C" : "#2E86C1"; 214 | } 215 | 216 | if(this.polygon){ 217 | ctx.beginPath(); 218 | ctx.moveTo(this.polygon[0].x, this.polygon[0].y); 219 | for(let i=1; i { 26 | switch (event.key) { 27 | case "ArrowLeft": 28 | this.left = true; 29 | break; 30 | case "ArrowRight": 31 | this.right = true; 32 | break; 33 | case "ArrowUp": 34 | this.forward = true; 35 | break; 36 | case "ArrowDown": 37 | this.reverse = true; 38 | break; 39 | } 40 | // Optional: console.table(this); 41 | } 42 | 43 | document.onkeyup = (event) => { 44 | switch (event.key) { 45 | case "ArrowLeft": 46 | this.left = false; 47 | break; 48 | case "ArrowRight": 49 | this.right = false; 50 | break; 51 | case "ArrowUp": 52 | this.forward = false; 53 | break; 54 | case "ArrowDown": 55 | this.reverse = false; 56 | break; 57 | } 58 | // Optional: console.table(this); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Self-driving car - No libraries 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const carCanvas=document.getElementById("carCanvas"); 2 | const networkCanvas=document.getElementById("networkCanvas"); 3 | const settingsCanvas = document.getElementById("settingsCanvas"); 4 | const carCtx = carCanvas.getContext("2d"); 5 | const networkCtx = networkCanvas.getContext("2d"); 6 | // settingsCtx is managed within Settings class 7 | 8 | // --- Initialize Settings Panel --- 9 | // Set canvas size before creating Settings instance 10 | settingsCanvas.height = window.innerHeight; // Use full height (assuming CSS sets width) 11 | settingsCanvas.width = 200; // Explicitly set width to match CSS 12 | const settings = new Settings("settingsCanvas"); 13 | // --------------------------------- 14 | 15 | // Game Elements 16 | const road = new Road(carCanvas.width/2, carCanvas.width*0.9); 17 | const N = settings.get('carCount'); // Get initial car count 18 | let cars = []; 19 | let bestCar = null; 20 | let traffic = []; 21 | 22 | // Game State 23 | let animationFrameId = null; 24 | 25 | // UI Elements 26 | // const startScreen = document.getElementById("startScreen"); // Removed 27 | const canvasContainer = document.getElementById("canvasContainer"); 28 | const controlsContainer = document.getElementById("controlsContainer"); 29 | const difficultySelect = document.getElementById("difficultySelect"); 30 | // const startButton = document.getElementById("startButton"); // Removed 31 | const saveButton = document.getElementById("saveButton"); 32 | const discardButton = document.getElementById("discardButton"); 33 | 34 | // --- TRAFFIC PATTERNS (Defined early) --- 35 | const trafficPatterns = { 36 | // Difficulty 1: Very Sparse 37 | 1: { 38 | height: 600, // Estimated total height of this pattern 39 | cars: [ 40 | { lane: 0, yOffset: 0 }, 41 | { lane: 2, yOffset: -300 } 42 | ] 43 | }, 44 | // Difficulty 2: Sparse rows 45 | 2: { 46 | height: 800, 47 | cars: [ 48 | { lane: 1, yOffset: 0 }, 49 | { lane: 3, yOffset: -250 }, 50 | { lane: 0, yOffset: -500 }, 51 | { lane: 2, yOffset: -650 } 52 | ] 53 | }, 54 | // Difficulty 3: Some adjacent 55 | 3: { 56 | height: 1000, 57 | cars: [ 58 | { lane: 0, yOffset: 0 }, 59 | { lane: 1, yOffset: 0 }, // Adjacent 60 | { lane: 3, yOffset: -300 }, 61 | { lane: 1, yOffset: -600 }, 62 | { lane: 2, yOffset: -600 }, // Adjacent 63 | { lane: 0, yOffset: -850 } 64 | ] 65 | }, 66 | // Difficulty 4: Denser, more adjacent 67 | 4: { 68 | height: 1200, 69 | cars: [ 70 | { lane: 1, yOffset: 0 }, 71 | { lane: 2, yOffset: -100 }, // Offset adjacent 72 | { lane: 0, yOffset: -350 }, 73 | { lane: 3, yOffset: -550 }, 74 | { lane: 1, yOffset: -750 }, 75 | { lane: 2, yOffset: -750 }, // Adjacent 76 | { lane: 0, yOffset: -1000 }, 77 | { lane: 3, yOffset: -1100 } // Offset adjacent 78 | ] 79 | }, 80 | // Difficulty 5: Challenging blocks 81 | 5: { 82 | height: 1500, 83 | cars: [ 84 | { lane: 0, yOffset: 0 }, 85 | { lane: 1, yOffset: 0 }, // Block 86 | { lane: 2, yOffset: -250 }, 87 | { lane: 3, yOffset: -250 }, // Block 88 | { lane: 1, yOffset: -550 }, 89 | { lane: 0, yOffset: -750 }, 90 | { lane: 2, yOffset: -950 }, 91 | { lane: 3, yOffset: -950 }, // Block 92 | { lane: 1, yOffset: -1200 }, 93 | { lane: 0, yOffset: -1350 }, 94 | { lane: 2, yOffset: -1450 } // Offset 95 | ] 96 | } 97 | }; 98 | 99 | function getPatternHeight(difficulty) { 100 | return trafficPatterns[difficulty]?.height || 600; // Default height 101 | } 102 | 103 | function loadTrafficPattern(difficulty) { 104 | console.log(`Loading traffic pattern for difficulty ${difficulty}`); 105 | traffic = []; // Clear existing traffic 106 | const pattern = trafficPatterns[difficulty] || trafficPatterns[1]; // Default to 1 if invalid 107 | const startY = bestCar ? bestCar.y - 300 : -200; // Start traffic ahead of the car 108 | 109 | pattern.cars.forEach(carConfig => { 110 | const speed = 2 + (Math.random() - 0.5) * 0.5; // Add slight speed variation 111 | traffic.push(new Car( 112 | road.getLaneCenter(carConfig.lane), 113 | startY + carConfig.yOffset, // Calculate absolute Y 114 | 30, 50, "DUMMY", speed 115 | )); 116 | }); 117 | console.log("Traffic loaded:", traffic.length, "cars"); 118 | } 119 | // ------------------------------------------ 120 | 121 | // --- Initial Setup & Event Listeners --- 122 | 123 | // Load initial difficulty 124 | let currentDifficulty = parseInt(localStorage.getItem("difficulty") || "1"); 125 | difficultySelect.value = currentDifficulty.toString(); 126 | 127 | // Difficulty change listener (triggers reset) 128 | difficultySelect.addEventListener('change', handleDifficultyChange); 129 | 130 | // Start Button Listener Removed 131 | // startButton.addEventListener("click", startGame); 132 | 133 | // In-Game Buttons 134 | saveButton.addEventListener("click", saveBestBrain); 135 | discardButton.addEventListener("click", discardBrainAndReload); 136 | discardButton.title = "Removes the saved brain and reloads."; 137 | // Reset and Menu listeners already removed 138 | 139 | // --- Game Flow Functions (Removed startGame, goToMainMenu) --- 140 | /* 141 | function startGame() { ... } 142 | function goToMainMenu() { ... } 143 | */ 144 | 145 | // Difficulty Change Handler (NOW Reloads Page) 146 | function handleDifficultyChange(event) { 147 | const newDifficulty = parseInt(event.target.value); 148 | console.log(`Difficulty changed to ${newDifficulty}, reloading...`); 149 | // Save the new difficulty to localStorage 150 | localStorage.setItem("difficulty", newDifficulty.toString()); 151 | // Reload the page to apply the new difficulty and potentially load/reset brains 152 | location.reload(); 153 | } 154 | 155 | // Initialize simulation state directly on load 156 | initializeSimulationState(); 157 | 158 | function initializeSimulationState(){ 159 | console.log("Initializing simulation state..."); 160 | cars = generateCars(settings.get('carCount')); 161 | bestCar = cars[0]; 162 | loadBrainIfExists(); 163 | loadTrafficPattern(currentDifficulty); 164 | // Ensure difficulty dropdown reflects state after potential load/reset 165 | difficultySelect.value = currentDifficulty.toString(); 166 | } 167 | 168 | // --- Brain Management --- 169 | 170 | function loadBrainIfExists(){ 171 | if(localStorage.getItem("bestBrain")){ 172 | console.log("Loading brain from localStorage..."); 173 | const savedBrainJSON = localStorage.getItem("bestBrain"); 174 | try { 175 | const savedBrainData = JSON.parse(savedBrainJSON); 176 | for(let i = 0; i < cars.length; i++){ 177 | if (savedBrainData && savedBrainData.levels) { 178 | // Copy weights and biases 179 | if (cars[i].brain && cars[i].brain.levels) { 180 | for(let levelIndex = 0; levelIndex < cars[i].brain.levels.length; levelIndex++){ 181 | if(savedBrainData.levels[levelIndex] && cars[i].brain.levels[levelIndex]){ 182 | cars[i].brain.levels[levelIndex].biases = [...savedBrainData.levels[levelIndex].biases]; 183 | cars[i].brain.levels[levelIndex].weights = savedBrainData.levels[levelIndex].weights.map(row => [...row]); 184 | } 185 | } 186 | } else { 187 | console.warn(`Car ${i} does not have a valid brain structure to load into.`); 188 | } 189 | } else { 190 | console.error("Saved brain data is invalid or missing levels."); 191 | break; 192 | } 193 | // Apply mutation 194 | if(i > 0){ 195 | NeuralNetwork.mutate(cars[i].brain, settings.get('mutationRate')); 196 | } 197 | } 198 | bestCar = cars[0]; 199 | console.log("Brain loaded and applied/mutated to cars."); 200 | } catch (e) { 201 | console.error("Failed to parse or load brain:", e); 202 | localStorage.removeItem("bestBrain"); 203 | console.log("Removed potentially corrupted brain data."); 204 | } 205 | } else { 206 | console.log("No saved brain found. Starting with random brains."); 207 | } 208 | } 209 | 210 | function saveBestBrain(){ 211 | if(!bestCar || !bestCar.brain){ 212 | console.error("Cannot save, no best car or brain found."); 213 | return; 214 | } 215 | console.log("Saving best brain..."); 216 | localStorage.setItem("bestBrain", JSON.stringify(bestCar.brain)); 217 | console.log("Brain saved!"); 218 | // Maybe add visual feedback like button text change? 219 | } 220 | 221 | function discardBrainAndReload(){ 222 | console.log("Discarding saved brain and reloading..."); 223 | localStorage.removeItem("bestBrain"); 224 | location.reload(); 225 | } 226 | 227 | // Reset function removed 228 | /* 229 | function resetSimulationInPlace(){ ... } 230 | */ 231 | 232 | // --- Keyboard Listener for Reload (In-Game) --- 233 | window.addEventListener('keydown', (event) => { 234 | // Now always active, as game starts immediately 235 | if (event.code === 'Space') { 236 | console.log("Space pressed, reloading simulation..."); 237 | location.reload(); 238 | } 239 | }); 240 | // ---------------------------------- 241 | 242 | // --- Car Generation --- 243 | function generateCars(numCars){ 244 | console.log("Generating", numCars, "cars..."); 245 | const generatedCars = []; 246 | for(let i=1; i<=numCars; i++){ 247 | generatedCars.push(new Car(road.getLaneCenter(1), 100, 30, 50, "AI", settings.get('maxSpeed'))); 248 | } 249 | return generatedCars; 250 | } 251 | // -------------------- 252 | 253 | // --- Animation Loop --- 254 | // Start animation loop directly after initial setup 255 | animationFrameId = requestAnimationFrame(animate); 256 | 257 | function animate(time){ 258 | // 1. Update Traffic & Loop 259 | const patternHeight = getPatternHeight(currentDifficulty); // Need function to get pattern height 260 | const loopThreshold = bestCar.y + carCanvas.height * 0.5; // Point below the view 261 | 262 | for(let i=0; i loopThreshold) { 266 | // Move car to the top of the pattern 267 | traffic[i].y -= patternHeight; 268 | // Maybe slightly adjust x too, or re-read from pattern? 269 | // For now, just reset y. 270 | } 271 | } 272 | 273 | // 2. Update AI Cars 274 | for(let i=0; i !c.damaged); 280 | bestCar = candidates.length > 0 281 | ? candidates.reduce((best, current) => current.y < best.y ? current : best, candidates[0]) 282 | : cars.reduce((best, current) => current.y < best.y ? current : best, cars[0]); // Fallback if all damaged 283 | 284 | // 4. Manage Dynamic Traffic (REMOVED) 285 | // traffic = traffic.filter(...); 286 | // if (bestCar.y < lastSpawnTriggerY - trafficSpawnThreshold) { ... } 287 | 288 | // 5. Adjust Canvas Sizes 289 | const rowHeight = document.getElementById('canvasRow').clientHeight; 290 | if (settingsCanvas.height !== rowHeight) settingsCanvas.height = rowHeight; 291 | if (carCanvas.height !== rowHeight) carCanvas.height = rowHeight; 292 | if (networkCanvas.height !== rowHeight) networkCanvas.height = rowHeight; 293 | // Widths are mostly handled by CSS, but we might need to update Road if carCanvas width changes 294 | // road.width = carCanvas.width*0.9; // Update road width if canvas resizes - potential performance hit? 295 | 296 | // 6. Draw Canvases 297 | carCtx.clearRect(0, 0, carCanvas.width, carCanvas.height); 298 | networkCtx.clearRect(0, 0, networkCanvas.width, networkCanvas.height); 299 | 300 | carCtx.save(); 301 | carCtx.translate(0, -bestCar.y + carCanvas.height*0.7); 302 | road.draw(carCtx); 303 | for(let i=0; i 0) { // Check if weighted sum + bias is positive 54 | level.outputs[i] = 1; 55 | } else { 56 | level.outputs[i] = 0; 57 | } 58 | } 59 | 60 | return level.outputs; 61 | } 62 | } 63 | 64 | class NeuralNetwork { 65 | constructor(neuronCounts) { // neuronCounts is an array like [inputCount, hiddenCount1, ..., outputCount] 66 | this.levels = []; 67 | // Create levels based on the neuron counts 68 | for (let i = 0; i < neuronCounts.length - 1; i++) { 69 | this.levels.push(new Level( 70 | neuronCounts[i], // Input count for this level 71 | neuronCounts[i + 1] // Output count for this level 72 | )); 73 | } 74 | } 75 | 76 | static feedForward(givenInputs, network) { 77 | // Get initial outputs by feeding inputs to the first level 78 | let outputs = Level.feedForward( 79 | givenInputs, 80 | network.levels[0] 81 | ); 82 | 83 | // Propagate outputs through subsequent levels 84 | for (let i = 1; i < network.levels.length; i++) { 85 | // The output of the previous level is the input for the current level 86 | outputs = Level.feedForward( 87 | outputs, 88 | network.levels[i] 89 | ); 90 | } 91 | 92 | // Return the final outputs from the last level 93 | return outputs; 94 | } 95 | 96 | // Static method to mutate the network's weights and biases 97 | static mutate(network, amount = 1) { // amount: 0=no change, 1=full random change 98 | network.levels.forEach(level => { 99 | // Mutate biases 100 | for (let i = 0; i < level.biases.length; i++) { 101 | // Interpolate bias towards a random value (-1 to 1) 102 | level.biases[i] = Visualizer.lerp( // Using Visualizer.lerp, assumes it's available globally or passed 103 | level.biases[i], 104 | Math.random() * 2 - 1, 105 | amount 106 | ); 107 | } 108 | // Mutate weights 109 | for (let i = 0; i < level.weights.length; i++) { 110 | for (let j = 0; j < level.weights[i].length; j++) { 111 | // Interpolate weight towards a random value (-1 to 1) 112 | level.weights[i][j] = Visualizer.lerp( 113 | level.weights[i][j], 114 | Math.random() * 2 - 1, 115 | amount 116 | ); 117 | } 118 | } 119 | }); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /road.js: -------------------------------------------------------------------------------- 1 | class Road { 2 | constructor(x, width, laneCount = 4) { 3 | this.x = x; 4 | this.width = width; 5 | this.laneCount = laneCount; 6 | 7 | this.left = x - width / 2; 8 | this.right = x + width / 2; 9 | 10 | const infinity = 1000000; 11 | this.top = -infinity; 12 | this.bottom = infinity; 13 | 14 | // Define border corner points 15 | const topLeft = { x: this.left, y: this.top }; 16 | const topRight = { x: this.right, y: this.top }; 17 | const bottomLeft = { x: this.left, y: this.bottom }; 18 | const bottomRight = { x: this.right, y: this.bottom }; 19 | 20 | // Store borders as an array of two line segments 21 | this.borders = [ 22 | [topLeft, bottomLeft], // Left border segment 23 | [topRight, bottomRight] // Right border segment 24 | ]; 25 | 26 | // Lane marking properties 27 | this.laneWidth = this.width / this.laneCount; 28 | this.dashLength = 30; // Length of each dash 29 | this.dashGap = 20; // Gap between dashes 30 | } 31 | 32 | getLaneCenter(laneIndex){ 33 | const laneWidth = this.width/this.laneCount; 34 | return this.left + laneWidth/2 + 35 | Math.min(laneIndex, this.laneCount-1)*laneWidth; 36 | } 37 | 38 | // New method to get lane index from x-coordinate 39 | getLaneIndex(x) { 40 | // Calculate the lane index based on x position relative to the left edge and lane width 41 | // Ensure the index is within the valid range [0, laneCount - 1] 42 | const index = Math.floor((x - this.left) / this.laneWidth); 43 | return Math.max(0, Math.min(index, this.laneCount - 1)); 44 | } 45 | 46 | draw(ctx) { 47 | ctx.lineWidth = 2; 48 | ctx.strokeStyle = "#FFFFFF"; 49 | 50 | // Draw all lane markings 51 | for(let i = 1; i <= this.laneCount-1; i++){ 52 | const x = lerp( 53 | this.left, 54 | this.right, 55 | i/this.laneCount 56 | ); 57 | 58 | ctx.setLineDash([this.dashLength, this.dashGap]); 59 | ctx.beginPath(); 60 | ctx.moveTo(x, this.top); 61 | ctx.lineTo(x, this.bottom); 62 | ctx.stroke(); 63 | } 64 | 65 | // Draw road borders 66 | ctx.setLineDash([]); 67 | ctx.beginPath(); 68 | ctx.moveTo(this.left, this.top); 69 | ctx.lineTo(this.left, this.bottom); 70 | ctx.stroke(); 71 | 72 | ctx.beginPath(); 73 | ctx.moveTo(this.right, this.top); 74 | ctx.lineTo(this.right, this.bottom); 75 | ctx.stroke(); 76 | } 77 | } 78 | 79 | // Linear interpolation helper function 80 | function lerp(A,B,t){ 81 | return A+(B-A)*t; 82 | } -------------------------------------------------------------------------------- /sensor.js: -------------------------------------------------------------------------------- 1 | class Sensor{ 2 | constructor(car){ 3 | this.car=car; 4 | this.rayCount=5; 5 | this.rayLength=150; 6 | this.raySpread=Math.PI/2; // 45 degrees 7 | 8 | this.rays=[]; 9 | this.readings=[]; 10 | } 11 | 12 | update(roadBorders, traffic=[]){ 13 | this.#castRays(); 14 | this.readings=[]; 15 | for(let i=0;ie.offset); 62 | const minOffset=Math.min(...offsets); 63 | return touches.find(e=>e.offset==minOffset); 64 | } 65 | } 66 | 67 | #castRays(){ 68 | this.rays=[]; 69 | for(let i=0;i=0 && t<=1 && u>=0 && u<=1){ 137 | return { 138 | x:lerp(A.x,B.x,t), 139 | y:lerp(A.y,B.y,t), 140 | offset:t 141 | } 142 | } 143 | } 144 | 145 | return null; 146 | } -------------------------------------------------------------------------------- /settings.js: -------------------------------------------------------------------------------- 1 | class Settings { 2 | constructor(canvasId) { 3 | this.canvas = document.getElementById(canvasId); 4 | this.ctx = this.canvas.getContext("2d"); 5 | this.width = this.canvas.width; 6 | this.height = this.canvas.height; 7 | 8 | // --- Default Settings --- 9 | // Load from localStorage or use defaults 10 | this.settings = { 11 | carCount: parseInt(localStorage.getItem('setting_carCount') || '100'), 12 | maxSpeed: parseFloat(localStorage.getItem('setting_maxSpeed') || '4'), 13 | mutationRate: parseFloat(localStorage.getItem('setting_mutationRate') || '0.1'), 14 | // Add more settings as needed (e.g., sensor length, spread) 15 | }; 16 | 17 | // --- UI Elements (Sliders/Buttons drawn on canvas) --- 18 | this.uiElements = []; 19 | this.activeElement = null; // For dragging sliders 20 | this.mouse = { x: 0, y: 0, down: false }; 21 | 22 | this.#createUI(); 23 | this.#addEventListeners(); 24 | this.draw(); // Initial draw 25 | } 26 | 27 | get(key) { 28 | return this.settings[key]; 29 | } 30 | 31 | #createUI() { 32 | const margin = 20; 33 | let currentY = 30; 34 | const labelWidth = 100; 35 | const sliderWidth = this.width - labelWidth - margin * 2; 36 | const sliderHeight = 10; 37 | 38 | // Car Count Slider 39 | this.uiElements.push({ 40 | type: 'slider', id: 'carCount', label: 'Car Count:', 41 | x: margin + labelWidth, y: currentY + sliderHeight / 2, 42 | width: sliderWidth, height: sliderHeight, 43 | min: 1, max: 500, step: 1 44 | }); 45 | currentY += 40; 46 | 47 | // Max Speed Slider 48 | this.uiElements.push({ 49 | type: 'slider', id: 'maxSpeed', label: 'Max Speed:', 50 | x: margin + labelWidth, y: currentY + sliderHeight / 2, 51 | width: sliderWidth, height: sliderHeight, 52 | min: 1, max: 10, step: 0.1 53 | }); 54 | currentY += 40; 55 | 56 | // Mutation Rate Slider 57 | this.uiElements.push({ 58 | type: 'slider', id: 'mutationRate', label: 'Mutation:', 59 | x: margin + labelWidth, y: currentY + sliderHeight / 2, 60 | width: sliderWidth, height: sliderHeight, 61 | min: 0, max: 1, step: 0.01 62 | }); 63 | currentY += 40; 64 | 65 | // Add Apply Button? 66 | // For now, changes apply immediately and require manual reload/reset 67 | } 68 | 69 | #addEventListeners() { 70 | this.canvas.addEventListener('mousedown', (e) => { 71 | this.mouse.down = true; 72 | this.#updateMousePos(e); 73 | 74 | // Tüm potansiyel hit alanlarını loglayalım 75 | const hitResults = this.#getHitTestInfo(this.mouse.x, this.mouse.y); 76 | console.log("Hit Test Results:", hitResults); 77 | 78 | this.activeElement = this.#getElementAtPos(this.mouse.x, this.mouse.y); 79 | console.log("Mousedown:", this.mouse, "Active Element:", this.activeElement?.id); 80 | 81 | if (this.activeElement && this.activeElement.type === 'slider') { 82 | this.#updateSliderValue(this.activeElement, this.mouse.x); 83 | } 84 | }); 85 | 86 | this.canvas.addEventListener('mouseup', (e) => { 87 | if(this.mouse.down) { 88 | console.log("Mouseup"); 89 | } 90 | this.mouse.down = false; 91 | this.activeElement = null; 92 | }); 93 | 94 | this.canvas.addEventListener('mousemove', (e) => { 95 | this.#updateMousePos(e); 96 | if (this.mouse.down && this.activeElement && this.activeElement.type === 'slider') { 97 | this.#updateSliderValue(this.activeElement, this.mouse.x); 98 | } 99 | }); 100 | 101 | this.canvas.addEventListener('mouseleave', (e) => { 102 | // Opsiyonel: Mouse canvas'tan çıkarsa sürüklemeyi durdur 103 | this.mouse.down = false; 104 | this.activeElement = null; 105 | }); 106 | } 107 | 108 | #updateMousePos(event) { 109 | const rect = this.canvas.getBoundingClientRect(); 110 | this.mouse.x = event.clientX - rect.left; 111 | this.mouse.y = event.clientY - rect.top; 112 | 113 | // DPI ölçekleme için düzeltme yapabiliriz 114 | const cssWidth = this.canvas.clientWidth; 115 | const cssHeight = this.canvas.clientHeight; 116 | const canvasWidth = this.canvas.width; 117 | const canvasHeight = this.canvas.height; 118 | 119 | // Canvas piksel ile CSS piksel arasındaki dönüşüm 120 | this.mouse.x = (this.mouse.x / cssWidth) * canvasWidth; 121 | this.mouse.y = (this.mouse.y / cssHeight) * canvasHeight; 122 | } 123 | 124 | // Yeni yardımcı metod - tüm slider'ların hit alanlarını kontrol eder 125 | #getHitTestInfo(x, y) { 126 | const results = []; 127 | 128 | for (let i = 0; i < this.uiElements.length; i++) { 129 | const el = this.uiElements[i]; 130 | if (el.type === 'slider') { 131 | const value = this.settings[el.id]; 132 | const ratio = Math.max(0, Math.min(1, (value - el.min) / (el.max - el.min))); 133 | const handleX = el.x + ratio * el.width; 134 | const sliderY = el.y; 135 | 136 | const handleRadius = 20; 137 | const handleDist = Math.hypot(x - handleX, y - sliderY); 138 | const onSlider = x >= el.x && x <= el.x + el.width && Math.abs(y - sliderY) <= handleRadius; 139 | 140 | results.push({ 141 | id: el.id, 142 | handleX, 143 | sliderY, 144 | handleDist, 145 | onSlider, 146 | isHandle: handleDist <= handleRadius, 147 | mouseX: x, 148 | mouseY: y 149 | }); 150 | } 151 | } 152 | 153 | return results; 154 | } 155 | 156 | #getElementAtPos(x, y) { 157 | console.log(`Checking position: ${x}, ${y}`); 158 | 159 | // Önce handleları kontrol et (daha küçük kısımlara öncelik) 160 | for (let i = this.uiElements.length - 1; i >= 0; i--) { 161 | const el = this.uiElements[i]; 162 | if (el.type === 'slider') { 163 | const value = this.settings[el.id]; 164 | const ratio = Math.max(0, Math.min(1, (value - el.min) / (el.max - el.min))); 165 | const handleX = el.x + ratio * el.width; 166 | const sliderY = el.y; 167 | 168 | const handleRadius = 20; 169 | 170 | // Önce handle yakınında mı diye kontrol et 171 | if (Math.hypot(x - handleX, y - sliderY) <= handleRadius) { 172 | console.log(`--> Hit handle for ${el.id}`); 173 | return el; 174 | } 175 | } 176 | } 177 | 178 | // Sonra sliderlardaki barları kontrol et 179 | for (let i = this.uiElements.length - 1; i >= 0; i--) { 180 | const el = this.uiElements[i]; 181 | if (el.type === 'slider') { 182 | const sliderY = el.y; 183 | 184 | // Slider çubuğunun üzerinde mi kontrol et 185 | if (x >= el.x && x <= el.x + el.width && 186 | Math.abs(y - sliderY) <= 20) { 187 | console.log(`--> Hit bar for ${el.id}`); 188 | return el; 189 | } 190 | } 191 | } 192 | 193 | console.log("--> No element hit"); 194 | return null; 195 | } 196 | 197 | #updateSliderValue(element, mouseX) { 198 | let ratio = (mouseX - element.x) / element.width; 199 | ratio = Math.max(0, Math.min(1, ratio)); // Clamp between 0 and 1 200 | let value = element.min + ratio * (element.max - element.min); 201 | 202 | // Snap to step 203 | value = Math.round(value / element.step) * element.step; 204 | value = parseFloat(value.toFixed(element.step < 1 ? 2 : 0)); // Adjust precision 205 | 206 | if (this.settings[element.id] !== value) { 207 | this.settings[element.id] = value; 208 | // Save setting to localStorage 209 | localStorage.setItem(`setting_${element.id}`, value.toString()); 210 | this.draw(); // Redraw settings panel 211 | 212 | // Log change for debugging 213 | console.log(`Setting ${element.id} changed to ${value}. Reload or Reset simulation.`); 214 | } 215 | } 216 | 217 | draw() { 218 | // Clear canvas with a light gray background 219 | this.ctx.fillStyle = '#f0f0f0'; // Lighter background 220 | this.ctx.fillRect(0, 0, this.width, this.height); 221 | 222 | // Draw Title 223 | this.ctx.fillStyle = "#2c3e50"; // Darker blue-gray title 224 | this.ctx.font = "bold 18px 'Segoe UI', Arial, sans-serif"; 225 | this.ctx.textAlign = "center"; 226 | this.ctx.fillText("Settings", this.width / 2, 30); 227 | 228 | // Reset for drawing elements 229 | this.ctx.textAlign = "left"; 230 | this.ctx.textBaseline = "middle"; 231 | this.ctx.font = "14px 'Segoe UI', Arial, sans-serif"; 232 | this.ctx.lineWidth = 1; 233 | 234 | let startY = 60; // Start drawing elements lower 235 | const elementSpacing = 55; // Space between sliders 236 | const padding = 15; 237 | const labelWidth = 80; 238 | const valueWidth = 40; 239 | const sliderX = padding + labelWidth; 240 | const sliderWidth = this.width - sliderX - valueWidth - padding; 241 | 242 | this.uiElements.forEach((el, index) => { 243 | const currentY = startY + index * elementSpacing; 244 | 245 | if (el.type === 'slider') { 246 | // Update the slider's y position to match what's used in getElementAtPos 247 | el.y = currentY + 10; // Store this position for hit detection 248 | 249 | // Draw background box for slider area 250 | this.ctx.fillStyle = "#ffffff"; // White background box 251 | this.ctx.strokeStyle = "#ccc"; // Light gray border 252 | this.ctx.lineWidth = 1; 253 | this.roundRect(padding / 2, currentY - elementSpacing / 2 + 5, this.width - padding, elementSpacing - 10, 5); 254 | this.ctx.fill(); 255 | this.ctx.stroke(); 256 | 257 | // Draw Label 258 | this.ctx.fillStyle = "#34495e"; // Slightly darker text color 259 | this.ctx.font = "13px 'Segoe UI', Arial, sans-serif"; 260 | this.ctx.fillText(el.label, padding, currentY); 261 | 262 | // --- Draw Slider --- 263 | const sliderY = el.y; // Use the same y position saved in el.y 264 | 265 | // Draw Slider Bar background 266 | this.ctx.strokeStyle = "#bdc3c7"; // Gray bar 267 | this.ctx.lineWidth = 4; 268 | this.ctx.lineCap = "round"; 269 | this.ctx.beginPath(); 270 | this.ctx.moveTo(sliderX, sliderY); 271 | this.ctx.lineTo(sliderX + sliderWidth, sliderY); 272 | this.ctx.stroke(); 273 | 274 | // Draw Slider Filled Bar 275 | const value = this.settings[el.id]; 276 | const ratio = Math.max(0, Math.min(1, (value - el.min) / (el.max - el.min))); // Ensure ratio is valid 277 | const fillWidth = ratio * sliderWidth; 278 | this.ctx.strokeStyle = "#3498db"; // Blue for filled part 279 | this.ctx.beginPath(); 280 | this.ctx.moveTo(sliderX, sliderY); 281 | this.ctx.lineTo(sliderX + fillWidth, sliderY); 282 | this.ctx.stroke(); 283 | this.ctx.lineCap = "butt"; 284 | 285 | // Draw Handle 286 | const handleX = sliderX + fillWidth; 287 | const handleRadius = 7; 288 | this.ctx.fillStyle = "#2980b9"; // Darker blue handle 289 | if (this.activeElement === el) { 290 | this.ctx.fillStyle = "#1f618d"; 291 | } 292 | this.ctx.beginPath(); 293 | this.ctx.arc(handleX, sliderY, handleRadius, 0, Math.PI * 2); 294 | this.ctx.fill(); 295 | // Add a lighter border to handle 296 | this.ctx.strokeStyle = "#ecf0f1"; 297 | this.ctx.lineWidth = 1.5; 298 | this.ctx.stroke(); 299 | 300 | // Draw Value Text 301 | this.ctx.fillStyle = "#2c3e50"; 302 | this.ctx.font = "bold 13px 'Segoe UI', Arial, sans-serif"; 303 | this.ctx.textAlign = "right"; 304 | this.ctx.fillText(value.toFixed(el.step < 1 ? 2 : 0), this.width - padding, currentY); 305 | this.ctx.textAlign = "left"; // Reset alignment 306 | } 307 | }); 308 | 309 | // Update hint text position and style 310 | this.ctx.fillStyle = "#7f8c8d"; // Gray hint text 311 | this.ctx.font = "italic 11px 'Segoe UI', Arial, sans-serif"; 312 | this.ctx.textAlign = "center"; 313 | this.ctx.fillText("Reload (Space) or Discard", this.width / 2, this.height - 30); 314 | this.ctx.fillText("to apply changed settings.", this.width / 2, this.height - 15); 315 | } 316 | 317 | // Helper function to draw rounded rectangles (add this method) 318 | roundRect(x, y, w, h, r) { 319 | if (w < 2 * r) r = w / 2; 320 | if (h < 2 * r) r = h / 2; 321 | this.ctx.beginPath(); 322 | this.ctx.moveTo(x+r, y); 323 | this.ctx.arcTo(x+w, y, x+w, y+h, r); 324 | this.ctx.arcTo(x+w, y+h, x, y+h, r); 325 | this.ctx.arcTo(x, y+h, x, y, r); 326 | this.ctx.arcTo(x, y, x+w, y, r); 327 | this.ctx.closePath(); 328 | return this; 329 | } 330 | } -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body{ 2 | margin:0; 3 | background:darkgray; 4 | overflow:hidden; 5 | display: flex; 6 | flex-direction: column; /* Back to column: Canvases row above, controls below */ 7 | height: 100vh; 8 | font-family: Arial, sans-serif; /* Add default font */ 9 | } 10 | 11 | /* Start Screen Styling Removed */ 12 | /* 13 | #startScreen { ... } 14 | #startScreen h1 { ... } 15 | #difficultySelectorStart { ... } 16 | #difficultySelectorStart label { ... } 17 | #difficultySelectorStart select { ... } 18 | #startButton { ... } 19 | #startButton:hover { ... } 20 | */ 21 | 22 | /* Container for settings and simulation area */ 23 | #mainContainer { 24 | flex-grow: 1; /* Take remaining vertical space if body wasn't 100vh */ 25 | display: flex; 26 | flex-direction: row; 27 | width: 100%; 28 | overflow: hidden; /* Prevent inner content from overflowing */ 29 | } 30 | 31 | /* Row container for the three canvases */ 32 | #canvasRow { 33 | flex-grow: 1; 34 | display: flex; 35 | flex-direction: row; 36 | justify-content: center; /* Center the canvases horizontally */ 37 | align-items: stretch; /* Make canvases fill height */ 38 | width: 100%; 39 | margin: 0 auto; 40 | overflow: hidden; 41 | padding-top: 10px; 42 | padding-bottom: 10px; 43 | gap: 10px; /* Add gap between canvases */ 44 | } 45 | 46 | /* Settings Canvas Styling */ 47 | #settingsCanvas { 48 | width: 200px; 49 | background: #eee; 50 | border-right: 2px solid #555; 51 | height: 100%; 52 | flex-shrink: 0; /* Prevent shrinking */ 53 | } 54 | 55 | /* Container for the simulation canvases */ 56 | #simulationArea { 57 | flex-grow: 1; /* Take remaining horizontal space */ 58 | display: flex; 59 | flex-direction: column; /* Stack car/network canvases vertically */ 60 | height: 100%; 61 | } 62 | 63 | /* Old container removed */ 64 | /* 65 | #canvasContainer { 66 | ... 67 | } 68 | */ 69 | 70 | #carCanvas{ 71 | background:rgb(23, 22, 29); 72 | width: 600px; /* Set a fixed width for car sim */ 73 | height: 100%; 74 | min-width: 200px; 75 | flex-shrink: 0; /* Prevent shrinking */ 76 | } 77 | 78 | #networkCanvas{ 79 | background:rgb(40, 40, 40); 80 | width: 300px; 81 | height: 100%; 82 | border-left: 2px solid #555; 83 | flex-shrink: 0; /* Prevent shrinking */ 84 | } 85 | 86 | /* Container for controls below everything */ 87 | #controlsContainer { 88 | width: 100%; /* Take full width */ 89 | /* position: fixed; - No longer fixed, part of column flow */ 90 | display: flex; 91 | justify-content: space-around; 92 | align-items: center; 93 | padding: 10px 0; 94 | background: #555; 95 | flex-shrink: 0; /* Prevent shrinking */ 96 | /* z-index: 10; Not needed anymore */ 97 | } 98 | 99 | /* Difficulty selector styling (now in game screen) */ 100 | #difficultySelectorGame { 101 | color: white; 102 | font-size: 16px; 103 | } 104 | 105 | #difficultySelectorGame label { 106 | margin-right: 5px; 107 | } 108 | 109 | #difficultySelectorGame select { 110 | padding: 5px; 111 | font-size: 16px; 112 | border-radius: 5px; 113 | } 114 | 115 | #buttons{ 116 | text-align: center; 117 | } 118 | 119 | #buttons button { 120 | padding: 10px 20px; 121 | margin: 0 10px; 122 | font-size: 16px; 123 | cursor: pointer; 124 | border: none; 125 | border-radius: 5px; 126 | } 127 | 128 | #saveButton { 129 | background-color: #4CAF50; 130 | color: white; 131 | } 132 | 133 | #discardButton { 134 | background-color: #f44336; 135 | color: white; 136 | } 137 | 138 | #buttons button:hover { 139 | opacity: 0.8; 140 | } -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | function lerp(A,B,t){ 2 | return A+(B-A)*t; 3 | } 4 | 5 | function getIntersection(A,B,C,D){ 6 | const tTop=(D.x-C.x)*(A.y-C.y)-(D.y-C.y)*(A.x-C.x); 7 | const uTop=(C.y-A.y)*(A.x-B.x)-(C.x-A.x)*(A.y-B.y); 8 | const bottom=(D.y-C.y)*(B.x-A.x)-(D.x-C.x)*(B.y-A.y); 9 | 10 | if(bottom!=0){ 11 | const t=tTop/bottom; 12 | const u=uTop/bottom; 13 | if(t>=0 && t<=1 && u>=0 && u<=1){ 14 | return { 15 | x:lerp(A.x,B.x,t), 16 | y:lerp(A.y,B.y,t), 17 | offset:t 18 | } 19 | } 20 | } 21 | 22 | return null; 23 | } 24 | 25 | function polysIntersect(poly1, poly2){ 26 | for(let i=0;i= 0; i--) { 13 | const levelTop = top + 14 | Visualizer.lerp( 15 | height - levelHeight, 16 | 0, 17 | network.levels.length == 1 18 | ? 0.5 19 | : i / (network.levels.length - 1) 20 | ); 21 | 22 | ctx.setLineDash([7, 3]); // Dashed lines for level boundaries 23 | Visualizer.drawLevel(ctx, 24 | network.levels[i], 25 | left, 26 | levelTop, 27 | width, 28 | levelHeight, 29 | i == network.levels.length - 1 // Is it the output layer? 30 | ? ['↑', '←', '→', '↓'] // Output labels (arrows) 31 | : [], 32 | time // Pass time for potential animation 33 | ); 34 | } 35 | } 36 | 37 | static drawLevel(ctx, level, left, top, width, height, outputLabels, time) { 38 | const right = left + width; 39 | const bottom = top + height; 40 | 41 | const { inputs, outputs, weights, biases } = level; 42 | 43 | // Style connections (weights) 44 | const nodeRadius = 18; 45 | ctx.lineWidth = 2; 46 | 47 | // Draw connections between input and output nodes 48 | for (let i = 0; i < inputs.length; i++) { 49 | for (let j = 0; j < outputs.length; j++) { 50 | ctx.beginPath(); 51 | ctx.moveTo( 52 | Visualizer.getNodeX(inputs, i, left, right), 53 | bottom 54 | ); 55 | ctx.lineTo( 56 | Visualizer.getNodeX(outputs, j, left, right), 57 | top 58 | ); 59 | // Set color based on weight (positive=yellow, negative=blue) 60 | ctx.strokeStyle = Visualizer.getRGBA(weights[i][j]); 61 | ctx.stroke(); 62 | } 63 | } 64 | 65 | // Draw input nodes 66 | for (let i = 0; i < inputs.length; i++) { 67 | const x = Visualizer.getNodeX(inputs, i, left, right); 68 | ctx.beginPath(); 69 | ctx.arc(x, bottom, nodeRadius, 0, Math.PI * 2); 70 | ctx.fillStyle = "black"; // Node background 71 | ctx.fill(); 72 | ctx.beginPath(); 73 | ctx.arc(x, bottom, nodeRadius * 0.6, 0, Math.PI * 2); 74 | // Fill color based on input value (intensity) 75 | ctx.fillStyle = Visualizer.getRGBA(inputs[i]); 76 | ctx.fill(); 77 | } 78 | 79 | // Draw output nodes 80 | for (let i = 0; i < outputs.length; i++) { 81 | const x = Visualizer.getNodeX(outputs, i, left, right); 82 | // Draw node background 83 | ctx.beginPath(); 84 | ctx.arc(x, top, nodeRadius, 0, Math.PI * 2); 85 | ctx.fillStyle = "black"; 86 | ctx.fill(); 87 | // Draw activation fill based on output value 88 | ctx.beginPath(); 89 | ctx.arc(x, top, nodeRadius * 0.6, 0, Math.PI * 2); 90 | ctx.fillStyle = Visualizer.getRGBA(outputs[i]); 91 | ctx.fill(); 92 | 93 | // Draw bias visualization (outer ring) 94 | ctx.beginPath(); 95 | ctx.lineWidth = 2; 96 | ctx.arc(x, top, nodeRadius * 0.8, 0, Math.PI * 2); 97 | // Color based on bias value 98 | ctx.strokeStyle = Visualizer.getRGBA(biases[i]); 99 | // Add animation effect to bias ring using time 100 | const phase = Math.sin(time / 500 + i * 0.5) * 5 + 5; // Pulsating effect 101 | ctx.setLineDash([phase, 3]); 102 | ctx.stroke(); 103 | ctx.setLineDash([]); // Reset line dash 104 | 105 | // Draw output labels if provided 106 | if (outputLabels[i]) { 107 | ctx.beginPath(); 108 | ctx.textAlign = "center"; 109 | ctx.textBaseline = "middle"; 110 | ctx.fillStyle = "black"; 111 | ctx.strokeStyle = "white"; 112 | ctx.font = (nodeRadius * 1) + "px Arial"; 113 | ctx.fillText(outputLabels[i], x, top + nodeRadius * 0.1); 114 | ctx.lineWidth=0.5; 115 | ctx.strokeText(outputLabels[i], x, top + nodeRadius * 0.1); 116 | } 117 | } 118 | } 119 | 120 | // Helper to calculate horizontal position of a node 121 | static getNodeX(nodes, index, left, right) { 122 | return Visualizer.lerp( 123 | left, 124 | right, 125 | nodes.length == 1 126 | ? 0.5 127 | : index / (nodes.length - 1) 128 | ); 129 | } 130 | 131 | // Helper to get RGBA color based on value (positive=yellow, negative=blue) 132 | static getRGBA(value) { 133 | const alpha = Math.abs(value); // Intensity based on absolute value 134 | const R = value > 0 ? 0 : 255; // Blue if negative 135 | const G = R; // Yellow/Blue mix based on sign 136 | const B = value < 0 ? 0 : 255; // Yellow if positive 137 | return `rgba(${R},${G},${B},${alpha})`; 138 | } 139 | 140 | // Linear interpolation 141 | static lerp(a, b, t) { 142 | return a + (b - a) * t; 143 | } 144 | } --------------------------------------------------------------------------------