seawolf2357 commited on
Commit
0d4aab1
ยท
verified ยท
1 Parent(s): 4b5d684

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +261 -338
index.html CHANGED
@@ -4,7 +4,7 @@
4
  <meta charset="UTF-8">
5
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>AI Driving Simulation</title>
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" />
9
  <style>
10
  body {
@@ -263,7 +263,7 @@
263
  </head>
264
  <body>
265
  <div class="container">
266
- <h1><i class="fas fa-car-side"></i> Evolution Simulation</h1>
267
 
268
  <canvas id="simulationCanvas" width="800" height="500"></canvas>
269
 
@@ -329,14 +329,14 @@
329
  <div class="section-title">
330
  <i class="fas fa-info-circle"></i> <h3>About This Simulation</h3>
331
  </div>
332
- <p>This simulation demonstrates how AI can learn to drive using genetic algorithms and neural networks. Cars must navigate randomly generated tracks without any prior knowledge of the environment.</p>
333
- <p><strong>Key Improvements:</strong></p>
334
  <ul>
335
- <li><i class="fas fa-brain"></i> <strong>Enhanced Neural Network:</strong> Using Sigmoid activation function for smoother decision making</li>
336
- <li><i class="fas fa-random"></i> <strong>Crossover:</strong> Combining the best traits from parent models</li>
337
- <li><i class="fas fa-chart-line"></i> <strong>Adaptive Mutation:</strong> Automatically adjusts as generations progress</li>
338
- <li><i class="fas fa-bolt"></i> <strong>Performance Optimization:</strong> Delta-time based updates for consistent simulation</li>
339
- <li><i class="fas fa-exclamation-triangle"></i> <strong>Improved Collision Detection:</strong> More accurate polygon-based detection</li>
340
  <li><i class="fas fa-save"></i> <strong>Model Saving:</strong> Save and load your best models</li>
341
  </ul>
342
  </div>
@@ -344,7 +344,7 @@
344
 
345
  <script>
346
  document.addEventListener('DOMContentLoaded', () => {
347
- // Add roundRect polyfill for browsers that don't support it
348
  if (!CanvasRenderingContext2D.prototype.roundRect) {
349
  CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius) {
350
  if (typeof radius === 'undefined') {
@@ -366,11 +366,11 @@
366
  };
367
  }
368
 
369
- // Canvas ์„ค์ •
370
  const canvas = document.getElementById('simulationCanvas');
371
  const ctx = canvas.getContext('2d');
372
 
373
- // UI ์š”์†Œ
374
  const startBtn = document.getElementById('startBtn');
375
  const pauseBtn = document.getElementById('pauseBtn');
376
  const resetBtn = document.getElementById('resetBtn');
@@ -392,10 +392,10 @@
392
  const fpsCounter = document.getElementById('fpsCounter');
393
  const bestProgressBar = document.getElementById('bestProgressBar');
394
 
395
- // ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋งค๊ฐœ๋ณ€์ˆ˜
396
  let populationSize = parseInt(populationSlider.value);
397
  let mutationRate = parseInt(mutationSlider.value) / 100;
398
- let simulationSpeed = parseInt(speedSlider.value) * 3; // 3๋ฐฐ ๋น ๋ฅธ ์†๋„
399
  let isRunning = false;
400
  let generation = 0;
401
  let fps = 0;
@@ -405,137 +405,165 @@
405
  let frameCount = 0;
406
  let lastFpsUpdate = 0;
407
 
408
- // ํ™œ์„ฑํ™” ํ•จ์ˆ˜
409
  const sigmoid = (x) => 1 / (1 + Math.exp(-x));
410
- const relu = (x) => Math.max(0, x);
411
 
412
- // ํŠธ๋ž™ ์ •์˜
413
  const track = {
414
- walls: [],
415
- checkpoints: [],
416
  startPosition: { x: 100, y: 250, angle: 0 },
417
 
418
  generateRandomTrack() {
419
  this.walls = [];
420
  this.checkpoints = [];
421
 
422
- // ์™ธ๋ถ€ ๊ฒฝ๊ณ„ ๋ฒฝ (ํ•ญ์ƒ ์กด์žฌ)
423
- this.walls.push(
424
- { x: 50, y: 50, width: 700, height: 20 }, // ์ƒ๋‹จ
425
- { x: 50, y: 50, width: 20, height: 400 }, // ์ขŒ์ธก
426
- { x: 50, y: 430, width: 700, height: 20 }, // ํ•˜๋‹จ
427
- { x: 730, y: 50, width: 20, height: 400 } // ์šฐ์ธก
428
- );
 
 
 
 
429
 
430
- // ๋ฌด์ž‘์œ„ ์žฅ์• ๋ฌผ ์ƒ์„ฑ
431
- const obstacleCount = 3 + Math.floor(Math.random() * 6);
432
- for (let i = 0; i < obstacleCount; i++) {
433
- const isVertical = Math.random() > 0.5;
434
- let x, y, width, height;
435
 
436
- if (isVertical) {
437
- width = 20;
438
- height = 50 + Math.random() * 200;
439
- x = 100 + Math.random() * 600;
440
- y = 100 + Math.random() * (400 - height);
441
- } else {
442
- width = 50 + Math.random() * 200;
443
- height = 20;
444
- x = 100 + Math.random() * (700 - width);
445
- y = 100 + Math.random() * 300;
446
- }
447
 
448
- // ์‹œ์ž‘ ์œ„์น˜๋ฅผ ๋ง‰์ง€ ์•Š๋„๋ก ํ™•์ธ
449
- if (!(x < 150 && y < 300 && y + height > 200)) {
450
- this.walls.push({ x, y, width, height });
451
- }
 
 
 
 
 
 
 
 
452
  }
453
 
454
- // ์ฒดํฌํฌ์ธํŠธ ์ƒ์„ฑ
455
- const checkpointCount = 3 + Math.floor(Math.random() * 3);
456
- const checkpointSize = 30;
457
-
458
- // ์žฅ์• ๋ฌผ ์ฃผ๋ณ€์„ ํ†ต๊ณผํ•ด์•ผ ํ•˜๋Š” ์œ„์น˜ ์ƒ์„ฑ
459
- const possiblePositions = [
460
- { x: 700, y: 100 }, // ์šฐ์ธก ์ƒ๋‹จ
461
- { x: 600, y: 400 }, // ์šฐ์ธก ํ•˜๋‹จ ์ค‘์•™
462
- { x: 300, y: 400 }, // ํ•˜๋‹จ ์ค‘์•™
463
- { x: 100, y: 300 }, // ์ขŒ์ธก ์ค‘์•™
464
- { x: 400, y: 100 }, // ์ƒ๋‹จ ์ค‘์•™
465
- { x: 200, y: 200 }, // ์ขŒ์ธก ์ค‘์•™
466
- { x: 600, y: 200 } // ์šฐ์ธก ์ค‘์•™
467
- ];
 
 
 
 
 
 
468
 
469
- // ์…”ํ”Œํ•˜๊ณ  ์ฒดํฌํฌ์ธํŠธ ์ˆ˜๋งŒํผ ์„ ํƒ
470
- const shuffled = [...possiblePositions].sort(() => 0.5 - Math.random());
 
471
  for (let i = 0; i < checkpointCount; i++) {
472
- const pos = shuffled[i];
 
 
 
 
 
 
473
  this.checkpoints.push({
474
- x: pos.x,
475
- y: pos.y,
476
- width: checkpointSize,
477
- height: checkpointSize
478
  });
479
  }
480
 
481
- // ์‹œ์ž‘ ์œ„์น˜ ์„ค์ • (ํ•ญ์ƒ ์ขŒ์ธก, ์ˆ˜์ง ์œ„์น˜๋Š” ๋ฌด์ž‘์œ„)
 
 
482
  this.startPosition = {
483
- x: 100,
484
- y: 100 + Math.random() * 300,
485
- angle: 0
486
  };
487
  },
488
 
489
  draw(ctx) {
490
- // ๋ฒฝ ๊ทธ๋ฆฌ๊ธฐ
491
- ctx.fillStyle = '#4a5568';
 
 
492
  this.walls.forEach(wall => {
493
- ctx.fillRect(wall.x, wall.y, wall.width, wall.height);
 
 
 
494
  });
495
 
496
- // ์ฒดํฌํฌ์ธํŠธ ๊ทธ๋ฆฌ๊ธฐ
497
  ctx.fillStyle = 'rgba(74, 222, 128, 0.3)';
 
 
498
  this.checkpoints.forEach((checkpoint, index) => {
 
499
  ctx.fillRect(checkpoint.x, checkpoint.y, checkpoint.width, checkpoint.height);
500
 
501
- // Add checkpoint number
 
 
 
502
  ctx.fillStyle = 'white';
503
  ctx.font = '12px Arial';
504
  ctx.textAlign = 'center';
505
  ctx.textBaseline = 'middle';
506
- ctx.fillText((index + 1).toString(),
507
- checkpoint.x + checkpoint.width/2,
508
- checkpoint.y + checkpoint.height/2);
509
-
510
  ctx.fillStyle = 'rgba(74, 222, 128, 0.3)';
511
  });
512
 
513
- // ์‹œ์ž‘ ์œ„์น˜ ๊ทธ๋ฆฌ๊ธฐ
514
  ctx.fillStyle = 'rgba(96, 165, 250, 0.5)';
515
- ctx.fillRect(this.startPosition.x - 15, this.startPosition.y - 25, 30, 50);
 
 
 
516
 
517
- // Draw start flag
518
  ctx.fillStyle = 'white';
519
- ctx.font = '14px Arial';
520
  ctx.textAlign = 'center';
521
- ctx.fillText("START", this.startPosition.x, this.startPosition.y + 15);
522
  }
523
  };
524
 
525
- // ์ž๋™์ฐจ ํด๋ž˜์Šค
526
  class Car {
527
  constructor(brain) {
528
  this.reset();
529
  this.brain = brain ? brain : new NeuralNetwork([5, 8, 2]);
530
  this.fitness = 0;
531
  this.checkpointIndex = 0;
532
- this.sensors = [0, 0, 0, 0, 0]; // ์ „๋ฐฉ, ์ขŒ์ธก, ์šฐ์ธก, ์ขŒ์ „๋ฐฉ, ์šฐ์ „๋ฐฉ
533
  this.sensorAngles = [0, -Math.PI/4, Math.PI/4, -Math.PI/8, Math.PI/8];
534
  this.sensorLength = 100;
535
  this.color = 'rgba(59, 130, 246, 0.8)';
536
  this.isBest = false;
537
  this.lastPosition = { x: 0, y: 0 };
538
- this.stuckTime = 0; // ์ž๋™์ฐจ๊ฐ€ ์›€์ง์ด์ง€ ์•Š๋Š” ์‹œ๊ฐ„ ์ถ”์ 
539
  }
540
 
541
  reset() {
@@ -543,9 +571,9 @@
543
  this.y = track.startPosition.y;
544
  this.angle = track.startPosition.angle;
545
  this.speed = 0;
546
- this.maxSpeed = 10; // ์ตœ๋Œ€ ์†๋„ ์ฆ๊ฐ€
547
- this.acceleration = 0.2; // ๊ฐ€์†๋„ ์ฆ๊ฐ€
548
- this.rotationSpeed = 0.1; // ํšŒ์ „ ์†๋„ ์ฆ๊ฐ€
549
  this.damaged = false;
550
  this.checkpointIndex = 0;
551
  this.fitness = 0;
@@ -556,110 +584,84 @@
556
  update(dt) {
557
  if (this.damaged) return;
558
 
559
- // ์ด์ „ ์œ„์น˜ ์ €์žฅ
560
  this.lastPosition = { x: this.x, y: this.y };
561
 
562
- // ์„ผ์„œ ์—…๋ฐ์ดํŠธ
563
  this.updateSensors();
564
 
565
- // ์‹ ๊ฒฝ๋ง ์ถœ๋ ฅ ๊ฐ€์ ธ์˜ค๊ธฐ
566
  const outputs = this.brain.predict(this.sensors);
567
 
568
- // ์กฐํ–ฅ ์ ์šฉ (outputs[0] = ์™ผ์ชฝ, outputs[1] = ์˜ค๋ฅธ์ชฝ)
569
- const steering = outputs[1] - outputs[0]; // -1์—์„œ 1 ์‚ฌ์ด
570
  this.angle += steering * this.rotationSpeed * dt;
571
 
572
- // ์†๋„์™€ ์œ„์น˜ ์—…๋ฐ์ดํŠธ
573
  this.speed = this.maxSpeed;
574
  this.x += Math.sin(this.angle) * this.speed * dt;
575
  this.y -= Math.cos(this.angle) * this.speed * dt;
576
 
577
- // ์ถฉ๋Œ ๊ฒ€์‚ฌ
578
  this.checkCollisions();
579
 
580
- // ์ฒดํฌํฌ์ธํŠธ ๊ฒ€์‚ฌ
581
  this.checkCheckpoints();
582
 
583
- // ์ •์ง€ ํ™•์ธ (์ž๋™์ฐจ๊ฐ€ ์›€์ง์ด์ง€ ์•Š๋Š” ๊ฒฝ์šฐ)
584
  const distance = Math.sqrt(
585
- Math.pow(this.x - this.lastPosition.x, 2) +
586
- Math.pow(this.y - this.lastPosition.y, 2)
587
  );
588
 
589
  if (distance < 0.5 * dt) {
590
  this.stuckTime += dt;
591
- if (this.stuckTime > 1.5) { // ์ •์ง€ ํŒ๋‹จ ์‹œ๊ฐ„ ๋‹จ์ถ• (3์ดˆ โ†’ 1.5์ดˆ)
592
  this.damaged = true;
593
  }
594
  } else {
595
  this.stuckTime = 0;
596
-
597
- // ์ ํ•ฉ๋„ ์—…๋ฐ์ดํŠธ
598
- this.fitness += distance; // ์ด๋™ ๊ฑฐ๋ฆฌ์— ๋”ฐ๋ฅธ ์ ํ•ฉ๋„
599
  }
600
  }
601
 
602
  updateSensors() {
603
  this.sensors = this.sensorAngles.map(angle => {
604
  const sensorAngle = this.angle + angle;
605
- let sensorEndX = this.x + Math.sin(sensorAngle) * this.sensorLength;
606
- let sensorEndY = this.y - Math.cos(sensorAngle) * this.sensorLength;
607
 
608
  let minDistance = this.sensorLength;
609
 
610
- // ๋ชจ๋“  ๋ฒฝ์— ๋Œ€ํ•ด ๊ฒ€์‚ฌ
611
  for (const wall of track.walls) {
612
- const intersection = this.lineRectIntersection(
613
  this.x, this.y, sensorEndX, sensorEndY,
614
- wall.x, wall.y, wall.width, wall.height
615
  );
616
 
617
  if (intersection) {
618
- const distance = Math.sqrt(
619
- Math.pow(intersection.x - this.x, 2) +
620
- Math.pow(intersection.y - this.y, 2)
621
  );
622
-
623
- minDistance = Math.min(minDistance, distance);
 
624
  }
625
  }
626
 
627
- // ์ •๊ทœํ™”๋œ ๊ฑฐ๋ฆฌ ๋ฐ˜ํ™˜ (0-1 ๋ฒ”์œ„; 1 = ์žฅ์• ๋ฌผ ์—†์Œ, 0 = ์ž๋™์ฐจ ๋ฐ”๋กœ ์•ž์— ์žฅ์• ๋ฌผ)
628
  return 1 - (minDistance / this.sensorLength);
629
  });
630
  }
631
 
632
- lineRectIntersection(x1, y1, x2, y2, rx, ry, rw, rh) {
633
- // ์ง์„ ์ด ์ง์‚ฌ๊ฐํ˜•์˜ ๋ณ€๊ณผ ๊ต์ฐจํ•˜๋Š”์ง€ ํ™•์ธ
634
- const left = this.lineLineIntersection(x1, y1, x2, y2, rx, ry, rx, ry + rh);
635
- const right = this.lineLineIntersection(x1, y1, x2, y2, rx + rw, ry, rx + rw, ry + rh);
636
- const top = this.lineLineIntersection(x1, y1, x2, y2, rx, ry, rx + rw, ry);
637
- const bottom = this.lineLineIntersection(x1, y1, x2, y2, rx, ry + rh, rx + rw, ry + rh);
638
-
639
- let closestIntersection = null;
640
- let minDistance = Infinity;
641
-
642
- [left, right, top, bottom].forEach(intersection => {
643
- if (intersection) {
644
- const distance = Math.sqrt(Math.pow(intersection.x - x1, 2) + Math.pow(intersection.y - y1, 2));
645
- if (distance < minDistance) {
646
- minDistance = distance;
647
- closestIntersection = intersection;
648
- }
649
- }
650
- });
651
 
652
- return closestIntersection;
653
- }
654
-
655
- lineLineIntersection(x1, y1, x2, y2, x3, y3, x4, y4) {
656
- // ๋‘ ์ง์„  ์‚ฌ์ด์˜ ๊ต์ฐจ์  ๊ณ„์‚ฐ
657
- const denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
658
-
659
- if (denominator === 0) return null; // ์ง์„ ์ด ํ‰ํ–‰
660
-
661
- const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
662
- const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;
663
 
664
  if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
665
  return {
@@ -667,47 +669,48 @@
667
  y: y1 + ua * (y2 - y1)
668
  };
669
  }
670
-
671
  return null;
672
  }
673
 
674
  checkCollisions() {
675
- // ๋ฒฝ๊ณผ์˜ ์ถฉ๋Œ ๊ฐ์ง€ (๊ฐœ์„ ๋œ ๊ณ„์‚ฐ)
676
- // ์ž๋™์ฐจ๋ฅผ ๋‹ค๊ฐํ˜•์œผ๋กœ ํ‘œํ˜„ํ•˜์—ฌ ๋” ์ •ํ™•ํ•œ ์ถฉ๋Œ ๊ฐ์ง€
677
- const carCorners = this.getCarCorners();
 
 
 
 
 
 
678
 
679
- // ๋ชจ๋“  ๋ฒฝ์— ๋Œ€ํ•ด ๊ฒ€์‚ฌ
680
  for (const wall of track.walls) {
681
- // ๊ฐ„๋‹จํ•œ ์‚ฌ๊ฐํ˜• ์ถฉ๋Œ ํ™•์ธ (์ตœ์ ํ™”๋ฅผ ์œ„ํ•œ ์ฒซ ๋‹จ๊ณ„)
682
- if (this.x > wall.x - 10 && this.x < wall.x + wall.width + 10 &&
683
- this.y > wall.y - 10 && this.y < wall.y + wall.height + 10) {
684
-
685
- // ์ž๋™์ฐจ ๊ผญ์ง€์ ์ด ๋ฒฝ ๋‚ด๋ถ€์— ์žˆ๋Š”์ง€ ํ™•์ธ
686
- for (const corner of carCorners) {
687
- if (corner.x > wall.x && corner.x < wall.x + wall.width &&
688
- corner.y > wall.y && corner.y < wall.y + wall.height) {
689
- this.damaged = true;
690
- return;
691
- }
692
  }
693
  }
694
  }
695
 
696
- // ๊ฒฝ๊ณ„ ํ™•์ธ
697
  if (this.x < 0 || this.x > canvas.width || this.y < 0 || this.y > canvas.height) {
698
  this.damaged = true;
699
  }
700
  }
701
 
702
  getCarCorners() {
703
- // ์ž๋™์ฐจ์˜ ๋„ค ๊ผญ์ง€์  ๊ณ„์‚ฐ
704
  const width = 12;
705
  const height = 20;
706
  const cornerOffsets = [
707
- { x: -width/2, y: -height/2 }, // ์ขŒ์ƒ๋‹จ
708
- { x: width/2, y: -height/2 }, // ์šฐ์ƒ๋‹จ
709
- { x: width/2, y: height/2 }, // ์šฐํ•˜๋‹จ
710
- { x: -width/2, y: height/2 } // ์ขŒํ•˜๋‹จ
711
  ];
712
 
713
  return cornerOffsets.map(offset => {
@@ -724,21 +727,21 @@
724
  if (this.checkpointIndex >= track.checkpoints.length) return;
725
 
726
  const checkpoint = track.checkpoints[this.checkpointIndex];
727
- if (this.x > checkpoint.x && this.x < checkpoint.x + checkpoint.width &&
728
- this.y > checkpoint.y && this.y < checkpoint.y + checkpoint.height) {
 
 
 
 
729
  this.checkpointIndex++;
730
- this.fitness += 1000; // Bonus for reaching checkpoint
731
 
732
- // Update best progress visualization
733
  const progress = this.checkpointIndex / track.checkpoints.length;
734
  if (progress > bestCarProgress) {
735
  bestCarProgress = progress;
736
  bestProgressBar.style.width = `${progress * 100}%`;
737
-
738
- // Add confetti effect for completed checkpoints
739
- if (this.checkpointIndex > 0) {
740
- createConfetti(10, this.x, this.y);
741
- }
742
  }
743
  }
744
  }
@@ -750,26 +753,24 @@
750
  ctx.translate(this.x, this.y);
751
  ctx.rotate(this.angle);
752
 
753
- // Draw car body - Enhanced car shape with emoji style
754
  if (this.isBest) {
755
- // Draw fancy car for the best performer
756
  ctx.fillStyle = 'rgba(220, 38, 38, 0.9)';
757
-
758
- // Main body - using rectangles instead of roundRect to avoid issues
759
  ctx.fillRect(-6, -10, 12, 20);
760
 
761
  // Wheels
762
  ctx.fillStyle = '#000';
763
- ctx.fillRect(-7, -8, 2, 4); // left front
764
- ctx.fillRect(5, -8, 2, 4); // right front
765
- ctx.fillRect(-7, 4, 2, 4); // left rear
766
- ctx.fillRect(5, 4, 2, 4); // right rear
767
 
768
  // Windshield
769
  ctx.fillStyle = '#60a5fa';
770
  ctx.fillRect(-4, -8, 8, 6);
771
 
772
- // Draw a small crown on top
773
  ctx.fillStyle = '#facc15';
774
  ctx.beginPath();
775
  ctx.moveTo(-3, -11);
@@ -780,47 +781,43 @@
780
  ctx.lineTo(-3, -10);
781
  ctx.fill();
782
  } else {
783
- // Regular car
784
  ctx.fillStyle = this.color;
785
-
786
- // Main body - using rectangles
787
  ctx.fillRect(-6, -10, 12, 20);
788
 
789
- // Wheels (simple)
790
  ctx.fillStyle = '#000';
791
- ctx.fillRect(-7, -7, 2, 3); // left front
792
- ctx.fillRect(5, -7, 2, 3); // right front
793
- ctx.fillRect(-7, 4, 2, 3); // left rear
794
- ctx.fillRect(5, 4, 2, 3); // right rear
795
 
796
- // Simple windshield
797
  ctx.fillStyle = '#a3e0ff';
798
  ctx.fillRect(-4, -7, 8, 5);
799
  }
800
 
801
- // ์ตœ๊ณ  ์ž๋™์ฐจ์˜ ์„ผ์„œ ๊ทธ๋ฆฌ๊ธฐ
802
  if (this.isBest) {
803
- ctx.restore(); // ์ปจํ…์ŠคํŠธ ๋ณต์›
804
 
805
- // ์„ผ์„œ ๋ ˆ์ด ๊ทธ๋ฆฌ๊ธฐ
806
  ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
807
  ctx.lineWidth = 1;
808
-
809
  this.sensorAngles.forEach((angle, i) => {
810
  const sensorAngle = this.angle + angle;
811
  const sensorValue = this.sensors[i];
812
- const sensorLength = this.sensorLength * (1 - sensorValue);
813
 
814
- const endX = this.x + Math.sin(sensorAngle) * sensorLength;
815
- const endY = this.y - Math.cos(sensorAngle) * sensorLength;
816
 
817
  ctx.beginPath();
818
  ctx.moveTo(this.x, this.y);
819
  ctx.lineTo(endX, endY);
820
  ctx.stroke();
821
  });
822
-
823
- return; // ์ด๋ฏธ ctx.restore()๋ฅผ ํ˜ธ์ถœํ–ˆ์œผ๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ ์ข…๋ฃŒ
824
  }
825
 
826
  ctx.restore();
@@ -831,7 +828,7 @@
831
  }
832
  }
833
 
834
- // ์‹ ๊ฒฝ๋ง ํด๋ž˜์Šค
835
  class NeuralNetwork {
836
  constructor(neuronCounts) {
837
  this.levels = [];
@@ -888,9 +885,8 @@
888
  }
889
 
890
  static crossover(parentA, parentB) {
891
- // ๋‘ ๋ถ€๋ชจ ์‹ ๊ฒฝ๋ง์—์„œ ์ƒˆ ์‹ ๊ฒฝ๋ง ์ƒ์„ฑ
892
  if (parentA.levels.length !== parentB.levels.length) {
893
- console.error("๋ถ€๋ชจ ์‹ ๊ฒฝ๋ง ๊ตฌ์กฐ๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค!");
894
  return parentA.clone();
895
  }
896
 
@@ -903,26 +899,22 @@
903
 
904
  if (levelA.inputs.length !== levelB.inputs.length ||
905
  levelA.outputs.length !== levelB.outputs.length) {
906
- console.error("๋ถ€๋ชจ ๋ ˆ๋ฒจ ๊ตฌ์กฐ๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค!");
907
  return parentA.clone();
908
  }
909
 
910
  const childLevel = new Level(levelA.inputs.length, levelA.outputs.length);
911
 
912
- // ๊ต์ฐจ์  ์„ ํƒ (๋‹จ์ผ์  ๊ต์ฐจ)
913
  const biasesSwitch = Math.floor(Math.random() * levelA.biases.length);
914
-
915
- // ๋ฐ”์ด์–ด์Šค ๊ต์ฐจ
916
  for (let i = 0; i < childLevel.biases.length; i++) {
917
  childLevel.biases[i] = i < biasesSwitch
918
  ? levelA.biases[i]
919
  : levelB.biases[i];
920
  }
921
 
922
- // ๊ฐ€์ค‘์น˜ ๊ต์ฐจ
923
  for (let i = 0; i < childLevel.weights.length; i++) {
924
  const weightSwitch = Math.floor(Math.random() * levelA.weights[i].length);
925
-
926
  for (let j = 0; j < childLevel.weights[i].length; j++) {
927
  childLevel.weights[i][j] = j < weightSwitch
928
  ? levelA.weights[i][j]
@@ -976,35 +968,32 @@
976
  this.weights[i] = new Array(outputCount);
977
  }
978
 
979
- Level.#randomize(this);
980
  }
981
 
982
- static #randomize(level) {
983
  for (let i = 0; i < level.inputs.length; i++) {
984
  for (let j = 0; j < level.outputs.length; j++) {
985
  level.weights[i][j] = Math.random() * 2 - 1;
986
  }
987
  }
988
-
989
  for (let i = 0; i < level.biases.length; i++) {
990
  level.biases[i] = Math.random() * 2 - 1;
991
  }
992
  }
993
 
994
  static feedForward(givenInputs, level) {
995
- // ์ž…๋ ฅ ๊ฐ’ ์„ค์ •
996
  for (let i = 0; i < level.inputs.length; i++) {
997
  level.inputs[i] = givenInputs[i];
998
  }
999
 
1000
- // ๊ฐ ์ถœ๋ ฅ ๋‰ด๋Ÿฐ์— ๋Œ€ํ•ด ๊ฐ€์ค‘ ํ•ฉ๊ณ„ ๊ณ„์‚ฐ
1001
  for (let i = 0; i < level.outputs.length; i++) {
1002
  let sum = 0;
1003
  for (let j = 0; j < level.inputs.length; j++) {
1004
  sum += level.inputs[j] * level.weights[j][i];
1005
  }
1006
 
1007
- // Sigmoid ํ™œ์„ฑํ™” ํ•จ์ˆ˜ ์ ์šฉ
1008
  level.outputs[i] = sigmoid(sum - level.biases[i]);
1009
  }
1010
 
@@ -1021,46 +1010,41 @@
1021
  }
1022
  }
1023
 
1024
- // ์œ ์ „ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ํ•จ์ˆ˜
1025
  function nextGeneration() {
1026
  generation++;
1027
  generationCount.textContent = generation;
1028
 
1029
- // ์ ํ•ฉ๋„ ๊ณ„์‚ฐ
1030
  calculateFitness();
1031
 
1032
- // ์ƒˆ ์ธ๊ตฌ ์ƒ์„ฑ
1033
- const newPopulation = [];
1034
-
1035
- // ์ ์‘ํ˜• ๋Œ์—ฐ๋ณ€์ด์œจ ์ ์šฉ
1036
  const progressRate = bestCarProgress;
1037
  const adaptedRate = mutationRate * (1 - progressRate * 0.5);
1038
- mutationRate = Math.max(0.01, adaptedRate); // ์ตœ์†Œ 1%
1039
  mutationValue.textContent = `${Math.round(mutationRate * 100)}%`;
1040
  mutationSlider.value = Math.round(mutationRate * 100);
1041
 
1042
- // ์ด์ „ ์„ธ๋Œ€์—์„œ ์ตœ๊ณ  ์ž๋™์ฐจ ์ถ”๊ฐ€ (์—˜๋ฆฌํ‹ฐ์ฆ˜)
1043
- const eliteCount = Math.max(1, Math.floor(populationSize * 0.05)); // 5% ์—˜๋ฆฌํŠธ
 
1044
  const eliteCars = getTopCars(eliteCount);
1045
 
1046
  for (const eliteCar of eliteCars) {
1047
- eliteCar.isBest = eliteCar === eliteCars[0];
1048
  newPopulation.push(eliteCar.clone());
1049
  }
1050
 
1051
- // ๊ต์ฐจ์™€ ๋Œ์—ฐ๋ณ€์ด๋กœ ๋‚˜๋จธ์ง€ ์ฑ„์šฐ๊ธฐ
1052
  while (newPopulation.length < populationSize) {
1053
  if (Math.random() < 0.7 && newPopulation.length + 1 < populationSize) {
1054
- // ๊ต์ฐจ
1055
  const parentA = selectParent();
1056
  const parentB = selectParent();
1057
  const child = new Car(NeuralNetwork.crossover(parentA.brain, parentB.brain));
1058
-
1059
- // ์ž์‹์—๊ฒŒ ์•ฝ๊ฐ„์˜ ๋Œ์—ฐ๋ณ€์ด ์ ์šฉ
1060
  child.brain.mutate(mutationRate);
1061
  newPopulation.push(child);
1062
  } else {
1063
- // ๋Œ์—ฐ๋ณ€์ด๋งŒ
1064
  const parent = selectParent();
1065
  const child = parent.clone();
1066
  child.brain.mutate(mutationRate);
@@ -1068,13 +1052,9 @@
1068
  }
1069
  }
1070
 
1071
- // ์ด์ „ ์ธ๊ตฌ ๊ต์ฒด
1072
  cars = newPopulation;
1073
-
1074
- // ์ž๋™์ฐจ ์ดˆ๊ธฐํ™”
1075
  cars.forEach(car => car.reset());
1076
 
1077
- // ์ง„ํ–‰๋ฅ  ์ดˆ๊ธฐํ™”
1078
  bestCarProgress = 0;
1079
  bestProgressBar.style.width = '0%';
1080
  }
@@ -1084,26 +1064,20 @@
1084
  let max = 0;
1085
 
1086
  cars.forEach(car => {
1087
- // ์ฒดํฌํฌ์ธํŠธ ๋‹ฌ์„ฑ ๋ณด๋„ˆ์Šค
1088
  car.fitness += car.checkpointIndex * 500;
1089
-
1090
  sum += car.fitness;
1091
  if (car.fitness > max) max = car.fitness;
1092
  });
1093
 
1094
- // ์ ํ•ฉ๋„ ์ •๊ทœํ™”
1095
  cars.forEach(car => {
1096
  car.fitness = car.fitness / sum;
1097
  });
1098
 
1099
- // UI ์—…๋ฐ์ดํŠธ
1100
  maxFitness.textContent = Math.round(max);
1101
  }
1102
 
1103
  function getTopCars(count) {
1104
- return [...cars]
1105
- .sort((a, b) => b.fitness - a.fitness)
1106
- .slice(0, count);
1107
  }
1108
 
1109
  function getBestCar() {
@@ -1116,25 +1090,22 @@
1116
  bestCar = cars[i];
1117
  }
1118
  }
1119
-
1120
  return bestCar;
1121
  }
1122
 
1123
  function selectParent() {
1124
- // ๋ฃฐ๋ › ํœ  ์„ ํƒ
1125
  let index = 0;
1126
  let r = Math.random();
1127
-
1128
  while (r > 0 && index < cars.length) {
1129
  r -= cars[index].fitness;
1130
  index++;
1131
  }
1132
-
1133
  index = Math.min(cars.length - 1, Math.max(0, index - 1));
1134
  return cars[index];
1135
  }
1136
 
1137
- // ๋ชจ๋ธ ์ €์žฅ/๋ถˆ๋Ÿฌ์˜ค๊ธฐ ํ•จ์ˆ˜
1138
  function saveBestModel() {
1139
  const bestCar = getBestCar();
1140
  if (bestCar) {
@@ -1142,10 +1113,9 @@
1142
  const modelData = {
1143
  brain: bestCar.brain.toJSON(),
1144
  fitness: bestCar.fitness,
1145
- generation: generation,
1146
  timestamp: new Date().toISOString()
1147
  };
1148
-
1149
  localStorage.setItem('bestCarModel', JSON.stringify(modelData));
1150
  return true;
1151
  } catch (error) {
@@ -1161,32 +1131,24 @@
1161
  const savedModel = localStorage.getItem('bestCarModel');
1162
  if (savedModel) {
1163
  const modelData = JSON.parse(savedModel);
 
1164
 
1165
- // Use saved model for new population
1166
  const newPopulation = [];
1167
-
1168
- // Create best car with restored brain
1169
- const restoredBrain = NeuralNetwork.fromJSON(modelData.brain);
1170
  const bestCar = new Car(restoredBrain);
1171
  bestCar.isBest = true;
1172
  newPopulation.push(bestCar);
1173
 
1174
- // Create variants from this model to fill population
1175
  for (let i = 1; i < populationSize; i++) {
1176
  const car = bestCar.clone();
1177
  car.brain.mutate(mutationRate);
1178
  newPopulation.push(car);
1179
  }
1180
 
1181
- // Replace population
1182
  cars = newPopulation;
1183
-
1184
- // Reset cars
1185
  cars.forEach(car => car.reset());
1186
 
1187
- // Create a celebratory confetti effect
1188
- createConfetti(50, canvas.width/2, canvas.height/2);
1189
-
1190
  return true;
1191
  }
1192
  } catch (error) {
@@ -1195,29 +1157,25 @@
1195
  return false;
1196
  }
1197
 
1198
- // Reduce maximum number of confetti particles
1199
  const MAX_CONFETTI = 300;
1200
  const confetti = [];
1201
 
1202
  function createConfetti(count, x, y) {
1203
- // Limit the number of particles to prevent performance issues
1204
  if (confetti.length > MAX_CONFETTI) {
1205
- // Remove older particles if we exceed the limit
1206
  confetti.splice(0, count);
1207
  }
1208
-
1209
- const actualCount = Math.min(count, 50); // Limit particles per burst
1210
-
1211
  for (let i = 0; i < actualCount; i++) {
1212
  confetti.push({
1213
- x: x,
1214
- y: y,
1215
  size: 3 + Math.random() * 5,
1216
  color: `hsl(${Math.random() * 360}, 100%, 70%)`,
1217
  vx: -2 + Math.random() * 4,
1218
  vy: -3 - Math.random() * 2,
1219
  gravity: 0.1,
1220
- life: 1, // 1 = full life, 0 = dead
1221
  maxLife: 1 + Math.random()
1222
  });
1223
  }
@@ -1225,51 +1183,37 @@
1225
 
1226
  function updateConfetti(dt) {
1227
  for (let i = confetti.length - 1; i >= 0; i--) {
1228
- const particle = confetti[i];
1229
-
1230
- // Update position
1231
- particle.x += particle.vx * dt * 60;
1232
- particle.y += particle.vy * dt * 60;
1233
- particle.vy += particle.gravity * dt * 60;
1234
-
1235
- // Update life
1236
- particle.life -= 0.016 * dt * 60;
1237
-
1238
- // Remove dead particles
1239
- if (particle.life <= 0) {
1240
  confetti.splice(i, 1);
1241
  }
1242
  }
1243
  }
1244
 
1245
  function drawConfetti(ctx) {
1246
- for (const particle of confetti) {
1247
- ctx.fillStyle = particle.color;
1248
- ctx.globalAlpha = particle.life;
1249
- ctx.fillRect(
1250
- particle.x - particle.size/2,
1251
- particle.y - particle.size/2,
1252
- particle.size,
1253
- particle.size
1254
- );
1255
  }
1256
  ctx.globalAlpha = 1;
1257
  }
1258
 
1259
  function checkCourseCompletion() {
1260
  const bestCar = getBestCar();
1261
-
1262
- // Prevent function from firing multiple times for the same completion
1263
- if (bestCar && bestCar.checkpointIndex === track.checkpoints.length && !window.courseCompleted) {
1264
- // Set a flag to prevent multiple triggers
1265
  window.courseCompleted = true;
1266
 
1267
- // Launch a moderate amount of celebratory confetti
1268
- createConfetti(50, canvas.width/2, canvas.height/2);
1269
-
1270
- // Use setTimeout for additional confetti bursts to spread them out
1271
- setTimeout(() => createConfetti(25, canvas.width/4, canvas.height/2), 300);
1272
- setTimeout(() => createConfetti(25, 3*canvas.width/4, canvas.height/2), 600);
1273
 
1274
  // Show victory message
1275
  const message = document.createElement('div');
@@ -1307,67 +1251,51 @@
1307
 
1308
  document.body.appendChild(message);
1309
 
1310
- // Pause simulation
1311
  isRunning = false;
1312
  cancelAnimationFrame(animationId);
1313
 
1314
- // Event listener for the continue button
1315
  document.getElementById('continueBtn').addEventListener('click', () => {
1316
  document.body.removeChild(message);
1317
  isRunning = true;
1318
- window.courseCompleted = false; // Reset the completion flag
1319
  lastUpdateTime = performance.now();
1320
  animate();
1321
  });
1322
  }
1323
  }
1324
 
1325
- // ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ƒํƒœ
1326
  let cars = [];
1327
  let animationId;
1328
 
1329
- // ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ดˆ๊ธฐํ™”
1330
  function init() {
1331
- // ๋ฌด์ž‘์œ„ ํŠธ๋ž™ ์ƒ์„ฑ
1332
  track.generateRandomTrack();
1333
-
1334
- // ์ดˆ๊ธฐ ์ธ๊ตฌ ์ƒ์„ฑ
1335
  cars = [];
1336
  for (let i = 0; i < populationSize; i++) {
1337
  cars.push(new Car());
1338
  }
1339
-
1340
- // ํ†ต๊ณ„ ์ดˆ๊ธฐํ™”
1341
  generation = 0;
1342
  generationCount.textContent = generation;
1343
  populationCount.textContent = populationSize;
1344
 
1345
- // ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹œ์ž‘
1346
  isRunning = true;
1347
  lastUpdateTime = performance.now();
1348
- animate(); // Fix: Changed from init() to animate()
1349
  }
1350
 
1351
- // ๊ฒฐ๊ณผ ์—…๋ฐ์ดํŠธ ์†๋„ ์ œํ•œ (๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ํ•˜์ง€ ์•Š๊ณ  10ํ”„๋ ˆ์ž„๋งˆ๋‹ค ํ•œ ๋ฒˆ์”ฉ)
1352
  let updateFrameCount = 0;
1353
-
1354
- // ์ฃผ์š” ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฃจํ”„
1355
  function animate(currentTime = 0) {
1356
  if (!isRunning) return;
1357
-
1358
  animationId = requestAnimationFrame(animate);
1359
 
1360
- // ๋ธํƒ€ ํƒ€์ž„ ๊ณ„์‚ฐ
1361
- deltaTime = (currentTime - lastUpdateTime) / 1000; // ์ดˆ ๋‹จ์œ„
1362
  lastUpdateTime = currentTime;
1363
 
1364
- // ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์†๋„ ์ ์šฉ
1365
  deltaTime *= simulationSpeed;
1366
-
1367
- // ์ตœ๋Œ€ ๋ธํƒ€ ํƒ€์ž„ ์ œํ•œ (์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด)
1368
  deltaTime = Math.min(deltaTime, 0.2);
1369
 
1370
- // FPS ๊ณ„์‚ฐ (10ํ”„๋ ˆ์ž„๋งˆ๋‹ค ์—…๋ฐ์ดํŠธ)
1371
  frameCount++;
1372
  updateFrameCount++;
1373
  if (currentTime - lastFpsUpdate >= 1000) {
@@ -1380,13 +1308,13 @@
1380
  lastFpsUpdate = currentTime;
1381
  }
1382
 
1383
- // ์บ”๋ฒ„์Šค ์ง€์šฐ๊ธฐ
1384
  ctx.clearRect(0, 0, canvas.width, canvas.height);
1385
 
1386
- // ํŠธ๋ž™ ๊ทธ๋ฆฌ๊ธฐ
1387
  track.draw(ctx);
1388
 
1389
- // ์ž๋™์ฐจ ์—…๋ฐ์ดํŠธ ๋ฐ ๊ทธ๋ฆฌ๊ธฐ
1390
  let alive = 0;
1391
  cars.forEach(car => {
1392
  car.update(deltaTime);
@@ -1394,29 +1322,24 @@
1394
  if (!car.damaged) alive++;
1395
  });
1396
 
1397
- // Draw confetti particles
1398
  updateConfetti(deltaTime);
1399
  drawConfetti(ctx);
1400
 
1401
  aliveCount.textContent = alive;
1402
 
1403
- // ๋ชจ๋“  ์ž๋™์ฐจ๊ฐ€ ์†์ƒ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
1404
  if (alive === 0) {
1405
  nextGeneration();
1406
  }
1407
 
1408
- // ์ตœ๊ณ  ์ž๋™์ฐจ ๊ฐ•์กฐ ํ‘œ์‹œ ๋ฐ ์ฝ”์Šค ์™„๋ฃŒ ํ™•์ธ
1409
  const bestCar = getBestCar();
1410
  if (bestCar) {
1411
  bestCar.isBest = true;
1412
  bestCar.color = 'rgba(220, 38, 38, 0.9)';
1413
-
1414
- // Check if the best car has completed the course
1415
  checkCourseCompletion();
1416
  }
1417
  }
1418
 
1419
- // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
1420
  startBtn.addEventListener('click', () => {
1421
  if (!isRunning) {
1422
  isRunning = true;
@@ -1452,7 +1375,7 @@
1452
  }
1453
  });
1454
 
1455
- // ์Šฌ๋ผ์ด๋” ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
1456
  populationSlider.addEventListener('input', () => {
1457
  populationSize = parseInt(populationSlider.value);
1458
  populationValue.textContent = populationSize;
@@ -1469,9 +1392,9 @@
1469
  speedValue.textContent = `${simulationSpeed}x`;
1470
  });
1471
 
1472
- // ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ดˆ๊ธฐํ™” ๋ฐ ์‹œ์ž‘
1473
  init();
1474
  });
1475
  </script>
1476
  </body>
1477
- </html>
 
4
  <meta charset="UTF-8">
5
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>AI Driving Simulation (Circular Track)</title>
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" />
9
  <style>
10
  body {
 
263
  </head>
264
  <body>
265
  <div class="container">
266
+ <h1><i class="fas fa-car-side"></i> Evolution Simulation (Circular Track)</h1>
267
 
268
  <canvas id="simulationCanvas" width="800" height="500"></canvas>
269
 
 
329
  <div class="section-title">
330
  <i class="fas fa-info-circle"></i> <h3>About This Simulation</h3>
331
  </div>
332
+ <p>This simulation demonstrates how AI can learn to drive on a circular (looped) track using genetic algorithms and neural networks. Cars must navigate this randomly generated circuit without any prior knowledge of the environment.</p>
333
+ <p><strong>Key Features:</strong></p>
334
  <ul>
335
+ <li><i class="fas fa-brain"></i> <strong>Enhanced Neural Network:</strong> Using Sigmoid activation function</li>
336
+ <li><i class="fas fa-random"></i> <strong>Crossover:</strong> Combining best traits from top performers</li>
337
+ <li><i class="fas fa-chart-line"></i> <strong>Adaptive Mutation:</strong> Rate adjusts as generations progress</li>
338
+ <li><i class="fas fa-bolt"></i> <strong>Performance Optimization:</strong> Delta-time based updates</li>
339
+ <li><i class="fas fa-exclamation-triangle"></i> <strong>Improved Collision Detection:</strong> Polygon & line-segment based</li>
340
  <li><i class="fas fa-save"></i> <strong>Model Saving:</strong> Save and load your best models</li>
341
  </ul>
342
  </div>
 
344
 
345
  <script>
346
  document.addEventListener('DOMContentLoaded', () => {
347
+ // Add roundRect polyfill for browsers that don't support it (optional)
348
  if (!CanvasRenderingContext2D.prototype.roundRect) {
349
  CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius) {
350
  if (typeof radius === 'undefined') {
 
366
  };
367
  }
368
 
369
+ // Canvas setup
370
  const canvas = document.getElementById('simulationCanvas');
371
  const ctx = canvas.getContext('2d');
372
 
373
+ // UI elements
374
  const startBtn = document.getElementById('startBtn');
375
  const pauseBtn = document.getElementById('pauseBtn');
376
  const resetBtn = document.getElementById('resetBtn');
 
392
  const fpsCounter = document.getElementById('fpsCounter');
393
  const bestProgressBar = document.getElementById('bestProgressBar');
394
 
395
+ // Simulation parameters
396
  let populationSize = parseInt(populationSlider.value);
397
  let mutationRate = parseInt(mutationSlider.value) / 100;
398
+ let simulationSpeed = parseInt(speedSlider.value) * 3;
399
  let isRunning = false;
400
  let generation = 0;
401
  let fps = 0;
 
405
  let frameCount = 0;
406
  let lastFpsUpdate = 0;
407
 
408
+ // Activation function
409
  const sigmoid = (x) => 1 / (1 + Math.exp(-x));
 
410
 
411
+ // -- TRACK DEFINITION (MODIFIED FOR CIRCULAR CIRCUIT) --
412
  const track = {
413
+ walls: [], // Now each wall is a line segment: {x1, y1, x2, y2}
414
+ checkpoints: [],// Rectangular placeholders for checkpoint detection
415
  startPosition: { x: 100, y: 250, angle: 0 },
416
 
417
  generateRandomTrack() {
418
  this.walls = [];
419
  this.checkpoints = [];
420
 
421
+ // Center of the track
422
+ const cx = canvas.width / 2;
423
+ const cy = canvas.height / 2;
424
+
425
+ // Random outer/inner radius
426
+ const baseOuterRadius = 180 + Math.random() * 40;
427
+ const baseInnerRadius = baseOuterRadius - 40 - Math.random() * 10;
428
+
429
+ const segments = 36; // Number of line segments to approximate each ring
430
+ const outerPoints = [];
431
+ const innerPoints = [];
432
 
433
+ for (let i = 0; i < segments; i++) {
434
+ const angle = (2 * Math.PI * i) / segments;
 
 
 
435
 
436
+ // Optional small random offset for each point
437
+ const offsetOuter = Math.random() * 15 - 7.5;
438
+ const offsetInner = Math.random() * 10 - 5;
 
 
 
 
 
 
 
 
439
 
440
+ const rOuter = baseOuterRadius + offsetOuter;
441
+ const rInner = baseInnerRadius + offsetInner;
442
+
443
+ // Outer ring coordinates
444
+ const xOuter = cx + rOuter * Math.cos(angle);
445
+ const yOuter = cy + rOuter * Math.sin(angle);
446
+ outerPoints.push({ x: xOuter, y: yOuter });
447
+
448
+ // Inner ring coordinates
449
+ const xInner = cx + rInner * Math.cos(angle);
450
+ const yInner = cy + rInner * Math.sin(angle);
451
+ innerPoints.push({ x: xInner, y: yInner });
452
  }
453
 
454
+ // Create line segments for outer and inner rings
455
+ for (let i = 0; i < segments; i++) {
456
+ const nextIndex = (i + 1) % segments;
457
+
458
+ // Outer ring wall segment
459
+ this.walls.push({
460
+ x1: outerPoints[i].x,
461
+ y1: outerPoints[i].y,
462
+ x2: outerPoints[nextIndex].x,
463
+ y2: outerPoints[nextIndex].y
464
+ });
465
+
466
+ // Inner ring wall segment
467
+ this.walls.push({
468
+ x1: innerPoints[i].x,
469
+ y1: innerPoints[i].y,
470
+ x2: innerPoints[nextIndex].x,
471
+ y2: innerPoints[nextIndex].y
472
+ });
473
+ }
474
 
475
+ // Create checkpoints around the mid-radius
476
+ const midRadius = (baseOuterRadius + baseInnerRadius) / 2;
477
+ const checkpointCount = 5; // Number of checkpoints
478
  for (let i = 0; i < checkpointCount; i++) {
479
+ const angle = (2 * Math.PI * i) / checkpointCount;
480
+
481
+ const cxp = cx + midRadius * Math.cos(angle);
482
+ const cyp = cy + midRadius * Math.sin(angle);
483
+ const size = 24;
484
+
485
+ // Store checkpoint as a rectangle for bounding-box collision
486
  this.checkpoints.push({
487
+ x: cxp - size / 2,
488
+ y: cyp - size / 2,
489
+ width: size,
490
+ height: size
491
  });
492
  }
493
 
494
+ // Choose start position near the inner ring at angle=0 (to the right, for example)
495
+ // Adjust the angle so the car is tangent to the track
496
+ const startAngle = 0;
497
  this.startPosition = {
498
+ x: cx + (baseInnerRadius + 10) * Math.cos(startAngle),
499
+ y: cy + (baseInnerRadius + 10) * Math.sin(startAngle),
500
+ angle: Math.PI / 2 // face "down" (adjust as needed)
501
  };
502
  },
503
 
504
  draw(ctx) {
505
+ // Draw outer & inner ring segments
506
+ ctx.strokeStyle = '#4a5568';
507
+ ctx.lineWidth = 3;
508
+
509
  this.walls.forEach(wall => {
510
+ ctx.beginPath();
511
+ ctx.moveTo(wall.x1, wall.y1);
512
+ ctx.lineTo(wall.x2, wall.y2);
513
+ ctx.stroke();
514
  });
515
 
516
+ // Draw checkpoints
517
  ctx.fillStyle = 'rgba(74, 222, 128, 0.3)';
518
+ ctx.strokeStyle = 'rgba(74, 222, 128, 0.7)';
519
+ ctx.lineWidth = 1.5;
520
  this.checkpoints.forEach((checkpoint, index) => {
521
+ // Draw filled box
522
  ctx.fillRect(checkpoint.x, checkpoint.y, checkpoint.width, checkpoint.height);
523
 
524
+ // Outline
525
+ ctx.strokeRect(checkpoint.x, checkpoint.y, checkpoint.width, checkpoint.height);
526
+
527
+ // Label the checkpoint number
528
  ctx.fillStyle = 'white';
529
  ctx.font = '12px Arial';
530
  ctx.textAlign = 'center';
531
  ctx.textBaseline = 'middle';
532
+ ctx.fillText((index + 1).toString(),
533
+ checkpoint.x + checkpoint.width / 2,
534
+ checkpoint.y + checkpoint.height / 2);
535
+ // Revert fill style for next checkpoint
536
  ctx.fillStyle = 'rgba(74, 222, 128, 0.3)';
537
  });
538
 
539
+ // Draw start area
540
  ctx.fillStyle = 'rgba(96, 165, 250, 0.5)';
541
+ ctx.beginPath();
542
+ // Just draw a small rectangle or circle around the start
543
+ ctx.arc(this.startPosition.x, this.startPosition.y, 10, 0, 2 * Math.PI);
544
+ ctx.fill();
545
 
 
546
  ctx.fillStyle = 'white';
547
+ ctx.font = '12px Arial';
548
  ctx.textAlign = 'center';
549
+ ctx.fillText("START", this.startPosition.x, this.startPosition.y - 15);
550
  }
551
  };
552
 
553
+ // Car class
554
  class Car {
555
  constructor(brain) {
556
  this.reset();
557
  this.brain = brain ? brain : new NeuralNetwork([5, 8, 2]);
558
  this.fitness = 0;
559
  this.checkpointIndex = 0;
560
+ this.sensors = [0, 0, 0, 0, 0];
561
  this.sensorAngles = [0, -Math.PI/4, Math.PI/4, -Math.PI/8, Math.PI/8];
562
  this.sensorLength = 100;
563
  this.color = 'rgba(59, 130, 246, 0.8)';
564
  this.isBest = false;
565
  this.lastPosition = { x: 0, y: 0 };
566
+ this.stuckTime = 0;
567
  }
568
 
569
  reset() {
 
571
  this.y = track.startPosition.y;
572
  this.angle = track.startPosition.angle;
573
  this.speed = 0;
574
+ this.maxSpeed = 10;
575
+ this.acceleration = 0.2;
576
+ this.rotationSpeed = 0.1;
577
  this.damaged = false;
578
  this.checkpointIndex = 0;
579
  this.fitness = 0;
 
584
  update(dt) {
585
  if (this.damaged) return;
586
 
 
587
  this.lastPosition = { x: this.x, y: this.y };
588
 
589
+ // Update sensors
590
  this.updateSensors();
591
 
592
+ // Neural net outputs
593
  const outputs = this.brain.predict(this.sensors);
594
 
595
+ // Steering
596
+ const steering = outputs[1] - outputs[0];
597
  this.angle += steering * this.rotationSpeed * dt;
598
 
599
+ // Move (constant throttle for simplicity)
600
  this.speed = this.maxSpeed;
601
  this.x += Math.sin(this.angle) * this.speed * dt;
602
  this.y -= Math.cos(this.angle) * this.speed * dt;
603
 
604
+ // Collision check
605
  this.checkCollisions();
606
 
607
+ // Checkpoints
608
  this.checkCheckpoints();
609
 
610
+ // Check if car is stuck
611
  const distance = Math.sqrt(
612
+ (this.x - this.lastPosition.x) ** 2 +
613
+ (this.y - this.lastPosition.y) ** 2
614
  );
615
 
616
  if (distance < 0.5 * dt) {
617
  this.stuckTime += dt;
618
+ if (this.stuckTime > 1.5) {
619
  this.damaged = true;
620
  }
621
  } else {
622
  this.stuckTime = 0;
623
+ this.fitness += distance;
 
 
624
  }
625
  }
626
 
627
  updateSensors() {
628
  this.sensors = this.sensorAngles.map(angle => {
629
  const sensorAngle = this.angle + angle;
630
+ const sensorEndX = this.x + Math.sin(sensorAngle) * this.sensorLength;
631
+ const sensorEndY = this.y - Math.cos(sensorAngle) * this.sensorLength;
632
 
633
  let minDistance = this.sensorLength;
634
 
635
+ // Check intersection with each wall segment
636
  for (const wall of track.walls) {
637
+ const intersection = this.lineSegmentIntersection(
638
  this.x, this.y, sensorEndX, sensorEndY,
639
+ wall.x1, wall.y1, wall.x2, wall.y2
640
  );
641
 
642
  if (intersection) {
643
+ const dist = Math.sqrt(
644
+ (intersection.x - this.x) ** 2 +
645
+ (intersection.y - this.y) ** 2
646
  );
647
+ if (dist < minDistance) {
648
+ minDistance = dist;
649
+ }
650
  }
651
  }
652
 
653
+ // Normalized distance
654
  return 1 - (minDistance / this.sensorLength);
655
  });
656
  }
657
 
658
+ lineSegmentIntersection(x1, y1, x2, y2, x3, y3, x4, y4) {
659
+ // Standard line-line intersection
660
+ const denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
661
+ if (denom === 0) return null; // Parallel
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
662
 
663
+ const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom;
664
+ const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom;
 
 
 
 
 
 
 
 
 
665
 
666
  if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
667
  return {
 
669
  y: y1 + ua * (y2 - y1)
670
  };
671
  }
 
672
  return null;
673
  }
674
 
675
  checkCollisions() {
676
+ // Car polygon (4 corners)
677
+ const corners = this.getCarCorners();
678
+
679
+ // Each side of the car is a line segment
680
+ const carEdges = [];
681
+ for (let i = 0; i < 4; i++) {
682
+ const nextIndex = (i + 1) % 4;
683
+ carEdges.push([ corners[i], corners[nextIndex] ]);
684
+ }
685
 
686
+ // Check each car edge vs each wall segment
687
  for (const wall of track.walls) {
688
+ for (const edge of carEdges) {
689
+ const intersect = this.lineSegmentIntersection(
690
+ edge[0].x, edge[0].y, edge[1].x, edge[1].y,
691
+ wall.x1, wall.y1, wall.x2, wall.y2
692
+ );
693
+ if (intersect) {
694
+ this.damaged = true;
695
+ return;
 
 
 
696
  }
697
  }
698
  }
699
 
700
+ // Also check if out of canvas bounds (optional)
701
  if (this.x < 0 || this.x > canvas.width || this.y < 0 || this.y > canvas.height) {
702
  this.damaged = true;
703
  }
704
  }
705
 
706
  getCarCorners() {
 
707
  const width = 12;
708
  const height = 20;
709
  const cornerOffsets = [
710
+ { x: -width/2, y: -height/2 }, // top-left
711
+ { x: width/2, y: -height/2 }, // top-right
712
+ { x: width/2, y: height/2 }, // bottom-right
713
+ { x: -width/2, y: height/2 } // bottom-left
714
  ];
715
 
716
  return cornerOffsets.map(offset => {
 
727
  if (this.checkpointIndex >= track.checkpoints.length) return;
728
 
729
  const checkpoint = track.checkpoints[this.checkpointIndex];
730
+ if (
731
+ this.x > checkpoint.x &&
732
+ this.x < checkpoint.x + checkpoint.width &&
733
+ this.y > checkpoint.y &&
734
+ this.y < checkpoint.y + checkpoint.height
735
+ ) {
736
  this.checkpointIndex++;
737
+ this.fitness += 1000;
738
 
739
+ // Update global best progress bar
740
  const progress = this.checkpointIndex / track.checkpoints.length;
741
  if (progress > bestCarProgress) {
742
  bestCarProgress = progress;
743
  bestProgressBar.style.width = `${progress * 100}%`;
744
+ createConfetti(10, this.x, this.y);
 
 
 
 
745
  }
746
  }
747
  }
 
753
  ctx.translate(this.x, this.y);
754
  ctx.rotate(this.angle);
755
 
756
+ // Draw the car
757
  if (this.isBest) {
758
+ // "Best" car in red
759
  ctx.fillStyle = 'rgba(220, 38, 38, 0.9)';
 
 
760
  ctx.fillRect(-6, -10, 12, 20);
761
 
762
  // Wheels
763
  ctx.fillStyle = '#000';
764
+ ctx.fillRect(-7, -8, 2, 4);
765
+ ctx.fillRect(5, -8, 2, 4);
766
+ ctx.fillRect(-7, 4, 2, 4);
767
+ ctx.fillRect(5, 4, 2, 4);
768
 
769
  // Windshield
770
  ctx.fillStyle = '#60a5fa';
771
  ctx.fillRect(-4, -8, 8, 6);
772
 
773
+ // Simple crown on top
774
  ctx.fillStyle = '#facc15';
775
  ctx.beginPath();
776
  ctx.moveTo(-3, -11);
 
781
  ctx.lineTo(-3, -10);
782
  ctx.fill();
783
  } else {
784
+ // Normal car
785
  ctx.fillStyle = this.color;
 
 
786
  ctx.fillRect(-6, -10, 12, 20);
787
 
788
+ // Wheels
789
  ctx.fillStyle = '#000';
790
+ ctx.fillRect(-7, -7, 2, 3);
791
+ ctx.fillRect(5, -7, 2, 3);
792
+ ctx.fillRect(-7, 4, 2, 3);
793
+ ctx.fillRect(5, 4, 2, 3);
794
 
795
+ // Windshield
796
  ctx.fillStyle = '#a3e0ff';
797
  ctx.fillRect(-4, -7, 8, 5);
798
  }
799
 
800
+ // If it's best car, draw sensors after restoring context
801
  if (this.isBest) {
802
+ ctx.restore();
803
 
804
+ // Draw sensor lines
805
  ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
806
  ctx.lineWidth = 1;
 
807
  this.sensorAngles.forEach((angle, i) => {
808
  const sensorAngle = this.angle + angle;
809
  const sensorValue = this.sensors[i];
810
+ const length = this.sensorLength * (1 - sensorValue);
811
 
812
+ const endX = this.x + Math.sin(sensorAngle) * length;
813
+ const endY = this.y - Math.cos(sensorAngle) * length;
814
 
815
  ctx.beginPath();
816
  ctx.moveTo(this.x, this.y);
817
  ctx.lineTo(endX, endY);
818
  ctx.stroke();
819
  });
820
+ return;
 
821
  }
822
 
823
  ctx.restore();
 
828
  }
829
  }
830
 
831
+ // Neural Network classes
832
  class NeuralNetwork {
833
  constructor(neuronCounts) {
834
  this.levels = [];
 
885
  }
886
 
887
  static crossover(parentA, parentB) {
 
888
  if (parentA.levels.length !== parentB.levels.length) {
889
+ console.error("Parent network structures differ!");
890
  return parentA.clone();
891
  }
892
 
 
899
 
900
  if (levelA.inputs.length !== levelB.inputs.length ||
901
  levelA.outputs.length !== levelB.outputs.length) {
902
+ console.error("Parent level structures differ!");
903
  return parentA.clone();
904
  }
905
 
906
  const childLevel = new Level(levelA.inputs.length, levelA.outputs.length);
907
 
908
+ // Single-point crossover for biases & weights
909
  const biasesSwitch = Math.floor(Math.random() * levelA.biases.length);
 
 
910
  for (let i = 0; i < childLevel.biases.length; i++) {
911
  childLevel.biases[i] = i < biasesSwitch
912
  ? levelA.biases[i]
913
  : levelB.biases[i];
914
  }
915
 
 
916
  for (let i = 0; i < childLevel.weights.length; i++) {
917
  const weightSwitch = Math.floor(Math.random() * levelA.weights[i].length);
 
918
  for (let j = 0; j < childLevel.weights[i].length; j++) {
919
  childLevel.weights[i][j] = j < weightSwitch
920
  ? levelA.weights[i][j]
 
968
  this.weights[i] = new Array(outputCount);
969
  }
970
 
971
+ Level.randomize(this);
972
  }
973
 
974
+ static randomize(level) {
975
  for (let i = 0; i < level.inputs.length; i++) {
976
  for (let j = 0; j < level.outputs.length; j++) {
977
  level.weights[i][j] = Math.random() * 2 - 1;
978
  }
979
  }
 
980
  for (let i = 0; i < level.biases.length; i++) {
981
  level.biases[i] = Math.random() * 2 - 1;
982
  }
983
  }
984
 
985
  static feedForward(givenInputs, level) {
 
986
  for (let i = 0; i < level.inputs.length; i++) {
987
  level.inputs[i] = givenInputs[i];
988
  }
989
 
 
990
  for (let i = 0; i < level.outputs.length; i++) {
991
  let sum = 0;
992
  for (let j = 0; j < level.inputs.length; j++) {
993
  sum += level.inputs[j] * level.weights[j][i];
994
  }
995
 
996
+ // Sigmoid
997
  level.outputs[i] = sigmoid(sum - level.biases[i]);
998
  }
999
 
 
1010
  }
1011
  }
1012
 
1013
+ // Genetic Algorithm
1014
  function nextGeneration() {
1015
  generation++;
1016
  generationCount.textContent = generation;
1017
 
1018
+ // Compute fitness
1019
  calculateFitness();
1020
 
1021
+ // Adaptive mutation rate
 
 
 
1022
  const progressRate = bestCarProgress;
1023
  const adaptedRate = mutationRate * (1 - progressRate * 0.5);
1024
+ mutationRate = Math.max(0.01, adaptedRate);
1025
  mutationValue.textContent = `${Math.round(mutationRate * 100)}%`;
1026
  mutationSlider.value = Math.round(mutationRate * 100);
1027
 
1028
+ // Create new population
1029
+ const newPopulation = [];
1030
+ const eliteCount = Math.max(1, Math.floor(populationSize * 0.05));
1031
  const eliteCars = getTopCars(eliteCount);
1032
 
1033
  for (const eliteCar of eliteCars) {
1034
+ eliteCar.isBest = (eliteCar === eliteCars[0]);
1035
  newPopulation.push(eliteCar.clone());
1036
  }
1037
 
 
1038
  while (newPopulation.length < populationSize) {
1039
  if (Math.random() < 0.7 && newPopulation.length + 1 < populationSize) {
1040
+ // Crossover
1041
  const parentA = selectParent();
1042
  const parentB = selectParent();
1043
  const child = new Car(NeuralNetwork.crossover(parentA.brain, parentB.brain));
 
 
1044
  child.brain.mutate(mutationRate);
1045
  newPopulation.push(child);
1046
  } else {
1047
+ // Mutation only
1048
  const parent = selectParent();
1049
  const child = parent.clone();
1050
  child.brain.mutate(mutationRate);
 
1052
  }
1053
  }
1054
 
 
1055
  cars = newPopulation;
 
 
1056
  cars.forEach(car => car.reset());
1057
 
 
1058
  bestCarProgress = 0;
1059
  bestProgressBar.style.width = '0%';
1060
  }
 
1064
  let max = 0;
1065
 
1066
  cars.forEach(car => {
 
1067
  car.fitness += car.checkpointIndex * 500;
 
1068
  sum += car.fitness;
1069
  if (car.fitness > max) max = car.fitness;
1070
  });
1071
 
 
1072
  cars.forEach(car => {
1073
  car.fitness = car.fitness / sum;
1074
  });
1075
 
 
1076
  maxFitness.textContent = Math.round(max);
1077
  }
1078
 
1079
  function getTopCars(count) {
1080
+ return [...cars].sort((a, b) => b.fitness - a.fitness).slice(0, count);
 
 
1081
  }
1082
 
1083
  function getBestCar() {
 
1090
  bestCar = cars[i];
1091
  }
1092
  }
 
1093
  return bestCar;
1094
  }
1095
 
1096
  function selectParent() {
1097
+ // Roulette wheel
1098
  let index = 0;
1099
  let r = Math.random();
 
1100
  while (r > 0 && index < cars.length) {
1101
  r -= cars[index].fitness;
1102
  index++;
1103
  }
 
1104
  index = Math.min(cars.length - 1, Math.max(0, index - 1));
1105
  return cars[index];
1106
  }
1107
 
1108
+ // Save/load model
1109
  function saveBestModel() {
1110
  const bestCar = getBestCar();
1111
  if (bestCar) {
 
1113
  const modelData = {
1114
  brain: bestCar.brain.toJSON(),
1115
  fitness: bestCar.fitness,
1116
+ generation,
1117
  timestamp: new Date().toISOString()
1118
  };
 
1119
  localStorage.setItem('bestCarModel', JSON.stringify(modelData));
1120
  return true;
1121
  } catch (error) {
 
1131
  const savedModel = localStorage.getItem('bestCarModel');
1132
  if (savedModel) {
1133
  const modelData = JSON.parse(savedModel);
1134
+ const restoredBrain = NeuralNetwork.fromJSON(modelData.brain);
1135
 
1136
+ // Create new population from loaded model
1137
  const newPopulation = [];
 
 
 
1138
  const bestCar = new Car(restoredBrain);
1139
  bestCar.isBest = true;
1140
  newPopulation.push(bestCar);
1141
 
 
1142
  for (let i = 1; i < populationSize; i++) {
1143
  const car = bestCar.clone();
1144
  car.brain.mutate(mutationRate);
1145
  newPopulation.push(car);
1146
  }
1147
 
 
1148
  cars = newPopulation;
 
 
1149
  cars.forEach(car => car.reset());
1150
 
1151
+ createConfetti(50, canvas.width / 2, canvas.height / 2);
 
 
1152
  return true;
1153
  }
1154
  } catch (error) {
 
1157
  return false;
1158
  }
1159
 
1160
+ // Confetti
1161
  const MAX_CONFETTI = 300;
1162
  const confetti = [];
1163
 
1164
  function createConfetti(count, x, y) {
 
1165
  if (confetti.length > MAX_CONFETTI) {
 
1166
  confetti.splice(0, count);
1167
  }
1168
+ const actualCount = Math.min(count, 50);
 
 
1169
  for (let i = 0; i < actualCount; i++) {
1170
  confetti.push({
1171
+ x,
1172
+ y,
1173
  size: 3 + Math.random() * 5,
1174
  color: `hsl(${Math.random() * 360}, 100%, 70%)`,
1175
  vx: -2 + Math.random() * 4,
1176
  vy: -3 - Math.random() * 2,
1177
  gravity: 0.1,
1178
+ life: 1,
1179
  maxLife: 1 + Math.random()
1180
  });
1181
  }
 
1183
 
1184
  function updateConfetti(dt) {
1185
  for (let i = confetti.length - 1; i >= 0; i--) {
1186
+ const p = confetti[i];
1187
+ p.x += p.vx * dt * 60;
1188
+ p.y += p.vy * dt * 60;
1189
+ p.vy += p.gravity * dt * 60;
1190
+ p.life -= 0.016 * dt * 60;
1191
+ if (p.life <= 0) {
 
 
 
 
 
 
1192
  confetti.splice(i, 1);
1193
  }
1194
  }
1195
  }
1196
 
1197
  function drawConfetti(ctx) {
1198
+ for (const p of confetti) {
1199
+ ctx.fillStyle = p.color;
1200
+ ctx.globalAlpha = p.life;
1201
+ ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
 
 
 
 
 
1202
  }
1203
  ctx.globalAlpha = 1;
1204
  }
1205
 
1206
  function checkCourseCompletion() {
1207
  const bestCar = getBestCar();
1208
+ if (bestCar &&
1209
+ bestCar.checkpointIndex === track.checkpoints.length &&
1210
+ !window.courseCompleted
1211
+ ) {
1212
  window.courseCompleted = true;
1213
 
1214
+ createConfetti(50, canvas.width / 2, canvas.height / 2);
1215
+ setTimeout(() => createConfetti(25, canvas.width / 4, canvas.height / 2), 300);
1216
+ setTimeout(() => createConfetti(25, 3 * canvas.width / 4, canvas.height / 2), 600);
 
 
 
1217
 
1218
  // Show victory message
1219
  const message = document.createElement('div');
 
1251
 
1252
  document.body.appendChild(message);
1253
 
 
1254
  isRunning = false;
1255
  cancelAnimationFrame(animationId);
1256
 
 
1257
  document.getElementById('continueBtn').addEventListener('click', () => {
1258
  document.body.removeChild(message);
1259
  isRunning = true;
1260
+ window.courseCompleted = false;
1261
  lastUpdateTime = performance.now();
1262
  animate();
1263
  });
1264
  }
1265
  }
1266
 
1267
+ // Simulation state
1268
  let cars = [];
1269
  let animationId;
1270
 
1271
+ // Initialize
1272
  function init() {
 
1273
  track.generateRandomTrack();
 
 
1274
  cars = [];
1275
  for (let i = 0; i < populationSize; i++) {
1276
  cars.push(new Car());
1277
  }
 
 
1278
  generation = 0;
1279
  generationCount.textContent = generation;
1280
  populationCount.textContent = populationSize;
1281
 
 
1282
  isRunning = true;
1283
  lastUpdateTime = performance.now();
1284
+ animate();
1285
  }
1286
 
 
1287
  let updateFrameCount = 0;
 
 
1288
  function animate(currentTime = 0) {
1289
  if (!isRunning) return;
 
1290
  animationId = requestAnimationFrame(animate);
1291
 
1292
+ deltaTime = (currentTime - lastUpdateTime) / 1000;
 
1293
  lastUpdateTime = currentTime;
1294
 
 
1295
  deltaTime *= simulationSpeed;
 
 
1296
  deltaTime = Math.min(deltaTime, 0.2);
1297
 
1298
+ // FPS
1299
  frameCount++;
1300
  updateFrameCount++;
1301
  if (currentTime - lastFpsUpdate >= 1000) {
 
1308
  lastFpsUpdate = currentTime;
1309
  }
1310
 
1311
+ // Clear
1312
  ctx.clearRect(0, 0, canvas.width, canvas.height);
1313
 
1314
+ // Draw track
1315
  track.draw(ctx);
1316
 
1317
+ // Update & draw cars
1318
  let alive = 0;
1319
  cars.forEach(car => {
1320
  car.update(deltaTime);
 
1322
  if (!car.damaged) alive++;
1323
  });
1324
 
 
1325
  updateConfetti(deltaTime);
1326
  drawConfetti(ctx);
1327
 
1328
  aliveCount.textContent = alive;
1329
 
 
1330
  if (alive === 0) {
1331
  nextGeneration();
1332
  }
1333
 
 
1334
  const bestCar = getBestCar();
1335
  if (bestCar) {
1336
  bestCar.isBest = true;
1337
  bestCar.color = 'rgba(220, 38, 38, 0.9)';
 
 
1338
  checkCourseCompletion();
1339
  }
1340
  }
1341
 
1342
+ // Buttons
1343
  startBtn.addEventListener('click', () => {
1344
  if (!isRunning) {
1345
  isRunning = true;
 
1375
  }
1376
  });
1377
 
1378
+ // Sliders
1379
  populationSlider.addEventListener('input', () => {
1380
  populationSize = parseInt(populationSlider.value);
1381
  populationValue.textContent = populationSize;
 
1392
  speedValue.textContent = `${simulationSpeed}x`;
1393
  });
1394
 
1395
+ // Start
1396
  init();
1397
  });
1398
  </script>
1399
  </body>
1400
+ </html>