hackstone/Evolution-Simulation
0
1<!DOCTYPE html>2<html lang="en">3<head>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 {11 background-color: #111827;12 color: #f3f4f6;13 font-family: Arial, sans-serif;14 line-height: 1.5;15 margin: 0;16 padding: 20px;17 }18 19 .container {20 max-width: 1200px;21 margin: 0 auto;22 }23 24 h1 {25 color: #60a5fa;26 text-align: center;27 margin-bottom: 20px;28 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;29 text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);30 letter-spacing: 1px;31 }32 33 canvas {34 background-color: #2d3748;35 border-radius: 12px;36 display: block;37 margin: 0 auto 20px;38 box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1);39 border: 2px solid #4a5568;40 }41 42 .controls {43 display: flex;44 gap: 10px;45 margin-bottom: 20px;46 justify-content: center;47 flex-wrap: wrap;48 }49 50 button {51 background-color: #3b82f6;52 color: white;53 border: none;54 padding: 10px 18px;55 border-radius: 8px;56 cursor: pointer;57 font-weight: bold;58 transition: all 0.2s ease;59 display: flex;60 align-items: center;61 gap: 5px;62 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);63 }64 65 button:hover {66 background-color: #2563eb;67 transform: translateY(-2px);68 box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);69 }70 71 button:active {72 transform: translateY(1px);73 }74 75 .start-btn {76 background-color: #10b981;77 }78 79 .start-btn:hover {80 background-color: #059669;81 }82 83 .pause-btn {84 background-color: #f59e0b;85 }86 87 .pause-btn:hover {88 background-color: #d97706;89 }90 91 .reset-btn {92 background-color: #ef4444;93 }94 95 .reset-btn:hover {96 background-color: #dc2626;97 }98 99 .stats {100 display: flex;101 justify-content: space-between;102 background-color: #1f2937;103 padding: 18px;104 border-radius: 12px;105 margin-bottom: 20px;106 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);107 flex-wrap: wrap;108 gap: 10px;109 border: 1px solid #374151;110 }111 112 .stat-item {113 text-align: center;114 background-color: #2d3748;115 padding: 10px 15px;116 border-radius: 8px;117 min-width: 90px;118 transition: all 0.3s ease;119 }120 121 .stat-item:hover {122 transform: translateY(-2px);123 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);124 }125 126 .stat-value {127 font-size: 1.4em;128 font-weight: bold;129 color: #60a5fa;130 margin-top: 5px;131 }132 133 .settings {134 background-color: #1f2937;135 padding: 20px;136 border-radius: 12px;137 margin-bottom: 20px;138 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);139 border: 1px solid #374151;140 }141 142 .slider-container {143 margin-bottom: 15px;144 background-color: #2d3748;145 padding: 12px;146 border-radius: 8px;147 }148 149 .slider-container label {150 display: block;151 margin-bottom: 8px;152 font-weight: 500;153 color: #d1d5db;154 }155 156 input[type="range"] {157 width: 100%;158 margin-bottom: 8px;159 height: 6px;160 -webkit-appearance: none;161 background: #4b5563;162 border-radius: 5px;163 outline: none;164 }165 166 input[type="range"]::-webkit-slider-thumb {167 -webkit-appearance: none;168 width: 18px;169 height: 18px;170 background: #3b82f6;171 border-radius: 50%;172 cursor: pointer;173 transition: background 0.2s;174 }175 176 input[type="range"]::-webkit-slider-thumb:hover {177 background: #2563eb;178 }179 180 .progress-container {181 background-color: #374151;182 height: 10px;183 border-radius: 5px;184 margin-top: 10px;185 overflow: hidden;186 box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);187 }188 189 .progress-bar {190 height: 100%;191 background: linear-gradient(90deg, #10b981, #3b82f6);192 width: 0;193 transition: width 0.3s;194 box-shadow: 0 0 5px rgba(16, 185, 129, 0.5);195 }196 197 /* Car emoji styling */198 .car-emoji {199 font-size: 1.2em;200 margin-right: 5px;201 }202 203 /* Added visual elements */204 .section-title {205 display: flex;206 align-items: center;207 margin-bottom: 15px;208 border-bottom: 1px solid #374151;209 padding-bottom: 8px;210 }211 212 .section-title i {213 margin-right: 8px;214 color: #60a5fa;215 }216 217 /* Custom checkbox */218 .toggle-switch {219 position: relative;220 display: inline-block;221 width: 50px;222 height: 24px;223 }224 225 .toggle-switch input {226 opacity: 0;227 width: 0;228 height: 0;229 }230 231 .toggle-slider {232 position: absolute;233 cursor: pointer;234 top: 0;235 left: 0;236 right: 0;237 bottom: 0;238 background-color: #4b5563;239 transition: .4s;240 border-radius: 24px;241 }242 243 .toggle-slider:before {244 position: absolute;245 content: "";246 height: 16px;247 width: 16px;248 left: 4px;249 bottom: 4px;250 background-color: white;251 transition: .4s;252 border-radius: 50%;253 }254 255 input:checked + .toggle-slider {256 background-color: #3b82f6;257 }258 259 input:checked + .toggle-slider:before {260 transform: translateX(26px);261 }262 </style>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 270 <div class="controls">271 <button id="startBtn" class="start-btn"><i class="fas fa-play"></i> Start</button>272 <button id="pauseBtn" class="pause-btn"><i class="fas fa-pause"></i> Pause</button>273 <button id="resetBtn" class="reset-btn"><i class="fas fa-sync-alt"></i> New Track</button>274 <button id="saveBtn"><i class="fas fa-save"></i> Save Model</button>275 <button id="loadBtn"><i class="fas fa-upload"></i> Load Model</button>276 </div>277 278 <div class="stats">279 <div class="stat-item">280 <div><i class="fas fa-dna"></i> Generation</div>281 <div id="generationCount" class="stat-value">0</div>282 </div>283 <div class="stat-item">284 <div><i class="fas fa-car"></i> Alive</div>285 <div class="stat-value"><span id="aliveCount">0</span>/<span id="populationCount">0</span></div>286 </div>287 <div class="stat-item">288 <div><i class="fas fa-trophy"></i> Best Fitness</div>289 <div id="maxFitness" class="stat-value">0</div>290 </div>291 <div class="stat-item">292 <div><i class="fas fa-tachometer-alt"></i> FPS</div>293 <div id="fpsCounter" class="stat-value">0</div>294 </div>295 </div>296 297 <div class="settings">298 <div class="section-title">299 <i class="fas fa-sliders-h"></i> <h3>Simulation Settings</h3>300 </div>301 302 <div class="slider-container">303 <label for="populationSlider"><i class="fas fa-users"></i> Population Size:</label>304 <input type="range" id="populationSlider" min="10" max="300" value="100">305 <span id="populationValue">100</span>306 </div>307 308 <div class="slider-container">309 <label for="mutationSlider"><i class="fas fa-random"></i> Mutation Rate:</label>310 <input type="range" id="mutationSlider" min="1" max="100" value="10">311 <span id="mutationValue">10%</span>312 </div>313 314 <div class="slider-container">315 <label for="speedSlider"><i class="fas fa-fast-forward"></i> Simulation Speed:</label>316 <input type="range" id="speedSlider" min="1" max="30" value="5">317 <span id="speedValue">15x</span>318 </div>319 320 <div class="slider-container">321 <label><i class="fas fa-flag-checkered"></i> Track Progress:</label>322 <div class="progress-container">323 <div id="bestProgressBar" class="progress-bar"></div>324 </div>325 </div>326 </div>327 328 <div class="settings">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>343 </div>344 345 <script>346 document.addEventListener('DOMContentLoaded', () => {347 // Add roundRect polyfill for browsers that don't support it348 if (!CanvasRenderingContext2D.prototype.roundRect) {349 CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius) {350 if (typeof radius === 'undefined') {351 radius = 5;352 }353 354 this.beginPath();355 this.moveTo(x + radius, y);356 this.lineTo(x + width - radius, y);357 this.arcTo(x + width, y, x + width, y + radius, radius);358 this.lineTo(x + width, y + height - radius);359 this.arcTo(x + width, y + height, x + width - radius, y + height, radius);360 this.lineTo(x + radius, y + height);361 this.arcTo(x, y + height, x, y + height - radius, radius);362 this.lineTo(x, y + radius);363 this.arcTo(x, y, x + radius, y, radius);364 this.closePath();365 return this;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');377 const saveBtn = document.getElementById('saveBtn');378 const loadBtn = document.getElementById('loadBtn');379 380 const populationSlider = document.getElementById('populationSlider');381 const mutationSlider = document.getElementById('mutationSlider');382 const speedSlider = document.getElementById('speedSlider');383 384 const populationValue = document.getElementById('populationValue');385 const mutationValue = document.getElementById('mutationValue');386 const speedValue = document.getElementById('speedValue');387 388 const generationCount = document.getElementById('generationCount');389 const aliveCount = document.getElementById('aliveCount');390 const populationCount = document.getElementById('populationCount');391 const maxFitness = document.getElementById('maxFitness');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;402 let bestCarProgress = 0;403 let deltaTime = 0;404 let lastUpdateTime = 0;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: checkpointSize478 });479 }480 481 // 시작 위치 설정 (항상 좌측, 수직 위치는 무작위)482 this.startPosition = {483 x: 100,484 y: 100 + Math.random() * 300,485 angle: 0486 };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 number502 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 flag518 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() {542 this.x = track.startPosition.x;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;552 this.stuckTime = 0;553 this.lastPosition = { x: this.x, y: this.y };554 }555 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.height615 );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 {666 x: x1 + ua * (x2 - x1),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 => {714 const rotatedX = offset.x * Math.cos(this.angle) - offset.y * Math.sin(this.angle);715 const rotatedY = offset.x * Math.sin(this.angle) + offset.y * Math.cos(this.angle);716 return {717 x: this.x + rotatedX,718 y: this.y + rotatedY719 };720 });721 }722 723 checkCheckpoints() {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 checkpoint731 732 // Update best progress visualization733 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 checkpoints739 if (this.checkpointIndex > 0) {740 createConfetti(10, this.x, this.y);741 }742 }743 }744 }745 746 draw(ctx) {747 if (this.damaged) return;748 749 ctx.save();750 ctx.translate(this.x, this.y);751 ctx.rotate(this.angle);752 753 // Draw car body - Enhanced car shape with emoji style754 if (this.isBest) {755 // Draw fancy car for the best performer756 ctx.fillStyle = 'rgba(220, 38, 38, 0.9)';757 758 // Main body - using rectangles instead of roundRect to avoid issues759 ctx.fillRect(-6, -10, 12, 20);760 761 // Wheels762 ctx.fillStyle = '#000';763 ctx.fillRect(-7, -8, 2, 4); // left front764 ctx.fillRect(5, -8, 2, 4); // right front765 ctx.fillRect(-7, 4, 2, 4); // left rear766 ctx.fillRect(5, 4, 2, 4); // right rear767 768 // Windshield769 ctx.fillStyle = '#60a5fa';770 ctx.fillRect(-4, -8, 8, 6);771 772 // Draw a small crown on top773 ctx.fillStyle = '#facc15';774 ctx.beginPath();775 ctx.moveTo(-3, -11);776 ctx.lineTo(-1, -13);777 ctx.lineTo(1, -11);778 ctx.lineTo(3, -13);779 ctx.lineTo(3, -10);780 ctx.lineTo(-3, -10);781 ctx.fill();782 } else {783 // Regular car784 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 front792 ctx.fillRect(5, -7, 2, 3); // right front793 ctx.fillRect(-7, 4, 2, 3); // left rear794 ctx.fillRect(5, 4, 2, 3); // right rear795 796 // Simple windshield797 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();827 }828 829 clone() {830 return new Car(this.brain.clone());831 }832 }833 834 // 신경망 클래스835 class NeuralNetwork {836 constructor(neuronCounts) {837 this.levels = [];838 for (let i = 0; i < neuronCounts.length - 1; i++) {839 this.levels.push(new Level(840 neuronCounts[i], neuronCounts[i + 1]841 ));842 }843 }844 845 predict(givenInputs) {846 let outputs = Level.feedForward(847 givenInputs, this.levels[0]848 );849 850 for (let i = 1; i < this.levels.length; i++) {851 outputs = Level.feedForward(852 outputs, this.levels[i]853 );854 }855 856 return outputs;857 }858 859 clone() {860 const clone = new NeuralNetwork([]);861 clone.levels = this.levels.map(level => level.clone());862 return clone;863 }864 865 mutate(rate) {866 for (const level of this.levels) {867 for (let i = 0; i < level.biases.length; i++) {868 if (Math.random() < rate) {869 level.biases[i] = lerp(870 level.biases[i],871 Math.random() * 2 - 1,872 0.5873 );874 }875 }876 for (let i = 0; i < level.weights.length; i++) {877 for (let j = 0; j < level.weights[i].length; j++) {878 if (Math.random() < rate) {879 level.weights[i][j] = lerp(880 level.weights[i][j],881 Math.random() * 2 - 1,882 0.5883 );884 }885 }886 }887 }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 897 const childNetwork = new NeuralNetwork([]);898 childNetwork.levels = [];899 900 for (let l = 0; l < parentA.levels.length; l++) {901 const levelA = parentA.levels[l];902 const levelB = parentB.levels[l];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] 929 : levelB.weights[i][j];930 }931 }932 933 childNetwork.levels.push(childLevel);934 }935 936 return childNetwork;937 }938 939 toJSON() {940 return {941 levels: this.levels.map(level => ({942 inputs: level.inputs,943 outputs: level.outputs,944 biases: level.biases,945 weights: level.weights946 }))947 };948 }949 950 static fromJSON(data) {951 const network = new NeuralNetwork([]);952 network.levels = data.levels.map(levelData => {953 const level = new Level(levelData.inputs.length, levelData.outputs.length);954 level.inputs = [...levelData.inputs];955 level.outputs = [...levelData.outputs];956 level.biases = [...levelData.biases];957 level.weights = levelData.weights.map(w => [...w]);958 return level;959 });960 return network;961 }962 }963 964 function lerp(a, b, t) {965 return a + (b - a) * t;966 }967 968 class Level {969 constructor(inputCount, outputCount) {970 this.inputs = new Array(inputCount);971 this.outputs = new Array(outputCount);972 this.biases = new Array(outputCount);973 this.weights = [];974 975 for (let i = 0; i < inputCount; i++) {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 1011 return level.outputs;1012 }1013 1014 clone() {1015 const clone = new Level(this.inputs.length, this.outputs.length);1016 clone.inputs = [...this.inputs];1017 clone.outputs = [...this.outputs];1018 clone.biases = [...this.biases];1019 clone.weights = this.weights.map(arr => [...arr]);1020 return clone;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);1067 newPopulation.push(child);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 }1081 1082 function calculateFitness() {1083 let sum = 0;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() {1110 let bestCar = cars[0];1111 let bestFitness = cars[0].fitness;1112 1113 for (let i = 1; i < cars.length; i++) {1114 if (cars[i].fitness > bestFitness) {1115 bestFitness = cars[i].fitness;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) {1141 try {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) {1152 console.error('Error saving model:', error);1153 return false;1154 }1155 }1156 return false;1157 }1158 1159 function loadModel() {1160 try {1161 const savedModel = localStorage.getItem('bestCarModel');1162 if (savedModel) {1163 const modelData = JSON.parse(savedModel);1164 1165 // Use saved model for new population1166 const newPopulation = [];1167 1168 // Create best car with restored brain1169 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 population1175 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 population1182 cars = newPopulation;1183 1184 // Reset cars1185 cars.forEach(car => car.reset());1186 1187 // Create a celebratory confetti effect1188 createConfetti(50, canvas.width/2, canvas.height/2);1189 1190 return true;1191 }1192 } catch (error) {1193 console.error('Error loading model:', error);1194 }1195 return false;1196 }1197 1198 // Reduce maximum number of confetti particles1199 const MAX_CONFETTI = 300;1200 const confetti = [];