/* =========================== GLOBAL & DEFAULT VARIABLES =========================== */ let snakeIdCounter = 0; let maxLengths = {}; let elapsedTime = 0; let canvasWidth, canvasHeight; let lastFrameTime = null; /* Updated defaults */ const DEFAULT_BRAIN_SIZE = 15; const DEFAULT_SNAKE_SPEED = 1; const DEFAULT_MAX_FOOD = 120; const DEFAULT_SPAWN_CHANCE = 3; const DEFAULT_FOOD_POINTS = 3; const DEFAULT_DROPPED_FOOD_POINTS = 10; const DEFAULT_SURVIVAL_BONUS = 2; const DEFAULT_BODY_LENGTH_INCREMENT = 10; const DEFAULT_THICKNESS_INCREMENT = 0.05; const DEFAULT_SNAKE_COUNT = 15; const DEFAULT_LENGTH_BONUS = 3; const DEFAULT_KILL_BONUS = 3; /* Evolution & Environment Defaults */ const DEFAULT_MUTATION_RATE = 0.1; const DEFAULT_CROSSOVER_BIAS = 0.5; const DEFAULT_FOOD_DECAY_TIME = 2000; const DEFAULT_RESPAWN_DELAY = 0; /* Newly added default constants for Turn Rate & Boost Feature */ const DEFAULT_TURN_RATE = 0.15; const DEFAULT_BOOST_COST = 1.0; const DEFAULT_BOOST_MULTIPLIER = 2.0; /* Live controls */ let CONTROL_BRAIN_SIZE = DEFAULT_BRAIN_SIZE; let CONTROL_SNAKE_SPEED = DEFAULT_SNAKE_SPEED; let CONTROL_MAX_FOOD = DEFAULT_MAX_FOOD; let CONTROL_SPAWN_CHANCE = DEFAULT_SPAWN_CHANCE / 100; let CONTROL_FOOD_POINTS = DEFAULT_FOOD_POINTS; let CONTROL_DROPPED_FOOD_POINTS = DEFAULT_DROPPED_FOOD_POINTS; let CONTROL_SURVIVAL_BONUS = DEFAULT_SURVIVAL_BONUS / 1000; let CONTROL_BODY_LENGTH_INCREMENT = DEFAULT_BODY_LENGTH_INCREMENT; let CONTROL_THICKNESS_INCREMENT = DEFAULT_THICKNESS_INCREMENT; let CONTROL_SNAKE_COUNT = DEFAULT_SNAKE_COUNT; let CONTROL_LENGTH_BONUS = DEFAULT_LENGTH_BONUS; let CONTROL_KILL_BONUS = DEFAULT_KILL_BONUS; let CONTROL_MUTATION_RATE = DEFAULT_MUTATION_RATE; let CONTROL_CROSSOVER_BIAS = DEFAULT_CROSSOVER_BIAS; let CONTROL_FOOD_DECAY_TIME = DEFAULT_FOOD_DECAY_TIME; let CONTROL_RESPAWN_DELAY = DEFAULT_RESPAWN_DELAY; /* Newly added live control variables for Turn Rate & Boost */ let CONTROL_TURN_RATE = DEFAULT_TURN_RATE; let CONTROL_BOOST_COST = DEFAULT_BOOST_COST; let CONTROL_BOOST_MULTIPLIER = DEFAULT_BOOST_MULTIPLIER; /* Random name generator arrays */ const CREATURE_WORDS = [ "Shadow", "Arcane", "Giant", "Flying", "Poison", "Cosmic", "Metal", "Lurking", "Frost", "Ghoulish", "Infernal", "Serpent", "Draconic", "Wraith", "Neon", "Psychedelic", "Electro", "Turbo", "Nova", "Cyborg", "Spooky", "Radiant", "Celestial", "Demonic", "Slimy", "Furry", "Scaled", "Venomous", "Quantum", "Crystal", "Armored", "Vampire", "Bio", "Cyber", "Warped", "Chaotic", "Magnetic", "Sonic", "Pulsing", "Dark", "Psycho", "Laser", "Molten", "Mystic", "Ghostly", "Titanic", "Ancient", "Cursed", // Playful/fun names "Bouncy", "Zany", "Wacky", "Funky", "Quirky", "Silly", "Peppy", "Jolly" ]; const OBJECT_WORDS = [ "Hydra", "Sphinx", "Slime", "Golem", "Dragon", "Beast", "Worm", "Wraith", "Zombie", "Ogre", "Witch", "Basilisk", "Wyvern", "Spider", "Phantom", "Fiend", "Demon", "Alien", "Parasite", "Robot", "Ghoul", "Tentacle", "Construct", "Crawler", "Serpent", "Locust", "Lizard", "Poltergeist", "Chimera", "Bug", "Scorpion", "Leech", "Wisp", "Mothman", "Revenant", "Medusa", "Dragonfly", "Amoeba", "Pixie", "Doppelganger", "Harpy", "Gargoyle", "Troglodyte", "Valkyrie", "Kelpie", "Manticore", "Cockatrice", "Homunculus", // Playful/fun names "Cupcake", "Noodle", "Banana", "Pickle", "Gizmo", "Biscuit", "Muffin", "Sparky" ]; function getRandomSnakeName(id) { const cWord = CREATURE_WORDS[Math.floor(Math.random() * CREATURE_WORDS.length)]; const oWord = OBJECT_WORDS[Math.floor(Math.random() * OBJECT_WORDS.length)]; return `${cWord}${oWord}#${id}`; } /* =========================== Canvas Setup =========================== */ const canvas = document.getElementById('simCanvas'); const ctx = canvas.getContext('2d'); function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; canvasWidth = canvas.width; canvasHeight = canvas.height; } resizeCanvas(); window.addEventListener('resize', () => { resizeCanvas(); updateLayout(); }); /* Utility */ function distance(x1, y1, x2, y2) { return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); } function angleDifference(a, b) { let diff = a - b; while (diff < -Math.PI) diff += 2 * Math.PI; while (diff > Math.PI) diff -= 2 * Math.PI; return diff; } function getRandomNeonColor() { const hue = Math.floor(Math.random() * 360); return `hsl(${hue}, 100%, 50%)`; } /* =========================== RESPONSIVE LAYOUT LOGIC =========================== */ let isMobileLayout = null; function updateLayout() { const width = window.innerWidth; const newIsMobile = (width < 1000); if (newIsMobile === isMobileLayout) return; isMobileLayout = newIsMobile; const tlPanel = document.getElementById('timeAndLeaderboardPanel'); const ctrlPanel = document.getElementById('controlPanel'); if (isMobileLayout) { // fix top & bottom for mobile tlPanel.style.position = 'fixed'; tlPanel.style.top = '20px'; tlPanel.style.left = '50%'; tlPanel.style.transform = 'translateX(-50%)'; ctrlPanel.style.position = 'fixed'; ctrlPanel.style.bottom = '20px'; ctrlPanel.style.left = '50%'; ctrlPanel.style.transform = 'translateX(-50%)'; removeDraggable(tlPanel); removeDraggable(ctrlPanel); // auto-minimize control panel, expand top panel const tlContent = tlPanel.querySelector('.content'); const tlMinimizeBtn = tlPanel.querySelector('.minimize'); if (tlContent && tlMinimizeBtn) { tlContent.style.display = 'block'; tlMinimizeBtn.textContent = '–'; } const ctrlContent = ctrlPanel.querySelector('.content'); const ctrlMinimizeBtn = ctrlPanel.querySelector('.minimize'); if (ctrlContent && ctrlMinimizeBtn) { ctrlContent.style.display = 'none'; ctrlMinimizeBtn.textContent = '+'; } } else { // desktop layout tlPanel.style.position = 'absolute'; tlPanel.style.top = '20px'; tlPanel.style.left = '50%'; tlPanel.style.transform = 'translateX(-50%)'; ctrlPanel.style.position = 'absolute'; ctrlPanel.style.bottom = '20px'; ctrlPanel.style.left = '50%'; ctrlPanel.style.transform = 'translateX(-50%)'; removeDraggable(tlPanel); removeDraggable(ctrlPanel); } } function removeDraggable(panel) { if (!panel) return; const header = panel.querySelector('.header'); if (!header) return; const clone = panel.cloneNode(true); panel.parentNode.replaceChild(clone, panel); attachMinimizeLogic(clone); attachTopLevelTabLogic(clone); attachRewardSubTabLogic(clone); // new calls for sub-tabs: attachSnakesSubTabLogic(clone); attachWorldSubTabLogic(clone); wireUIEvents(); } function attachMinimizeLogic(panel) { const miniBtn = panel.querySelector('.minimize'); if (!miniBtn) return; miniBtn.addEventListener('click', () => { const content = panel.querySelector('.content'); if (!content) return; const isClosed = (content.style.display === 'none'); if (isClosed) { content.style.display = 'block'; miniBtn.textContent = '–'; if (isMobileLayout) { // auto-minimize the other panel if mobile if (panel.id === 'controlPanel') { const otherPanel = document.getElementById('timeAndLeaderboardPanel'); const otherContent = otherPanel.querySelector('.content'); if (otherContent && otherContent.style.display !== 'none') { otherContent.style.display = 'none'; otherPanel.querySelector('.minimize').textContent = '+'; } } else { const otherPanel = document.getElementById('controlPanel'); const otherContent = otherPanel.querySelector('.content'); if (otherContent && otherContent.style.display !== 'none') { otherContent.style.display = 'none'; otherPanel.querySelector('.minimize').textContent = '+'; } } } } else { content.style.display = 'none'; miniBtn.textContent = '+'; } }); } /** * Top-level tab logic (Reward, Food, etc.) */ function attachTopLevelTabLogic(panel) { const topLevelBtns = panel.querySelectorAll('.top-level-tabs .tabBtn'); if (!topLevelBtns.length) return; topLevelBtns.forEach(btn => { btn.addEventListener('click', () => { const parent = btn.closest('.top-level-tabs'); const panelContent = parent.parentNode; const allTabBtns = parent.querySelectorAll('.tabBtn'); const allTabs = panelContent.querySelectorAll('.tab-content'); allTabBtns.forEach(b => b.classList.remove('active')); allTabs.forEach(t => t.classList.remove('active')); btn.classList.add('active'); const tabId = btn.getAttribute('data-tab'); const target = panelContent.querySelector('#' + tabId); if (target) target.classList.add('active'); // if "rewardTab," default sub-tab = "Points & Kills" if (tabId === 'rewardTab') { const rewardSubTabs = target.querySelectorAll('.reward-sub-content'); const subBtns = target.querySelectorAll('.reward-subtabs .tabBtn'); rewardSubTabs.forEach(s => s.classList.remove('active')); subBtns.forEach(s => s.classList.remove('active')); const firstSubBtn = target.querySelector('.reward-subtabs .tabBtn[data-subtab="rewardPointsSubTab"]'); const firstSubContent = target.querySelector('#rewardPointsSubTab'); if (firstSubBtn) firstSubBtn.classList.add('active'); if (firstSubContent) firstSubContent.classList.add('active'); } }); }); } /** * Sub-tab logic for the "Reward" panel */ function attachRewardSubTabLogic(panel) { const rewardTab = panel.querySelector('#rewardTab'); if (!rewardTab) return; const subTabButtons = rewardTab.querySelectorAll('.reward-subtabs .tabBtn'); subTabButtons.forEach(subBtn => { subBtn.addEventListener('click', () => { subTabButtons.forEach(b => b.classList.remove('active')); const allSubContents = rewardTab.querySelectorAll('.reward-sub-content'); allSubContents.forEach(s => s.classList.remove('active')); subBtn.classList.add('active'); const subId = subBtn.getAttribute('data-subtab'); const targetSub = rewardTab.querySelector('#' + subId); if (targetSub) targetSub.classList.add('active'); }); }); } /* NEW: Attach snakes sub-tab logic */ function attachSnakesSubTabLogic(panel) { const snakesTab = panel.querySelector('#snakesTab'); if (!snakesTab) return; const subTabButtons = snakesTab.querySelectorAll('.snakes-subtabs .tabBtn'); subTabButtons.forEach(subBtn => { subBtn.addEventListener('click', () => { subTabButtons.forEach(b => b.classList.remove('active')); const allSubContents = snakesTab.querySelectorAll('.snakes-sub-content'); allSubContents.forEach(s => s.classList.remove('active')); subBtn.classList.add('active'); const subId = subBtn.getAttribute('data-subtab'); const targetSub = snakesTab.querySelector('#' + subId); if (targetSub) { targetSub.classList.add('active'); } }); }); } /* NEW: Attach world sub-tab logic */ function attachWorldSubTabLogic(panel) { const worldTab = panel.querySelector('#worldTab'); if (!worldTab) return; const subTabButtons = worldTab.querySelectorAll('.world-subtabs .tabBtn'); subTabButtons.forEach(subBtn => { subBtn.addEventListener('click', () => { subTabButtons.forEach(b => b.classList.remove('active')); const allSubContents = worldTab.querySelectorAll('.world-sub-content'); allSubContents.forEach(s => s.classList.remove('active')); subBtn.classList.add('active'); const subId = subBtn.getAttribute('data-subtab'); const targetSub = worldTab.querySelector('#' + subId); if (targetSub) { targetSub.classList.add('active'); } }); }); } /* Wire up UI events (sliders, etc.) */ function wireUIEvents() { // EXACTLY as before, hooking each slider to the right variable and label const killBonusSlider = document.getElementById('killBonusSlider'); if (killBonusSlider) { killBonusSlider.oninput = e => { CONTROL_KILL_BONUS = parseFloat(e.target.value); updateControlValue('killBonusVal', e.target.value); }; } const snakeSpeedSlider = document.getElementById('snakeSpeedSlider'); if (snakeSpeedSlider) { snakeSpeedSlider.oninput = e => { CONTROL_SNAKE_SPEED = parseFloat(e.target.value); updateControlValue('snakeSpeedVal', e.target.value); }; } const brainSizeSlider = document.getElementById('brainSizeSlider'); if (brainSizeSlider) { brainSizeSlider.oninput = e => { const newVal = parseFloat(e.target.value); if (newVal > CONTROL_BRAIN_SIZE) CONTROL_BRAIN_SIZE = newVal; else e.target.value = CONTROL_BRAIN_SIZE; updateControlValue('brainSizeVal', e.target.value); }; } const survivalBonusSlider = document.getElementById('survivalBonusSlider'); if (survivalBonusSlider) { survivalBonusSlider.oninput = e => { CONTROL_SURVIVAL_BONUS = parseFloat(e.target.value) * 0.001; updateControlValue('survivalBonusVal', e.target.value); }; } const bodyLengthIncSlider = document.getElementById('bodyLengthIncSlider'); if (bodyLengthIncSlider) { bodyLengthIncSlider.oninput = e => { CONTROL_BODY_LENGTH_INCREMENT = parseFloat(e.target.value); updateControlValue('bodyLengthIncVal', e.target.value); }; } const thicknessIncSlider = document.getElementById('thicknessIncSlider'); if (thicknessIncSlider) { thicknessIncSlider.oninput = e => { CONTROL_THICKNESS_INCREMENT = parseFloat(e.target.value); updateControlValue('thicknessIncVal', e.target.value); }; } const maxFoodSlider = document.getElementById('maxFoodSlider'); if (maxFoodSlider) { maxFoodSlider.oninput = e => { CONTROL_MAX_FOOD = parseInt(e.target.value); updateControlValue('maxFoodVal', e.target.value); }; } const spawnChanceSlider = document.getElementById('spawnChanceSlider'); if (spawnChanceSlider) { spawnChanceSlider.oninput = e => { CONTROL_SPAWN_CHANCE = parseFloat(e.target.value) / 100; updateControlValue('spawnChanceVal', e.target.value); }; } const foodPointsSlider = document.getElementById('foodPointsSlider'); if (foodPointsSlider) { foodPointsSlider.oninput = e => { CONTROL_FOOD_POINTS = parseFloat(e.target.value); updateControlValue('foodPointsVal', e.target.value); }; } const droppedFoodPointsSlider = document.getElementById('droppedFoodPointsSlider'); if (droppedFoodPointsSlider) { droppedFoodPointsSlider.oninput = e => { CONTROL_DROPPED_FOOD_POINTS = parseFloat(e.target.value); updateControlValue('droppedFoodPointsVal', e.target.value); }; } const snakeCountSlider = document.getElementById('snakeCountSlider'); if (snakeCountSlider) { snakeCountSlider.oninput = e => { const val = parseInt(e.target.value); updateControlValue('snakeCountVal', val); adjustSnakeCount(val); }; } const lengthBonusSlider = document.getElementById('lengthBonusSlider'); if (lengthBonusSlider) { lengthBonusSlider.oninput = e => { CONTROL_LENGTH_BONUS = parseFloat(e.target.value); updateControlValue('lengthBonusVal', e.target.value); }; } const timeAccelSlider = document.getElementById('timeAccelSlider'); if (timeAccelSlider) { timeAccelSlider.oninput = e => { updateControlValue('timeAccelVal', e.target.value); }; } const mutationRateSlider = document.getElementById('mutationRateSlider'); if (mutationRateSlider) { mutationRateSlider.oninput = e => { CONTROL_MUTATION_RATE = parseFloat(e.target.value); updateControlValue('mutationRateVal', e.target.value); }; } const crossoverBiasSlider = document.getElementById('crossoverBiasSlider'); if (crossoverBiasSlider) { crossoverBiasSlider.oninput = e => { CONTROL_CROSSOVER_BIAS = parseFloat(e.target.value); updateControlValue('crossoverBiasVal', e.target.value); }; } const foodDecayTimeSlider = document.getElementById('foodDecayTimeSlider'); if (foodDecayTimeSlider) { foodDecayTimeSlider.oninput = e => { CONTROL_FOOD_DECAY_TIME = parseInt(e.target.value); updateControlValue('foodDecayTimeVal', e.target.value); }; } const respawnDelaySlider = document.getElementById('respawnDelaySlider'); if (respawnDelaySlider) { respawnDelaySlider.oninput = e => { CONTROL_RESPAWN_DELAY = parseFloat(e.target.value); updateControlValue('respawnDelayVal', e.target.value); }; } // Turn Rate const turnRateSlider = document.getElementById('turnRateSlider'); if (turnRateSlider) { turnRateSlider.oninput = e => { CONTROL_TURN_RATE = parseFloat(e.target.value); updateControlValue('turnRateVal', e.target.value); }; } // Boost Cost const boostCostSlider = document.getElementById('boostCostSlider'); if (boostCostSlider) { boostCostSlider.oninput = e => { CONTROL_BOOST_COST = parseFloat(e.target.value); updateControlValue('boostCostVal', e.target.value); }; } // Boost Multiplier const boostMultiplierSlider = document.getElementById('boostMultiplierSlider'); if (boostMultiplierSlider) { boostMultiplierSlider.oninput = e => { CONTROL_BOOST_MULTIPLIER = parseFloat(e.target.value); updateControlValue('boostMultiplierVal', e.target.value); }; } // Buttons const restoreBtn = document.getElementById('restoreDefaultsButton'); if (restoreBtn) restoreBtn.onclick = restoreDefaults; const resetBtn = document.getElementById('resetSimButton'); if (resetBtn) resetBtn.onclick = resetSimulation; const fastForwardBtn = document.getElementById('fastForwardButton'); if (fastForwardBtn) fastForwardBtn.onclick = fastForwardSimulation; } /* Adjust snake population */ function adjustSnakeCount(desiredCount) { if (!sim) return; const currentCount = sim.snakes.length; if (desiredCount < currentCount) { const diff = currentCount - desiredCount; // sort by fitness, remove the lowest sim.snakes.sort((a, b) => a.fitness - b.fitness); for (let i = 0; i < diff; i++) { sim.removeSnakeNoRespawn(0, true); } } else if (desiredCount > currentCount) { const diff = desiredCount - currentCount; for (let i = 0; i < diff; i++) { sim.spawnNewSnake(); } } } /* CONTROL FUNCTIONS */ function updateControlValue(labelId, val) { const el = document.getElementById(labelId); if (el) el.textContent = val; } function restoreDefaults() { CONTROL_BRAIN_SIZE = 15; CONTROL_SNAKE_SPEED = 3.5; CONTROL_MAX_FOOD = 120; CONTROL_SPAWN_CHANCE = 0.03; CONTROL_FOOD_POINTS = 3; CONTROL_DROPPED_FOOD_POINTS = 10; CONTROL_SURVIVAL_BONUS = 0.002; CONTROL_BODY_LENGTH_INCREMENT = 10; CONTROL_THICKNESS_INCREMENT = 0.05; CONTROL_SNAKE_COUNT = 30; CONTROL_LENGTH_BONUS = 1; CONTROL_KILL_BONUS = 0; CONTROL_MUTATION_RATE = 0.1; CONTROL_CROSSOVER_BIAS = 0.5; CONTROL_FOOD_DECAY_TIME = 2000; CONTROL_RESPAWN_DELAY = 0; CONTROL_TURN_RATE = DEFAULT_TURN_RATE; CONTROL_BOOST_COST = DEFAULT_BOOST_COST; CONTROL_BOOST_MULTIPLIER = DEFAULT_BOOST_MULTIPLIER; // Update all slider positions: document.getElementById('brainSizeSlider').value = 15; document.getElementById('snakeSpeedSlider').value = 3.5; document.getElementById('maxFoodSlider').value = 120; document.getElementById('spawnChanceSlider').value = 3; document.getElementById('foodPointsSlider').value = 3; document.getElementById('droppedFoodPointsSlider').value = 10; document.getElementById('survivalBonusSlider').value = 2; document.getElementById('bodyLengthIncSlider').value = 10; document.getElementById('thicknessIncSlider').value = 0.05; document.getElementById('snakeCountSlider').value = 30; document.getElementById('lengthBonusSlider').value = 1; document.getElementById('killBonusSlider').value = 0; document.getElementById('mutationRateSlider').value = 0.1; document.getElementById('crossoverBiasSlider').value = 0.5; document.getElementById('foodDecayTimeSlider').value = 2000; document.getElementById('respawnDelaySlider').value = 0; document.getElementById('timeAccelSlider').value = 0; document.getElementById('turnRateSlider').value = DEFAULT_TURN_RATE; document.getElementById('boostCostSlider').value = DEFAULT_BOOST_COST; document.getElementById('boostMultiplierSlider').value = DEFAULT_BOOST_MULTIPLIER; // Update all displayed labels: updateControlValue('brainSizeVal', 15); updateControlValue('snakeSpeedVal', 3.5); updateControlValue('maxFoodVal', 120); updateControlValue('spawnChanceVal', 3); updateControlValue('foodPointsVal', 3); updateControlValue('droppedFoodPointsVal', 10); updateControlValue('survivalBonusVal', 2); updateControlValue('bodyLengthIncVal', 10); updateControlValue('thicknessIncVal', 0.05); updateControlValue('snakeCountVal', 30); updateControlValue('lengthBonusVal', 1); updateControlValue('killBonusVal', 0); updateControlValue('mutationRateVal', 0.1); updateControlValue('crossoverBiasVal', 0.5); updateControlValue('foodDecayTimeVal', 2000); updateControlValue('respawnDelayVal', 0); updateControlValue('timeAccelVal', 0); updateControlValue('turnRateVal', DEFAULT_TURN_RATE); updateControlValue('boostCostVal', DEFAULT_BOOST_COST); updateControlValue('boostMultiplierVal', DEFAULT_BOOST_MULTIPLIER); } function resetSimulation() { snakeIdCounter = 0; maxLengths = {}; elapsedTime = 0; sim = new Simulation(); } function fastForwardSimulation() { if (!sim) return; const minutesToSkip = parseInt(document.getElementById('timeAccelSlider').value); updateControlValue('timeAccelVal', minutesToSkip); if (minutesToSkip > 0) { showLoadingOverlay(); setTimeout(() => { const framesToSim = minutesToSkip * 60; // e.g. 1 minute => 60 frames for (let i = 0; i < framesToSim; i++) { sim.update(1.0 / 60.0); } elapsedTime += minutesToSkip; hideLoadingOverlay(); }, 50); } } function showLoadingOverlay() { document.getElementById('loadingOverlay').style.display = 'flex'; } function hideLoadingOverlay() { document.getElementById('loadingOverlay').style.display = 'none'; } /* =========================== CLASS DEFINITIONS =========================== */ /** * Brain with two outputs (turn, boost). */ class Brain { constructor(size, existingConfig = null) { this.size = size; if (existingConfig) { this.config = existingConfig.slice(); } else { this.config = Array.from({ length: size }, () => Math.random() < 0.5); } this.fitness = 0; } decide(sensors) { let sumForTurning = 0; let sumForBoost = 0; const half = Math.floor(this.config.length / 2); // turning for (let i = 0; i < sensors.length; i++) { if (sensors[i]) sumForTurning++; } for (let i = 0; i < half; i++) { if (this.config[i]) sumForTurning++; } sumForTurning += Math.random(); const turnThreshold = (sensors.length + half) / 2; const turn = (sumForTurning > turnThreshold) ? 1 : -1; // boost for (let i = half; i < this.config.length; i++) { if (this.config[i]) sumForBoost++; } sumForBoost += Math.random(); const boostThreshold = (this.config.length - half) / 2; const boost = (sumForBoost > boostThreshold); return { turn, boost }; } mutate() { if (Math.random() < CONTROL_MUTATION_RATE) { const index = Math.floor(Math.random() * this.config.length); this.config[index] = !this.config[index]; } } static crossover(b1, b2) { const size = Math.max(b1.size, b2.size); const newBrain = new Brain(size); for (let i = 0; i < size; i++) { const bit1 = b1.config[i % b1.config.length]; const bit2 = b2.config[i % b2.config.length]; newBrain.config[i] = (Math.random() < CONTROL_CROSSOVER_BIAS) ? bit1 : bit2; } return newBrain; } } class Food { constructor(x, y, isDropped = false, worth = CONTROL_FOOD_POINTS) { this.x = x; this.y = y; this.radius = 3; this.color = '#FFF'; this.isDropped = isDropped; this.worth = worth; } update() { return true; } render(ctx) { ctx.fillStyle = this.color; ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fill(); } } class DroppedFood extends Food { constructor(x, y) { super(x, y, true, CONTROL_DROPPED_FOOD_POINTS); this.creationTime = Date.now(); this.lifespan = CONTROL_FOOD_DECAY_TIME; } update() { const age = Date.now() - this.creationTime; return age < this.lifespan; } render(ctx) { const age = Date.now() - this.creationTime; let alpha = 1.0; if (age < 1000) { alpha = 0.5 + 0.5 * Math.sin(performance.now() / 80); } else { const fadeFrac = 1 - (age - 1000) / 1000; alpha = fadeFrac; } ctx.save(); ctx.fillStyle = "#ff66ff"; ctx.shadowColor = "rgba(255,102,255,0.8)"; ctx.shadowBlur = 10; ctx.globalAlpha = alpha; ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } } class Snake { constructor(x, y, brain = null) { this.id = snakeIdCounter++; this.name = getRandomSnakeName(this.id); this.x = x; this.y = y; this.thickness = 4; this.radius = this.thickness * 0.6; this.speed = CONTROL_SNAKE_SPEED; this.direction = Math.random() * Math.PI * 2; this.frontColor = getRandomNeonColor(); this.backColor = getRandomNeonColor(); this.brain = brain || new Brain(CONTROL_BRAIN_SIZE); this.fitness = 0; // We'll store objects: { x, y, edgeJump?: boolean } this.body = [{ x: this.x, y: this.y }]; this.bodyLength = 50; this.isBoosting = false; } gatherSensors(allFood, allSnakes) { // Omitted for brevity return []; } update(dt, allFood, allSnakes) { // 1) Decide const decision = this.brain.decide(this.gatherSensors(allFood, allSnakes)); const turn = decision.turn; const wantBoost = decision.boost; // 2) Turning const sizeFactor = Math.max(1, this.bodyLength / 50); const turnAngle = CONTROL_TURN_RATE / sizeFactor; this.direction += turn * turnAngle; // 3) Boost if (wantBoost && this.bodyLength > 1) { this.isBoosting = true; } else { this.isBoosting = false; } if (this.isBoosting) { this.speed = CONTROL_SNAKE_SPEED * CONTROL_BOOST_MULTIPLIER; const lengthLost = CONTROL_BOOST_COST * dt; this.bodyLength -= lengthLost; const thicknessLost = lengthLost * CONTROL_THICKNESS_INCREMENT; this.thickness = Math.max(1, this.thickness - thicknessLost); if (this.bodyLength < 1) { this.bodyLength = 1; this.isBoosting = false; } this.radius = this.thickness * 0.6; } else { this.speed = CONTROL_SNAKE_SPEED; } // 4) Move const oldX = this.x; const oldY = this.y; this.x += this.speed * Math.cos(this.direction); this.y += this.speed * Math.sin(this.direction); // Check for wrapping let wrapped = false; if (this.x < 0) { this.x += canvasWidth; wrapped = true; } else if (this.x > canvasWidth) { this.x -= canvasWidth; wrapped = true; } if (this.y < 0) { this.y += canvasHeight; wrapped = true; } else if (this.y > canvasHeight) { this.y -= canvasHeight; wrapped = true; } // If we did wrap, push an edgeJump marker if (wrapped) { this.body.push({ x: NaN, y: NaN, edgeJump: true }); } this.body.push({ x: this.x, y: this.y }); while (this.body.length > Math.floor(this.bodyLength)) { this.body.shift(); } // 5) Mutate this.brain.mutate(); // 6) Survival bonus this.fitness += CONTROL_SURVIVAL_BONUS; // 7) Length bonus if (CONTROL_LENGTH_BONUS > 0) { this.fitness += (this.bodyLength * CONTROL_LENGTH_BONUS * 0.001); } // record best length if (!maxLengths[this.id]) { maxLengths[this.id] = { name: this.name, length: this.bodyLength, color: this.frontColor }; } else if (this.bodyLength > maxLengths[this.id].length) { maxLengths[this.id].length = this.bodyLength; } } render(ctx) { ctx.save(); if (this.isBoosting) { ctx.shadowBlur = 15; ctx.shadowColor = this.frontColor; } const halfIndex = Math.floor(this.body.length / 2); // front half ctx.strokeStyle = this.frontColor; ctx.lineWidth = this.thickness; ctx.beginPath(); ctx.moveTo(this.body[0].x, this.body[0].y); for (let i = 1; i < halfIndex; i++) { const seg = this.body[i]; if (seg.edgeJump) { // break the path ctx.stroke(); ctx.beginPath(); if (i + 1 < halfIndex) { ctx.moveTo(this.body[i + 1].x, this.body[i + 1].y); i++; } } else { ctx.lineTo(seg.x, seg.y); } } ctx.stroke(); // back half ctx.strokeStyle = this.backColor; ctx.beginPath(); ctx.moveTo(this.body[halfIndex].x, this.body[halfIndex].y); for (let i = halfIndex + 1; i < this.body.length; i++) { const seg = this.body[i]; if (seg.edgeJump) { ctx.stroke(); ctx.beginPath(); if (i + 1 < this.body.length) { ctx.moveTo(this.body[i + 1].x, this.body[i + 1].y); i++; } } else { ctx.lineTo(seg.x, seg.y); } } ctx.stroke(); // head ctx.fillStyle = this.frontColor; ctx.beginPath(); ctx.arc(this.x, this.y, this.radius * 1.3, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } } class Simulation { constructor() { this.snakes = []; this.food = []; this.deadSnakesBrains = []; this.maxFoodAllowed = CONTROL_MAX_FOOD; this.spawnFoodChance = CONTROL_SPAWN_CHANCE; // initial snakes for (let i = 0; i < CONTROL_SNAKE_COUNT; i++) { this.spawnNewSnake(); } // initial food for (let i = 0; i < 50; i++) { const x = Math.random() * canvasWidth; const y = Math.random() * canvasHeight; this.food.push(new Food(x, y, false, CONTROL_FOOD_POINTS)); } } spawnNewSnake() { let newBrain; if (this.deadSnakesBrains.length > 0) { const b1 = this.deadSnakesBrains[Math.floor(Math.random() * this.deadSnakesBrains.length)]; const b2 = b1; newBrain = Brain.crossover(b1, b2); } else { newBrain = new Brain(CONTROL_BRAIN_SIZE); } const x = Math.random() * canvasWidth; const y = Math.random() * canvasHeight; const newSnake = new Snake(x, y, newBrain); this.snakes.push(newSnake); maxLengths[newSnake.id] = { name: newSnake.name, length: newSnake.bodyLength, color: newSnake.frontColor }; } removeSnakeNoRespawn(index, dropBody = false) { const snake = this.snakes[index]; if (dropBody) { snake.body.forEach(seg => { if (!seg.edgeJump) { // only add actual segments as dropped food this.food.push(new DroppedFood(seg.x, seg.y)); } }); } this.snakes.splice(index, 1); } spawnFood() { const x = Math.random() * canvasWidth; const y = Math.random() * canvasHeight; this.food.push(new Food(x, y, false, CONTROL_FOOD_POINTS)); } killSnake(index) { const snake = this.snakes[index]; this.deadSnakesBrains.push(snake.brain); // drop the snake's body snake.body.forEach(seg => { if (!seg.edgeJump) { this.food.push(new DroppedFood(seg.x, seg.y)); } }); this.snakes.splice(index, 1); // respawn setTimeout(() => { let newBrain; if (this.deadSnakesBrains.length > 0) { const b1 = this.deadSnakesBrains[Math.floor(Math.random() * this.deadSnakesBrains.length)]; const b2 = b1; newBrain = Brain.crossover(b1, b2); } else { newBrain = new Brain(CONTROL_BRAIN_SIZE); } const x = Math.random() * canvasWidth; const y = Math.random() * canvasHeight; const newbie = new Snake(x, y, newBrain); this.snakes.push(newbie); maxLengths[newbie.id] = { name: newbie.name, length: newbie.bodyLength, color: newbie.frontColor }; }, CONTROL_RESPAWN_DELAY * 1000); } update(dt) { const clampedDt = Math.min(dt, 0.05); // spawn extra food if needed const normalFoodCount = this.food.filter(f => !f.isDropped).length; if (normalFoodCount < this.maxFoodAllowed / 2) { if (Math.random() < this.spawnFoodChance) { this.spawnFood(); } } // update snakes for (let i = this.snakes.length - 1; i >= 0; i--) { const snake = this.snakes[i]; snake.update(clampedDt, this.food, this.snakes); // check if eats food for (let j = this.food.length - 1; j >= 0; j--) { const f = this.food[j]; if (distance(snake.x, snake.y, f.x, f.y) < snake.radius + f.radius) { snake.bodyLength += CONTROL_BODY_LENGTH_INCREMENT; snake.thickness += CONTROL_THICKNESS_INCREMENT; snake.radius = snake.thickness * 0.6; snake.fitness += f.worth; this.food.splice(j, 1); if (snake.bodyLength > maxLengths[snake.id].length) { maxLengths[snake.id].length = snake.bodyLength; } } } // collisions with other snakes for (let j = 0; j < this.snakes.length; j++) { if (i === j) continue; const other = this.snakes[j]; for (let k = 1; k < other.body.length; k++) { const point = other.body[k]; if (point.edgeJump) continue; // skip jump markers if (distance(snake.x, snake.y, point.x, point.y) < snake.radius + 0.5) { if (CONTROL_KILL_BONUS > 0) { other.fitness += CONTROL_KILL_BONUS; } this.killSnake(i); break; } } } } // update dropped food for (let i = this.food.length - 1; i >= 0; i--) { if (typeof this.food[i].update === 'function') { const keep = this.food[i].update(); if (!keep) { this.food.splice(i, 1); } } } } render(ctx) { ctx.clearRect(0, 0, canvasWidth, canvasHeight); for (const f of this.food) { f.render(ctx); } for (const s of this.snakes) { s.render(ctx); } } } /* Leaderboards & Main Loop */ let sim = null; function updateLeaderboards() { if (!sim) return; // Current top 5 const currentLeaders = sim.snakes .slice() .sort((a, b) => b.bodyLength - a.bodyLength) .slice(0, 5); let currentList = "