Evolution-Simulation / index.html
openfree's picture
Update index.html
aa77edf verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Driving Simulation</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" />
<style>
body {
background-color: #111827;
color: #f3f4f6;
font-family: Arial, sans-serif;
line-height: 1.5;
margin: 0;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: #60a5fa;
text-align: center;
margin-bottom: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
letter-spacing: 1px;
}
canvas {
background-color: #2d3748;
border-radius: 12px;
display: block;
margin: 0 auto 20px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
border: 2px solid #4a5568;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
justify-content: center;
flex-wrap: wrap;
}
button {
background-color: #3b82f6;
color: white;
border: none;
padding: 10px 18px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 5px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
button:hover {
background-color: #2563eb;
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
button:active {
transform: translateY(1px);
}
.start-btn {
background-color: #10b981;
}
.start-btn:hover {
background-color: #059669;
}
.pause-btn {
background-color: #f59e0b;
}
.pause-btn:hover {
background-color: #d97706;
}
.reset-btn {
background-color: #ef4444;
}
.reset-btn:hover {
background-color: #dc2626;
}
.stats {
display: flex;
justify-content: space-between;
background-color: #1f2937;
padding: 18px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
flex-wrap: wrap;
gap: 10px;
border: 1px solid #374151;
}
.stat-item {
text-align: center;
background-color: #2d3748;
padding: 10px 15px;
border-radius: 8px;
min-width: 90px;
transition: all 0.3s ease;
}
.stat-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.stat-value {
font-size: 1.4em;
font-weight: bold;
color: #60a5fa;
margin-top: 5px;
}
.settings {
background-color: #1f2937;
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid #374151;
}
.slider-container {
margin-bottom: 15px;
background-color: #2d3748;
padding: 12px;
border-radius: 8px;
}
.slider-container label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #d1d5db;
}
input[type="range"] {
width: 100%;
margin-bottom: 8px;
height: 6px;
-webkit-appearance: none;
background: #4b5563;
border-radius: 5px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: #3b82f6;
border-radius: 50%;
cursor: pointer;
transition: background 0.2s;
}
input[type="range"]::-webkit-slider-thumb:hover {
background: #2563eb;
}
.progress-container {
background-color: #374151;
height: 10px;
border-radius: 5px;
margin-top: 10px;
overflow: hidden;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #10b981, #3b82f6);
width: 0;
transition: width 0.3s;
box-shadow: 0 0 5px rgba(16, 185, 129, 0.5);
}
/* Car emoji styling */
.car-emoji {
font-size: 1.2em;
margin-right: 5px;
}
/* Added visual elements */
.section-title {
display: flex;
align-items: center;
margin-bottom: 15px;
border-bottom: 1px solid #374151;
padding-bottom: 8px;
}
.section-title i {
margin-right: 8px;
color: #60a5fa;
}
/* Custom checkbox */
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #4b5563;
transition: .4s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: #3b82f6;
}
input:checked + .toggle-slider:before {
transform: translateX(26px);
}
</style>
</head>
<body>
<div class="container">
<h1><i class="fas fa-car-side"></i> Evolution Simulation</h1>
<canvas id="simulationCanvas" width="800" height="500"></canvas>
<div class="controls">
<button id="startBtn" class="start-btn"><i class="fas fa-play"></i> Start</button>
<button id="pauseBtn" class="pause-btn"><i class="fas fa-pause"></i> Pause</button>
<button id="resetBtn" class="reset-btn"><i class="fas fa-sync-alt"></i> New Track</button>
<button id="saveBtn"><i class="fas fa-save"></i> Save Model</button>
<button id="loadBtn"><i class="fas fa-upload"></i> Load Model</button>
</div>
<div class="stats">
<div class="stat-item">
<div><i class="fas fa-dna"></i> Generation</div>
<div id="generationCount" class="stat-value">0</div>
</div>
<div class="stat-item">
<div><i class="fas fa-car"></i> Alive</div>
<div class="stat-value"><span id="aliveCount">0</span>/<span id="populationCount">0</span></div>
</div>
<div class="stat-item">
<div><i class="fas fa-trophy"></i> Best Fitness</div>
<div id="maxFitness" class="stat-value">0</div>
</div>
<div class="stat-item">
<div><i class="fas fa-tachometer-alt"></i> FPS</div>
<div id="fpsCounter" class="stat-value">0</div>
</div>
</div>
<div class="settings">
<div class="section-title">
<i class="fas fa-sliders-h"></i> <h3>Simulation Settings</h3>
</div>
<div class="slider-container">
<label for="populationSlider"><i class="fas fa-users"></i> Population Size:</label>
<input type="range" id="populationSlider" min="10" max="300" value="100">
<span id="populationValue">100</span>
</div>
<div class="slider-container">
<label for="mutationSlider"><i class="fas fa-random"></i> Mutation Rate:</label>
<input type="range" id="mutationSlider" min="1" max="100" value="10">
<span id="mutationValue">10%</span>
</div>
<div class="slider-container">
<label for="speedSlider"><i class="fas fa-fast-forward"></i> Simulation Speed:</label>
<input type="range" id="speedSlider" min="1" max="30" value="5">
<span id="speedValue">15x</span>
</div>
<div class="slider-container">
<label><i class="fas fa-flag-checkered"></i> Track Progress:</label>
<div class="progress-container">
<div id="bestProgressBar" class="progress-bar"></div>
</div>
</div>
</div>
<div class="settings">
<div class="section-title">
<i class="fas fa-info-circle"></i> <h3>About This Simulation</h3>
</div>
<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>
<p><strong>Key Improvements:</strong></p>
<ul>
<li><i class="fas fa-brain"></i> <strong>Enhanced Neural Network:</strong> Using Sigmoid activation function for smoother decision making</li>
<li><i class="fas fa-random"></i> <strong>Crossover:</strong> Combining the best traits from parent models</li>
<li><i class="fas fa-chart-line"></i> <strong>Adaptive Mutation:</strong> Automatically adjusts as generations progress</li>
<li><i class="fas fa-bolt"></i> <strong>Performance Optimization:</strong> Delta-time based updates for consistent simulation</li>
<li><i class="fas fa-exclamation-triangle"></i> <strong>Improved Collision Detection:</strong> More accurate polygon-based detection</li>
<li><i class="fas fa-save"></i> <strong>Model Saving:</strong> Save and load your best models</li>
</ul>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Add roundRect polyfill for browsers that don't support it
if (!CanvasRenderingContext2D.prototype.roundRect) {
CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius) {
if (typeof radius === 'undefined') {
radius = 5;
}
this.beginPath();
this.moveTo(x + radius, y);
this.lineTo(x + width - radius, y);
this.arcTo(x + width, y, x + width, y + radius, radius);
this.lineTo(x + width, y + height - radius);
this.arcTo(x + width, y + height, x + width - radius, y + height, radius);
this.lineTo(x + radius, y + height);
this.arcTo(x, y + height, x, y + height - radius, radius);
this.lineTo(x, y + radius);
this.arcTo(x, y, x + radius, y, radius);
this.closePath();
return this;
};
}
// Canvas ์„ค์ •
const canvas = document.getElementById('simulationCanvas');
const ctx = canvas.getContext('2d');
// UI ์š”์†Œ
const startBtn = document.getElementById('startBtn');
const pauseBtn = document.getElementById('pauseBtn');
const resetBtn = document.getElementById('resetBtn');
const saveBtn = document.getElementById('saveBtn');
const loadBtn = document.getElementById('loadBtn');
const populationSlider = document.getElementById('populationSlider');
const mutationSlider = document.getElementById('mutationSlider');
const speedSlider = document.getElementById('speedSlider');
const populationValue = document.getElementById('populationValue');
const mutationValue = document.getElementById('mutationValue');
const speedValue = document.getElementById('speedValue');
const generationCount = document.getElementById('generationCount');
const aliveCount = document.getElementById('aliveCount');
const populationCount = document.getElementById('populationCount');
const maxFitness = document.getElementById('maxFitness');
const fpsCounter = document.getElementById('fpsCounter');
const bestProgressBar = document.getElementById('bestProgressBar');
// ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋งค๊ฐœ๋ณ€์ˆ˜
let populationSize = parseInt(populationSlider.value);
let mutationRate = parseInt(mutationSlider.value) / 100;
let simulationSpeed = parseInt(speedSlider.value) * 3; // 3๋ฐฐ ๋น ๋ฅธ ์†๋„
let isRunning = false;
let generation = 0;
let fps = 0;
let bestCarProgress = 0;
let deltaTime = 0;
let lastUpdateTime = 0;
let frameCount = 0;
let lastFpsUpdate = 0;
// ํ™œ์„ฑํ™” ํ•จ์ˆ˜
const sigmoid = (x) => 1 / (1 + Math.exp(-x));
const relu = (x) => Math.max(0, x);
// ํŠธ๋ž™ ์ •์˜
const track = {
walls: [],
checkpoints: [],
startPosition: { x: 100, y: 250, angle: 0 },
generateRandomTrack() {
this.walls = [];
this.checkpoints = [];
// ์™ธ๋ถ€ ๊ฒฝ๊ณ„ ๋ฒฝ (ํ•ญ์ƒ ์กด์žฌ)
this.walls.push(
{ x: 50, y: 50, width: 700, height: 20 }, // ์ƒ๋‹จ
{ x: 50, y: 50, width: 20, height: 400 }, // ์ขŒ์ธก
{ x: 50, y: 430, width: 700, height: 20 }, // ํ•˜๋‹จ
{ x: 730, y: 50, width: 20, height: 400 } // ์šฐ์ธก
);
// ๋ฌด์ž‘์œ„ ์žฅ์• ๋ฌผ ์ƒ์„ฑ
const obstacleCount = 3 + Math.floor(Math.random() * 6);
for (let i = 0; i < obstacleCount; i++) {
const isVertical = Math.random() > 0.5;
let x, y, width, height;
if (isVertical) {
width = 20;
height = 50 + Math.random() * 200;
x = 100 + Math.random() * 600;
y = 100 + Math.random() * (400 - height);
} else {
width = 50 + Math.random() * 200;
height = 20;
x = 100 + Math.random() * (700 - width);
y = 100 + Math.random() * 300;
}
// ์‹œ์ž‘ ์œ„์น˜๋ฅผ ๋ง‰์ง€ ์•Š๋„๋ก ํ™•์ธ
if (!(x < 150 && y < 300 && y + height > 200)) {
this.walls.push({ x, y, width, height });
}
}
// ์ฒดํฌํฌ์ธํŠธ ์ƒ์„ฑ
const checkpointCount = 3 + Math.floor(Math.random() * 3);
const checkpointSize = 30;
// ์žฅ์• ๋ฌผ ์ฃผ๋ณ€์„ ํ†ต๊ณผํ•ด์•ผ ํ•˜๋Š” ์œ„์น˜ ์ƒ์„ฑ
const possiblePositions = [
{ x: 700, y: 100 }, // ์šฐ์ธก ์ƒ๋‹จ
{ x: 600, y: 400 }, // ์šฐ์ธก ํ•˜๋‹จ ์ค‘์•™
{ x: 300, y: 400 }, // ํ•˜๋‹จ ์ค‘์•™
{ x: 100, y: 300 }, // ์ขŒ์ธก ์ค‘์•™
{ x: 400, y: 100 }, // ์ƒ๋‹จ ์ค‘์•™
{ x: 200, y: 200 }, // ์ขŒ์ธก ์ค‘์•™
{ x: 600, y: 200 } // ์šฐ์ธก ์ค‘์•™
];
// ์…”ํ”Œํ•˜๊ณ  ์ฒดํฌํฌ์ธํŠธ ์ˆ˜๋งŒํผ ์„ ํƒ
const shuffled = [...possiblePositions].sort(() => 0.5 - Math.random());
for (let i = 0; i < checkpointCount; i++) {
const pos = shuffled[i];
this.checkpoints.push({
x: pos.x,
y: pos.y,
width: checkpointSize,
height: checkpointSize
});
}
// ์‹œ์ž‘ ์œ„์น˜ ์„ค์ • (ํ•ญ์ƒ ์ขŒ์ธก, ์ˆ˜์ง ์œ„์น˜๋Š” ๋ฌด์ž‘์œ„)
this.startPosition = {
x: 100,
y: 100 + Math.random() * 300,
angle: 0
};
},
draw(ctx) {
// ๋ฒฝ ๊ทธ๋ฆฌ๊ธฐ
ctx.fillStyle = '#4a5568';
this.walls.forEach(wall => {
ctx.fillRect(wall.x, wall.y, wall.width, wall.height);
});
// ์ฒดํฌํฌ์ธํŠธ ๊ทธ๋ฆฌ๊ธฐ
ctx.fillStyle = 'rgba(74, 222, 128, 0.3)';
this.checkpoints.forEach((checkpoint, index) => {
ctx.fillRect(checkpoint.x, checkpoint.y, checkpoint.width, checkpoint.height);
// Add checkpoint number
ctx.fillStyle = 'white';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText((index + 1).toString(),
checkpoint.x + checkpoint.width/2,
checkpoint.y + checkpoint.height/2);
ctx.fillStyle = 'rgba(74, 222, 128, 0.3)';
});
// ์‹œ์ž‘ ์œ„์น˜ ๊ทธ๋ฆฌ๊ธฐ
ctx.fillStyle = 'rgba(96, 165, 250, 0.5)';
ctx.fillRect(this.startPosition.x - 15, this.startPosition.y - 25, 30, 50);
// Draw start flag
ctx.fillStyle = 'white';
ctx.font = '14px Arial';
ctx.textAlign = 'center';
ctx.fillText("START", this.startPosition.x, this.startPosition.y + 15);
}
};
// ์ž๋™์ฐจ ํด๋ž˜์Šค
class Car {
constructor(brain) {
this.reset();
this.brain = brain ? brain : new NeuralNetwork([5, 8, 2]);
this.fitness = 0;
this.checkpointIndex = 0;
this.sensors = [0, 0, 0, 0, 0]; // ์ „๋ฐฉ, ์ขŒ์ธก, ์šฐ์ธก, ์ขŒ์ „๋ฐฉ, ์šฐ์ „๋ฐฉ
this.sensorAngles = [0, -Math.PI/4, Math.PI/4, -Math.PI/8, Math.PI/8];
this.sensorLength = 100;
this.color = 'rgba(59, 130, 246, 0.8)';
this.isBest = false;
this.lastPosition = { x: 0, y: 0 };
this.stuckTime = 0; // ์ž๋™์ฐจ๊ฐ€ ์›€์ง์ด์ง€ ์•Š๋Š” ์‹œ๊ฐ„ ์ถ”์ 
}
reset() {
this.x = track.startPosition.x;
this.y = track.startPosition.y;
this.angle = track.startPosition.angle;
this.speed = 0;
this.maxSpeed = 10; // ์ตœ๋Œ€ ์†๋„ ์ฆ๊ฐ€
this.acceleration = 0.2; // ๊ฐ€์†๋„ ์ฆ๊ฐ€
this.rotationSpeed = 0.1; // ํšŒ์ „ ์†๋„ ์ฆ๊ฐ€
this.damaged = false;
this.checkpointIndex = 0;
this.fitness = 0;
this.stuckTime = 0;
this.lastPosition = { x: this.x, y: this.y };
}
update(dt) {
if (this.damaged) return;
// ์ด์ „ ์œ„์น˜ ์ €์žฅ
this.lastPosition = { x: this.x, y: this.y };
// ์„ผ์„œ ์—…๋ฐ์ดํŠธ
this.updateSensors();
// ์‹ ๊ฒฝ๋ง ์ถœ๋ ฅ ๊ฐ€์ ธ์˜ค๊ธฐ
const outputs = this.brain.predict(this.sensors);
// ์กฐํ–ฅ ์ ์šฉ (outputs[0] = ์™ผ์ชฝ, outputs[1] = ์˜ค๋ฅธ์ชฝ)
const steering = outputs[1] - outputs[0]; // -1์—์„œ 1 ์‚ฌ์ด
this.angle += steering * this.rotationSpeed * dt;
// ์†๋„์™€ ์œ„์น˜ ์—…๋ฐ์ดํŠธ
this.speed = this.maxSpeed;
this.x += Math.sin(this.angle) * this.speed * dt;
this.y -= Math.cos(this.angle) * this.speed * dt;
// ์ถฉ๋Œ ๊ฒ€์‚ฌ
this.checkCollisions();
// ์ฒดํฌํฌ์ธํŠธ ๊ฒ€์‚ฌ
this.checkCheckpoints();
// ์ •์ง€ ํ™•์ธ (์ž๋™์ฐจ๊ฐ€ ์›€์ง์ด์ง€ ์•Š๋Š” ๊ฒฝ์šฐ)
const distance = Math.sqrt(
Math.pow(this.x - this.lastPosition.x, 2) +
Math.pow(this.y - this.lastPosition.y, 2)
);
if (distance < 0.5 * dt) {
this.stuckTime += dt;
if (this.stuckTime > 1.5) { // ์ •์ง€ ํŒ๋‹จ ์‹œ๊ฐ„ ๋‹จ์ถ• (3์ดˆ โ†’ 1.5์ดˆ)
this.damaged = true;
}
} else {
this.stuckTime = 0;
// ์ ํ•ฉ๋„ ์—…๋ฐ์ดํŠธ
this.fitness += distance; // ์ด๋™ ๊ฑฐ๋ฆฌ์— ๋”ฐ๋ฅธ ์ ํ•ฉ๋„
}
}
updateSensors() {
this.sensors = this.sensorAngles.map(angle => {
const sensorAngle = this.angle + angle;
let sensorEndX = this.x + Math.sin(sensorAngle) * this.sensorLength;
let sensorEndY = this.y - Math.cos(sensorAngle) * this.sensorLength;
let minDistance = this.sensorLength;
// ๋ชจ๋“  ๋ฒฝ์— ๋Œ€ํ•ด ๊ฒ€์‚ฌ
for (const wall of track.walls) {
const intersection = this.lineRectIntersection(
this.x, this.y, sensorEndX, sensorEndY,
wall.x, wall.y, wall.width, wall.height
);
if (intersection) {
const distance = Math.sqrt(
Math.pow(intersection.x - this.x, 2) +
Math.pow(intersection.y - this.y, 2)
);
minDistance = Math.min(minDistance, distance);
}
}
// ์ •๊ทœํ™”๋œ ๊ฑฐ๋ฆฌ ๋ฐ˜ํ™˜ (0-1 ๋ฒ”์œ„; 1 = ์žฅ์• ๋ฌผ ์—†์Œ, 0 = ์ž๋™์ฐจ ๋ฐ”๋กœ ์•ž์— ์žฅ์• ๋ฌผ)
return 1 - (minDistance / this.sensorLength);
});
}
lineRectIntersection(x1, y1, x2, y2, rx, ry, rw, rh) {
// ์ง์„ ์ด ์ง์‚ฌ๊ฐํ˜•์˜ ๋ณ€๊ณผ ๊ต์ฐจํ•˜๋Š”์ง€ ํ™•์ธ
const left = this.lineLineIntersection(x1, y1, x2, y2, rx, ry, rx, ry + rh);
const right = this.lineLineIntersection(x1, y1, x2, y2, rx + rw, ry, rx + rw, ry + rh);
const top = this.lineLineIntersection(x1, y1, x2, y2, rx, ry, rx + rw, ry);
const bottom = this.lineLineIntersection(x1, y1, x2, y2, rx, ry + rh, rx + rw, ry + rh);
let closestIntersection = null;
let minDistance = Infinity;
[left, right, top, bottom].forEach(intersection => {
if (intersection) {
const distance = Math.sqrt(Math.pow(intersection.x - x1, 2) + Math.pow(intersection.y - y1, 2));
if (distance < minDistance) {
minDistance = distance;
closestIntersection = intersection;
}
}
});
return closestIntersection;
}
lineLineIntersection(x1, y1, x2, y2, x3, y3, x4, y4) {
// ๋‘ ์ง์„  ์‚ฌ์ด์˜ ๊ต์ฐจ์  ๊ณ„์‚ฐ
const denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
if (denominator === 0) return null; // ์ง์„ ์ด ํ‰ํ–‰
const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;
if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
return {
x: x1 + ua * (x2 - x1),
y: y1 + ua * (y2 - y1)
};
}
return null;
}
checkCollisions() {
// ๋ฒฝ๊ณผ์˜ ์ถฉ๋Œ ๊ฐ์ง€ (๊ฐœ์„ ๋œ ๊ณ„์‚ฐ)
// ์ž๋™์ฐจ๋ฅผ ๋‹ค๊ฐํ˜•์œผ๋กœ ํ‘œํ˜„ํ•˜์—ฌ ๋” ์ •ํ™•ํ•œ ์ถฉ๋Œ ๊ฐ์ง€
const carCorners = this.getCarCorners();
// ๋ชจ๋“  ๋ฒฝ์— ๋Œ€ํ•ด ๊ฒ€์‚ฌ
for (const wall of track.walls) {
// ๊ฐ„๋‹จํ•œ ์‚ฌ๊ฐํ˜• ์ถฉ๋Œ ํ™•์ธ (์ตœ์ ํ™”๋ฅผ ์œ„ํ•œ ์ฒซ ๋‹จ๊ณ„)
if (this.x > wall.x - 10 && this.x < wall.x + wall.width + 10 &&
this.y > wall.y - 10 && this.y < wall.y + wall.height + 10) {
// ์ž๋™์ฐจ ๊ผญ์ง€์ ์ด ๋ฒฝ ๋‚ด๋ถ€์— ์žˆ๋Š”์ง€ ํ™•์ธ
for (const corner of carCorners) {
if (corner.x > wall.x && corner.x < wall.x + wall.width &&
corner.y > wall.y && corner.y < wall.y + wall.height) {
this.damaged = true;
return;
}
}
}
}
// ๊ฒฝ๊ณ„ ํ™•์ธ
if (this.x < 0 || this.x > canvas.width || this.y < 0 || this.y > canvas.height) {
this.damaged = true;
}
}
getCarCorners() {
// ์ž๋™์ฐจ์˜ ๋„ค ๊ผญ์ง€์  ๊ณ„์‚ฐ
const width = 12;
const height = 20;
const cornerOffsets = [
{ x: -width/2, y: -height/2 }, // ์ขŒ์ƒ๋‹จ
{ x: width/2, y: -height/2 }, // ์šฐ์ƒ๋‹จ
{ x: width/2, y: height/2 }, // ์šฐํ•˜๋‹จ
{ x: -width/2, y: height/2 } // ์ขŒํ•˜๋‹จ
];
return cornerOffsets.map(offset => {
const rotatedX = offset.x * Math.cos(this.angle) - offset.y * Math.sin(this.angle);
const rotatedY = offset.x * Math.sin(this.angle) + offset.y * Math.cos(this.angle);
return {
x: this.x + rotatedX,
y: this.y + rotatedY
};
});
}
checkCheckpoints() {
if (this.checkpointIndex >= track.checkpoints.length) return;
const checkpoint = track.checkpoints[this.checkpointIndex];
if (this.x > checkpoint.x && this.x < checkpoint.x + checkpoint.width &&
this.y > checkpoint.y && this.y < checkpoint.y + checkpoint.height) {
this.checkpointIndex++;
this.fitness += 1000; // Bonus for reaching checkpoint
// Update best progress visualization
const progress = this.checkpointIndex / track.checkpoints.length;
if (progress > bestCarProgress) {
bestCarProgress = progress;
bestProgressBar.style.width = `${progress * 100}%`;
// Add confetti effect for completed checkpoints
if (this.checkpointIndex > 0) {
createConfetti(10, this.x, this.y);
}
}
}
}
draw(ctx) {
if (this.damaged) return;
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
// Draw car body - Enhanced car shape with emoji style
if (this.isBest) {
// Draw fancy car for the best performer
ctx.fillStyle = 'rgba(220, 38, 38, 0.9)';
// Main body - using rectangles instead of roundRect to avoid issues
ctx.fillRect(-6, -10, 12, 20);
// Wheels
ctx.fillStyle = '#000';
ctx.fillRect(-7, -8, 2, 4); // left front
ctx.fillRect(5, -8, 2, 4); // right front
ctx.fillRect(-7, 4, 2, 4); // left rear
ctx.fillRect(5, 4, 2, 4); // right rear
// Windshield
ctx.fillStyle = '#60a5fa';
ctx.fillRect(-4, -8, 8, 6);
// Draw a small crown on top
ctx.fillStyle = '#facc15';
ctx.beginPath();
ctx.moveTo(-3, -11);
ctx.lineTo(-1, -13);
ctx.lineTo(1, -11);
ctx.lineTo(3, -13);
ctx.lineTo(3, -10);
ctx.lineTo(-3, -10);
ctx.fill();
} else {
// Regular car
ctx.fillStyle = this.color;
// Main body - using rectangles
ctx.fillRect(-6, -10, 12, 20);
// Wheels (simple)
ctx.fillStyle = '#000';
ctx.fillRect(-7, -7, 2, 3); // left front
ctx.fillRect(5, -7, 2, 3); // right front
ctx.fillRect(-7, 4, 2, 3); // left rear
ctx.fillRect(5, 4, 2, 3); // right rear
// Simple windshield
ctx.fillStyle = '#a3e0ff';
ctx.fillRect(-4, -7, 8, 5);
}
// ์ตœ๊ณ  ์ž๋™์ฐจ์˜ ์„ผ์„œ ๊ทธ๋ฆฌ๊ธฐ
if (this.isBest) {
ctx.restore(); // ์ปจํ…์ŠคํŠธ ๋ณต์›
// ์„ผ์„œ ๋ ˆ์ด ๊ทธ๋ฆฌ๊ธฐ
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
ctx.lineWidth = 1;
this.sensorAngles.forEach((angle, i) => {
const sensorAngle = this.angle + angle;
const sensorValue = this.sensors[i];
const sensorLength = this.sensorLength * (1 - sensorValue);
const endX = this.x + Math.sin(sensorAngle) * sensorLength;
const endY = this.y - Math.cos(sensorAngle) * sensorLength;
ctx.beginPath();
ctx.moveTo(this.x, this.y);
ctx.lineTo(endX, endY);
ctx.stroke();
});
return; // ์ด๋ฏธ ctx.restore()๋ฅผ ํ˜ธ์ถœํ–ˆ์œผ๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ ์ข…๋ฃŒ
}
ctx.restore();
}
clone() {
return new Car(this.brain.clone());
}
}
// ์‹ ๊ฒฝ๋ง ํด๋ž˜์Šค
class NeuralNetwork {
constructor(neuronCounts) {
this.levels = [];
for (let i = 0; i < neuronCounts.length - 1; i++) {
this.levels.push(new Level(
neuronCounts[i], neuronCounts[i + 1]
));
}
}
predict(givenInputs) {
let outputs = Level.feedForward(
givenInputs, this.levels[0]
);
for (let i = 1; i < this.levels.length; i++) {
outputs = Level.feedForward(
outputs, this.levels[i]
);
}
return outputs;
}
clone() {
const clone = new NeuralNetwork([]);
clone.levels = this.levels.map(level => level.clone());
return clone;
}
mutate(rate) {
for (const level of this.levels) {
for (let i = 0; i < level.biases.length; i++) {
if (Math.random() < rate) {
level.biases[i] = lerp(
level.biases[i],
Math.random() * 2 - 1,
0.5
);
}
}
for (let i = 0; i < level.weights.length; i++) {
for (let j = 0; j < level.weights[i].length; j++) {
if (Math.random() < rate) {
level.weights[i][j] = lerp(
level.weights[i][j],
Math.random() * 2 - 1,
0.5
);
}
}
}
}
}
static crossover(parentA, parentB) {
// ๋‘ ๋ถ€๋ชจ ์‹ ๊ฒฝ๋ง์—์„œ ์ƒˆ ์‹ ๊ฒฝ๋ง ์ƒ์„ฑ
if (parentA.levels.length !== parentB.levels.length) {
console.error("๋ถ€๋ชจ ์‹ ๊ฒฝ๋ง ๊ตฌ์กฐ๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค!");
return parentA.clone();
}
const childNetwork = new NeuralNetwork([]);
childNetwork.levels = [];
for (let l = 0; l < parentA.levels.length; l++) {
const levelA = parentA.levels[l];
const levelB = parentB.levels[l];
if (levelA.inputs.length !== levelB.inputs.length ||
levelA.outputs.length !== levelB.outputs.length) {
console.error("๋ถ€๋ชจ ๋ ˆ๋ฒจ ๊ตฌ์กฐ๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค!");
return parentA.clone();
}
const childLevel = new Level(levelA.inputs.length, levelA.outputs.length);
// ๊ต์ฐจ์  ์„ ํƒ (๋‹จ์ผ์  ๊ต์ฐจ)
const biasesSwitch = Math.floor(Math.random() * levelA.biases.length);
// ๋ฐ”์ด์–ด์Šค ๊ต์ฐจ
for (let i = 0; i < childLevel.biases.length; i++) {
childLevel.biases[i] = i < biasesSwitch
? levelA.biases[i]
: levelB.biases[i];
}
// ๊ฐ€์ค‘์น˜ ๊ต์ฐจ
for (let i = 0; i < childLevel.weights.length; i++) {
const weightSwitch = Math.floor(Math.random() * levelA.weights[i].length);
for (let j = 0; j < childLevel.weights[i].length; j++) {
childLevel.weights[i][j] = j < weightSwitch
? levelA.weights[i][j]
: levelB.weights[i][j];
}
}
childNetwork.levels.push(childLevel);
}
return childNetwork;
}
toJSON() {
return {
levels: this.levels.map(level => ({
inputs: level.inputs,
outputs: level.outputs,
biases: level.biases,
weights: level.weights
}))
};
}
static fromJSON(data) {
const network = new NeuralNetwork([]);
network.levels = data.levels.map(levelData => {
const level = new Level(levelData.inputs.length, levelData.outputs.length);
level.inputs = [...levelData.inputs];
level.outputs = [...levelData.outputs];
level.biases = [...levelData.biases];
level.weights = levelData.weights.map(w => [...w]);
return level;
});
return network;
}
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
class Level {
constructor(inputCount, outputCount) {
this.inputs = new Array(inputCount);
this.outputs = new Array(outputCount);
this.biases = new Array(outputCount);
this.weights = [];
for (let i = 0; i < inputCount; i++) {
this.weights[i] = new Array(outputCount);
}
Level.#randomize(this);
}
static #randomize(level) {
for (let i = 0; i < level.inputs.length; i++) {
for (let j = 0; j < level.outputs.length; j++) {
level.weights[i][j] = Math.random() * 2 - 1;
}
}
for (let i = 0; i < level.biases.length; i++) {
level.biases[i] = Math.random() * 2 - 1;
}
}
static feedForward(givenInputs, level) {
// ์ž…๋ ฅ ๊ฐ’ ์„ค์ •
for (let i = 0; i < level.inputs.length; i++) {
level.inputs[i] = givenInputs[i];
}
// ๊ฐ ์ถœ๋ ฅ ๋‰ด๋Ÿฐ์— ๋Œ€ํ•ด ๊ฐ€์ค‘ ํ•ฉ๊ณ„ ๊ณ„์‚ฐ
for (let i = 0; i < level.outputs.length; i++) {
let sum = 0;
for (let j = 0; j < level.inputs.length; j++) {
sum += level.inputs[j] * level.weights[j][i];
}
// Sigmoid ํ™œ์„ฑํ™” ํ•จ์ˆ˜ ์ ์šฉ
level.outputs[i] = sigmoid(sum - level.biases[i]);
}
return level.outputs;
}
clone() {
const clone = new Level(this.inputs.length, this.outputs.length);
clone.inputs = [...this.inputs];
clone.outputs = [...this.outputs];
clone.biases = [...this.biases];
clone.weights = this.weights.map(arr => [...arr]);
return clone;
}
}
// ์œ ์ „ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ํ•จ์ˆ˜
function nextGeneration() {
generation++;
generationCount.textContent = generation;
// ์ ํ•ฉ๋„ ๊ณ„์‚ฐ
calculateFitness();
// ์ƒˆ ์ธ๊ตฌ ์ƒ์„ฑ
const newPopulation = [];
// ์ ์‘ํ˜• ๋Œ์—ฐ๋ณ€์ด์œจ ์ ์šฉ
const progressRate = bestCarProgress;
const adaptedRate = mutationRate * (1 - progressRate * 0.5);
mutationRate = Math.max(0.01, adaptedRate); // ์ตœ์†Œ 1%
mutationValue.textContent = `${Math.round(mutationRate * 100)}%`;
mutationSlider.value = Math.round(mutationRate * 100);
// ์ด์ „ ์„ธ๋Œ€์—์„œ ์ตœ๊ณ  ์ž๋™์ฐจ ์ถ”๊ฐ€ (์—˜๋ฆฌํ‹ฐ์ฆ˜)
const eliteCount = Math.max(1, Math.floor(populationSize * 0.05)); // 5% ์—˜๋ฆฌํŠธ
const eliteCars = getTopCars(eliteCount);
for (const eliteCar of eliteCars) {
eliteCar.isBest = eliteCar === eliteCars[0];
newPopulation.push(eliteCar.clone());
}
// ๊ต์ฐจ์™€ ๋Œ์—ฐ๋ณ€์ด๋กœ ๋‚˜๋จธ์ง€ ์ฑ„์šฐ๊ธฐ
while (newPopulation.length < populationSize) {
if (Math.random() < 0.7 && newPopulation.length + 1 < populationSize) {
// ๊ต์ฐจ
const parentA = selectParent();
const parentB = selectParent();
const child = new Car(NeuralNetwork.crossover(parentA.brain, parentB.brain));
// ์ž์‹์—๊ฒŒ ์•ฝ๊ฐ„์˜ ๋Œ์—ฐ๋ณ€์ด ์ ์šฉ
child.brain.mutate(mutationRate);
newPopulation.push(child);
} else {
// ๋Œ์—ฐ๋ณ€์ด๋งŒ
const parent = selectParent();
const child = parent.clone();
child.brain.mutate(mutationRate);
newPopulation.push(child);
}
}
// ์ด์ „ ์ธ๊ตฌ ๊ต์ฒด
cars = newPopulation;
// ์ž๋™์ฐจ ์ดˆ๊ธฐํ™”
cars.forEach(car => car.reset());
// ์ง„ํ–‰๋ฅ  ์ดˆ๊ธฐํ™”
bestCarProgress = 0;
bestProgressBar.style.width = '0%';
}
function calculateFitness() {
let sum = 0;
let max = 0;
cars.forEach(car => {
// ์ฒดํฌํฌ์ธํŠธ ๋‹ฌ์„ฑ ๋ณด๋„ˆ์Šค
car.fitness += car.checkpointIndex * 500;
sum += car.fitness;
if (car.fitness > max) max = car.fitness;
});
// ์ ํ•ฉ๋„ ์ •๊ทœํ™”
cars.forEach(car => {
car.fitness = car.fitness / sum;
});
// UI ์—…๋ฐ์ดํŠธ
maxFitness.textContent = Math.round(max);
}
function getTopCars(count) {
return [...cars]
.sort((a, b) => b.fitness - a.fitness)
.slice(0, count);
}
function getBestCar() {
let bestCar = cars[0];
let bestFitness = cars[0].fitness;
for (let i = 1; i < cars.length; i++) {
if (cars[i].fitness > bestFitness) {
bestFitness = cars[i].fitness;
bestCar = cars[i];
}
}
return bestCar;
}
function selectParent() {
// ๋ฃฐ๋ › ํœ  ์„ ํƒ
let index = 0;
let r = Math.random();
while (r > 0 && index < cars.length) {
r -= cars[index].fitness;
index++;
}
index = Math.min(cars.length - 1, Math.max(0, index - 1));
return cars[index];
}
// ๋ชจ๋ธ ์ €์žฅ/๋ถˆ๋Ÿฌ์˜ค๊ธฐ ํ•จ์ˆ˜
function saveBestModel() {
const bestCar = getBestCar();
if (bestCar) {
try {
const modelData = {
brain: bestCar.brain.toJSON(),
fitness: bestCar.fitness,
generation: generation,
timestamp: new Date().toISOString()
};
localStorage.setItem('bestCarModel', JSON.stringify(modelData));
return true;
} catch (error) {
console.error('Error saving model:', error);
return false;
}
}
return false;
}
function loadModel() {
try {
const savedModel = localStorage.getItem('bestCarModel');
if (savedModel) {
const modelData = JSON.parse(savedModel);
// Use saved model for new population
const newPopulation = [];
// Create best car with restored brain
const restoredBrain = NeuralNetwork.fromJSON(modelData.brain);
const bestCar = new Car(restoredBrain);
bestCar.isBest = true;
newPopulation.push(bestCar);
// Create variants from this model to fill population
for (let i = 1; i < populationSize; i++) {
const car = bestCar.clone();
car.brain.mutate(mutationRate);
newPopulation.push(car);
}
// Replace population
cars = newPopulation;
// Reset cars
cars.forEach(car => car.reset());
// Create a celebratory confetti effect
createConfetti(50, canvas.width/2, canvas.height/2);
return true;
}
} catch (error) {
console.error('Error loading model:', error);
}
return false;
}
// Reduce maximum number of confetti particles
const MAX_CONFETTI = 300;
const confetti = [];
function createConfetti(count, x, y) {
// Limit the number of particles to prevent performance issues
if (confetti.length > MAX_CONFETTI) {
// Remove older particles if we exceed the limit
confetti.splice(0, count);
}
const actualCount = Math.min(count, 50); // Limit particles per burst
for (let i = 0; i < actualCount; i++) {
confetti.push({
x: x,
y: y,
size: 3 + Math.random() * 5,
color: `hsl(${Math.random() * 360}, 100%, 70%)`,
vx: -2 + Math.random() * 4,
vy: -3 - Math.random() * 2,
gravity: 0.1,
life: 1, // 1 = full life, 0 = dead
maxLife: 1 + Math.random()
});
}
}
function updateConfetti(dt) {
for (let i = confetti.length - 1; i >= 0; i--) {
const particle = confetti[i];
// Update position
particle.x += particle.vx * dt * 60;
particle.y += particle.vy * dt * 60;
particle.vy += particle.gravity * dt * 60;
// Update life
particle.life -= 0.016 * dt * 60;
// Remove dead particles
if (particle.life <= 0) {
confetti.splice(i, 1);
}
}
}
function drawConfetti(ctx) {
for (const particle of confetti) {
ctx.fillStyle = particle.color;
ctx.globalAlpha = particle.life;
ctx.fillRect(
particle.x - particle.size/2,
particle.y - particle.size/2,
particle.size,
particle.size
);
}
ctx.globalAlpha = 1;
}
function checkCourseCompletion() {
const bestCar = getBestCar();
// Prevent function from firing multiple times for the same completion
if (bestCar && bestCar.checkpointIndex === track.checkpoints.length && !window.courseCompleted) {
// Set a flag to prevent multiple triggers
window.courseCompleted = true;
// Launch a moderate amount of celebratory confetti
createConfetti(50, canvas.width/2, canvas.height/2);
// Use setTimeout for additional confetti bursts to spread them out
setTimeout(() => createConfetti(25, canvas.width/4, canvas.height/2), 300);
setTimeout(() => createConfetti(25, 3*canvas.width/4, canvas.height/2), 600);
// Show victory message
const message = document.createElement('div');
message.style.position = 'absolute';
message.style.top = '50%';
message.style.left = '50%';
message.style.transform = 'translate(-50%, -50%)';
message.style.background = 'rgba(16, 185, 129, 0.9)';
message.style.color = 'white';
message.style.padding = '20px';
message.style.borderRadius = '10px';
message.style.fontSize = '24px';
message.style.fontWeight = 'bold';
message.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
message.style.zIndex = '1000';
message.style.textAlign = 'center';
message.innerHTML = `
<div><i class="fas fa-trophy"></i> Course Completed! <i class="fas fa-trophy"></i></div>
<div style="font-size: 16px; margin-top: 10px;">
Generations: ${generation}<br>
Fitness: ${Math.round(bestCar.fitness * 1000)}
</div>
<button id="continueBtn" style="
background-color: white;
color: #10b981;
border: none;
padding: 8px 16px;
border-radius: 5px;
margin-top: 15px;
cursor: pointer;
font-weight: bold;">
Continue Training
</button>
`;
document.body.appendChild(message);
// Pause simulation
isRunning = false;
cancelAnimationFrame(animationId);
// Event listener for the continue button
document.getElementById('continueBtn').addEventListener('click', () => {
document.body.removeChild(message);
isRunning = true;
window.courseCompleted = false; // Reset the completion flag
lastUpdateTime = performance.now();
animate();
});
}
}
// ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ƒํƒœ
let cars = [];
let animationId;
// ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ดˆ๊ธฐํ™”
function init() {
// ๋ฌด์ž‘์œ„ ํŠธ๋ž™ ์ƒ์„ฑ
track.generateRandomTrack();
// ์ดˆ๊ธฐ ์ธ๊ตฌ ์ƒ์„ฑ
cars = [];
for (let i = 0; i < populationSize; i++) {
cars.push(new Car());
}
// ํ†ต๊ณ„ ์ดˆ๊ธฐํ™”
generation = 0;
generationCount.textContent = generation;
populationCount.textContent = populationSize;
// ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹œ์ž‘
isRunning = true;
lastUpdateTime = performance.now();
animate(); // Fix: Changed from init() to animate()
}
// ๊ฒฐ๊ณผ ์—…๋ฐ์ดํŠธ ์†๋„ ์ œํ•œ (๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ํ•˜์ง€ ์•Š๊ณ  10ํ”„๋ ˆ์ž„๋งˆ๋‹ค ํ•œ ๋ฒˆ์”ฉ)
let updateFrameCount = 0;
// ์ฃผ์š” ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฃจํ”„
function animate(currentTime = 0) {
if (!isRunning) return;
animationId = requestAnimationFrame(animate);
// ๋ธํƒ€ ํƒ€์ž„ ๊ณ„์‚ฐ
deltaTime = (currentTime - lastUpdateTime) / 1000; // ์ดˆ ๋‹จ์œ„
lastUpdateTime = currentTime;
// ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์†๋„ ์ ์šฉ
deltaTime *= simulationSpeed;
// ์ตœ๋Œ€ ๋ธํƒ€ ํƒ€์ž„ ์ œํ•œ (์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด)
deltaTime = Math.min(deltaTime, 0.2);
// FPS ๊ณ„์‚ฐ (10ํ”„๋ ˆ์ž„๋งˆ๋‹ค ์—…๋ฐ์ดํŠธ)
frameCount++;
updateFrameCount++;
if (currentTime - lastFpsUpdate >= 1000) {
fps = Math.round((frameCount * 1000) / (currentTime - lastFpsUpdate));
if (updateFrameCount >= 10) {
fpsCounter.textContent = fps;
updateFrameCount = 0;
}
frameCount = 0;
lastFpsUpdate = currentTime;
}
// ์บ”๋ฒ„์Šค ์ง€์šฐ๊ธฐ
ctx.clearRect(0, 0, canvas.width, canvas.height);
// ํŠธ๋ž™ ๊ทธ๋ฆฌ๊ธฐ
track.draw(ctx);
// ์ž๋™์ฐจ ์—…๋ฐ์ดํŠธ ๋ฐ ๊ทธ๋ฆฌ๊ธฐ
let alive = 0;
cars.forEach(car => {
car.update(deltaTime);
car.draw(ctx);
if (!car.damaged) alive++;
});
// Draw confetti particles
updateConfetti(deltaTime);
drawConfetti(ctx);
aliveCount.textContent = alive;
// ๋ชจ๋“  ์ž๋™์ฐจ๊ฐ€ ์†์ƒ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
if (alive === 0) {
nextGeneration();
}
// ์ตœ๊ณ  ์ž๋™์ฐจ ๊ฐ•์กฐ ํ‘œ์‹œ ๋ฐ ์ฝ”์Šค ์™„๋ฃŒ ํ™•์ธ
const bestCar = getBestCar();
if (bestCar) {
bestCar.isBest = true;
bestCar.color = 'rgba(220, 38, 38, 0.9)';
// Check if the best car has completed the course
checkCourseCompletion();
}
}
// ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
startBtn.addEventListener('click', () => {
if (!isRunning) {
isRunning = true;
lastUpdateTime = performance.now();
animate();
}
});
pauseBtn.addEventListener('click', () => {
isRunning = false;
cancelAnimationFrame(animationId);
});
resetBtn.addEventListener('click', () => {
isRunning = false;
cancelAnimationFrame(animationId);
init();
});
saveBtn.addEventListener('click', () => {
if (saveBestModel()) {
alert('Model saved successfully!');
} else {
alert('Error saving model');
}
});
loadBtn.addEventListener('click', () => {
if (loadModel()) {
alert('Model loaded successfully!');
} else {
alert('No saved model found or error loading model');
}
});
// ์Šฌ๋ผ์ด๋” ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
populationSlider.addEventListener('input', () => {
populationSize = parseInt(populationSlider.value);
populationValue.textContent = populationSize;
populationCount.textContent = populationSize;
});
mutationSlider.addEventListener('input', () => {
mutationRate = parseInt(mutationSlider.value) / 100;
mutationValue.textContent = `${parseInt(mutationSlider.value)}%`;
});
speedSlider.addEventListener('input', () => {
simulationSpeed = parseInt(speedSlider.value) * 3;
speedValue.textContent = `${simulationSpeed}x`;
});
// ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ดˆ๊ธฐํ™” ๋ฐ ์‹œ์ž‘
init();
});
</script>
</body>
</html>