Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Neuraxon Game of Life v2.0 (3d) - By David Vivancos & Dr. Jose Sanchez</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Consolas', 'Courier New', monospace; | |
| background: #000; | |
| color: #eee; | |
| overflow: hidden; | |
| } | |
| #gameCanvas { | |
| display: block; | |
| width: 100vw; | |
| height: 100vh; | |
| outline: none; | |
| } | |
| .splash-screen { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: #000 url('GameOfLife2.0ByDavidVivancosAndJoseSanchez.jpg') center center no-repeat; | |
| background-size: 100% 100%; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 1000; | |
| animation: fadeIn 0.5s; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| .splash-screen h1 { | |
| font-size: 48px; | |
| color: #28b46c; | |
| text-align: center; | |
| margin-bottom: 20px; | |
| text-shadow: 0 0 20px rgba(40, 180, 108, 0.5); | |
| } | |
| .splash-screen p { | |
| font-size: 18px; | |
| color: #aaa; | |
| text-align: center; | |
| margin: 10px 0; | |
| } | |
| .config-screen { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(15, 15, 18, 0.95); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 50px 20px; | |
| overflow-y: auto; | |
| z-index: 999; | |
| } | |
| .config-screen h1 { | |
| font-size: 32px; | |
| color: #ebebf0; | |
| margin-bottom: 40px; | |
| } | |
| .slider-container { | |
| width: 700px; | |
| max-width: 90%; | |
| margin-bottom: 25px; | |
| } | |
| .slider-label { | |
| font-size: 16px; | |
| color: #dcdcdc; | |
| margin-bottom: 8px; | |
| } | |
| .slider-wrapper { | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| } | |
| .slider { | |
| flex: 1; | |
| height: 6px; | |
| background: #646464; | |
| border-radius: 3px; | |
| outline: none; | |
| -webkit-appearance: none; | |
| } | |
| .slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| background: #c8c8c8; | |
| border: 2px solid #969696; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| } | |
| .slider-value { | |
| font-size: 16px; | |
| color: #ff0; | |
| min-width: 60px; | |
| text-align: right; | |
| } | |
| .start-button { | |
| margin-top: 30px; | |
| padding: 15px 40px; | |
| font-size: 18px; | |
| background: #23b45c; | |
| color: #fff; | |
| border: 2px solid #3cdc5a; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-family: 'Consolas', monospace; | |
| transition: all 0.3s; | |
| } | |
| .start-button:hover { | |
| background: #2cd068; | |
| transform: scale(1.05); | |
| } | |
| .hud { | |
| position: fixed; | |
| top: 12px; | |
| right: 16px; | |
| background: rgba(0, 0, 0, 0.75); | |
| border: 1px solid #3c3c3c; | |
| border-radius: 8px; | |
| padding: 15px; | |
| min-width: 120px; | |
| max-width: 240px; | |
| max-height: 90vh; | |
| overflow-y: auto; | |
| z-index: 500; | |
| } | |
| .hud-title { | |
| font-size: 20px; | |
| font-weight: bold; | |
| color: #e6e6f0; | |
| margin-bottom: 15px; | |
| text-align: center; | |
| } | |
| .hud-section { margin-bottom: 10px; } | |
| .hud-section-title { font-size: 12px; color: #b4b4b4; margin-bottom: 6px; font-weight: bold; } | |
| .hud-entry { | |
| font-size: 10px; | |
| color: #e6e6e6; | |
| margin-bottom: 4px; | |
| display: flex; | |
| justify-content: space-between; | |
| cursor: pointer; | |
| padding: 3px 5px; | |
| border-radius: 3px; | |
| transition: background 0.2s; | |
| } | |
| .hud-entry:hover { background: rgba(255, 255, 255, 0.2); } | |
| .hud-dot { | |
| display: inline-block; | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| margin-right: 8px; | |
| border: 1px solid rgba(0,0,0,0.3); | |
| } | |
| .hud-stats { font-size: 12px; color: #dcdcdc; margin-bottom: 0px; } | |
| .button-group { display: flex; flex-direction: column; gap: 4px; margin-top: 10px; } | |
| .button-row { display: flex; gap: 8px; } | |
| .hud-button { | |
| flex: 1; | |
| padding: 8px 12px; | |
| font-size: 13px; | |
| background: #232328; | |
| color: #e6e6e6; | |
| border: 1px solid #5a5a64; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-family: 'Consolas', monospace; | |
| transition: all 0.2s; | |
| } | |
| .hud-button:hover { background: #2d2d35; border-color: #7a7a84; } | |
| .hud-button:active { transform: scale(0.98); } | |
| .detail-panel { | |
| position: fixed; | |
| top: 12px; | |
| right: 360px; | |
| background: rgba(0, 0, 0, 0.75); | |
| border: 1px solid #505050; | |
| border-radius: 8px; | |
| padding: 15px; | |
| min-width: 320px; | |
| max-width: 400px; | |
| max-height: 90vh; | |
| overflow-y: auto; | |
| z-index: 500; | |
| } | |
| .detail-title { font-size: 18px; font-weight: bold; color: #e6e6f0; margin-bottom: 12px; } | |
| .detail-info { font-size: 13px; color: #dcdcdc; margin-bottom: 3px; } | |
| .detail-section { margin-top: 12px; padding-top: 12px; border-top: 1px solid #3c3c3c; } | |
| .modal { | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| background: #0f0f12; | |
| border: 2px solid #5a5a64; | |
| border-radius: 12px; | |
| padding: 30px; | |
| z-index: 1001; | |
| min-width: 400px; | |
| box-shadow: 0 10px 40px rgba(0,0,0,0.8); | |
| } | |
| .modal-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.6); | |
| z-index: 1000; | |
| } | |
| .modal-title { font-size: 20px; font-weight: bold; color: #ebebf0; margin-bottom: 10px; text-align: center; } | |
| .modal-subtitle { font-size: 14px; color: #dcdcdc; margin-bottom: 30px; text-align: center; } | |
| .modal-buttons { display: flex; gap: 20px; justify-content: center; } | |
| .modal-button { padding: 12px 40px; font-size: 16px; border-radius: 10px; cursor: pointer; font-family: 'Consolas', monospace; border: 2px solid; transition: all 0.3s; } | |
| .modal-button.yes { background: #232328; color: #ebebf0; border-color: #6e6e82; } | |
| .modal-button.yes:hover { background: #2d2d35; } | |
| .modal-button.no { background: #232328; color: #ebebf0; border-color: #6e6e82; } | |
| .modal-button.no:hover { background: #2d2d35; } | |
| .hidden { display: none ; } | |
| .controls-hint { | |
| position: fixed; | |
| bottom: 20px; | |
| left: 20px; | |
| background: rgba(0, 0, 0, 0.65); | |
| border: 1px solid #3c3c3c; | |
| border-radius: 8px; | |
| padding: 12px 20px; | |
| font-size: 12px; | |
| color: #aaa; | |
| pointer-events: auto; | |
| z-index: 500; | |
| } | |
| .controls-hint div { margin-bottom: 4px; } | |
| a:visited { color: white; } | |
| /* Loading spinner for 3D init */ | |
| #loader { | |
| position: absolute; | |
| top: 50%; left: 50%; | |
| transform: translate(-50%, -50%); | |
| color: white; | |
| font-size: 24px; | |
| z-index: 2000; | |
| display: none; | |
| } | |
| </style> | |
| <!-- Import Map for Three.js --> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://unpkg.com/[email protected]/build/three.module.js", | |
| "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/" | |
| } | |
| } | |
| </script> | |
| </head> | |
| <body> | |
| <div id="loader">Initializing 3D World...</div> | |
| <!-- Splash Screen --> | |
| <div id="splashScreen" class="splash-screen"> | |
| <p style="margin-top: auto; font-size: 14px; opacity: 0.7; text-align: center; padding-bottom: 20px;">Click anywhere to continue...</p> | |
| </div> | |
| <!-- Configuration Screen --> | |
| <div id="configScreen" class="config-screen hidden"> | |
| <h1>Neuraxon Game Of Life - World Configuration</h1> | |
| <div class="slider-container"> | |
| <div class="slider-label">World Size (3D NxN Grid Sphere)</div> | |
| <div class="slider-wrapper"> | |
| <input type="range" class="slider" id="worldSize" min="30" max="200" value="150"> | |
| <span class="slider-value" id="worldSizeValue">150</span> | |
| </div> | |
| </div> | |
| <div class="slider-container"> | |
| <div class="slider-label">Sea Percentage</div> | |
| <div class="slider-wrapper"> | |
| <input type="range" class="slider" id="seaPct" min="15" max="80" value="60"> | |
| <span class="slider-value" id="seaPctValue">60%</span> | |
| </div> | |
| </div> | |
| <div class="slider-container"> | |
| <div class="slider-label">Rock Percentage</div> | |
| <div class="slider-wrapper"> | |
| <input type="range" class="slider" id="rockPct" min="1" max="30" value="10"> | |
| <span class="slider-value" id="rockPctValue">5%</span> | |
| </div> | |
| </div> | |
| <div class="slider-container"> | |
| <div class="slider-label">Starting NxErs (initial population size)</div> | |
| <div class="slider-wrapper"> | |
| <input type="range" class="slider" id="startingNxErs" min="10" max="400" value="300"> | |
| <span class="slider-value" id="startingNxErsValue">300</span> | |
| </div> | |
| </div> | |
| <div class="slider-container"> | |
| <div class="slider-label">Maximum Possible Number of NxErs</div> | |
| <div class="slider-wrapper"> | |
| <input type="range" class="slider" id="maxNxErs" min="50" max="1000" value="500"> | |
| <span class="slider-value" id="maxNxErsValue">500</span> | |
| </div> | |
| </div> | |
| <div class="slider-container"> | |
| <div class="slider-label">Food Sources (Red Pyramids)</div> | |
| <div class="slider-wrapper"> | |
| <input type="range" class="slider" id="maxFood" min="50" max="1500" value="300"> | |
| <span class="slider-value" id="maxFoodValue">300</span> | |
| </div> | |
| </div> | |
| <div class="slider-container"> | |
| <div class="slider-label">Food Respawn (seconds to reappear)</div> | |
| <div class="slider-wrapper"> | |
| <input type="range" class="slider" id="foodRespawn" min="200" max="1000" value="300"> | |
| <span class="slider-value" id="foodRespawnValue">300</span> | |
| </div> | |
| </div> | |
| <div class="slider-container"> | |
| <div class="slider-label">Starting Food for each NxEr (Energy)</div> | |
| <div class="slider-wrapper"> | |
| <input type="range" class="slider" id="startFood" min="20" max="300" value="150"> | |
| <span class="slider-value" id="startFoodValue">150</span> | |
| </div> | |
| </div> | |
| <div class="slider-container"> | |
| <div class="slider-label">Max Neurons per NxEr</div> | |
| <div class="slider-wrapper"> | |
| <input type="range" class="slider" id="maxNeurons" min="5" max="35" value="20"> | |
| <span class="slider-value" id="maxNeuronsValue">20</span> | |
| </div> | |
| </div> | |
| <div class="slider-container"> | |
| <div class="slider-label">Global Time Steps (Mental Time Scale)</div> | |
| <div class="slider-wrapper"> | |
| <input type="range" class="slider" id="globalTimeSteps" min="30" max="120" value="60"> | |
| <span class="slider-value" id="globalTimeStepsValue">60</span> | |
| </div> | |
| </div> | |
| <div class="slider-container"> | |
| <div class="slider-label">Mate Cooldown (seconds between mating again)</div> | |
| <div class="slider-wrapper"> | |
| <input type="range" class="slider" id="mateCooldown" min="5" max="30" value="10"> | |
| <span class="slider-value" id="mateCooldownValue">10</span> | |
| </div> | |
| </div> | |
| <div class="slider-container" style="margin-top: 20px; background: #1a1a20; padding: 15px; border-radius: 8px; border: 1px solid #333;"> | |
| <label class="slider-label" style="display:flex; align-items:center; cursor: pointer; justify-content: space-between;"> | |
| <span style="color: #4a9eff; font-weight: bold;">🌍 Use Simulated Earth Map (select 150 World Size for best results)</span> | |
| <input type="checkbox" id="useEarth" checked style="width:24px; height:24px; cursor: pointer; accent-color: #4a9eff;"> | |
| </label> | |
| </div> | |
| <button class="start-button" id="startButton">Start Simulation</button> | |
| </div> | |
| <!-- Game Canvas (WebGL) --> | |
| <canvas id="gameCanvas" class="hidden"></canvas> | |
| <!-- HUD --> | |
| <div id="hud" class="hud hidden"> | |
| <div class="hud-title" id="hudTitle">Metrics: Round #1</div> | |
| <div id="hudContent"></div> | |
| <div id="hudStats"></div> | |
| <div class="button-group" id="buttonGroup"></div> | |
| </div> | |
| <!-- Detail Panel --> | |
| <div id="detailPanel" class="detail-panel hidden"> | |
| <div class="detail-title" id="detailTitle"></div> | |
| <div id="detailContent"></div> | |
| <div class="button-group" id="detailButtons"></div> | |
| </div> | |
| <!-- Controls Hint --> | |
| <div id="controlsHint" class="controls-hint hidden"> | |
| <div style="text-align: center;"><strong><a href="https://www.researchgate.net/publication/397331336_Neuraxon" target="_blank">Neuraxon</a> Game of Life 2.0 3d (Lite version)</strong></div> | |
| <div style="text-align: center;"><strong>By <a href="https://vivancos.com/" target="_blank">David Vivancos</a> & <a href="https://josesanchezgarcia.com/" target="_blank">Jose Sanchez</a></div> | |
| <div style="text-align: center;"><strong>for <a href="https://qubic.org/" target="_blank">Qubic</a> Science</strong></div> | |
| <div>🖱️ Left Click + Drag: Rotate Camera</div> | |
| <div>🖱️ Right Click + Drag: Pan Camera</div> | |
| <div>🖱️ Scroll: Zoom</div> | |
| <div>🖱️ Click Object: Select NxEr</div> | |
| <div>⌨️ Space: Pause/Play</div> | |
| <div>⌨️ H: Toggle HUD</div> | |
| <div id="hideControlsHint" style="cursor: pointer; color: #4a9eff; text-decoration: underline; margin-top: 8px;">Hide Hints</div> | |
| </div> | |
| <!-- Game Over Modal --> | |
| <div id="gameOverOverlay" class="modal-overlay hidden"></div> | |
| <div id="gameOverModal" class="modal hidden"> | |
| <div class="modal-title">Extinction Event</div> | |
| <div class="modal-subtitle">Restart with genetic champions?</div> | |
| <div class="modal-buttons"> | |
| <button class="modal-button yes" id="restartYes">Yes</button> | |
| <button class="modal-button no" id="restartNo">No</button> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
| // ===================================================================== | |
| // LOGIC UTILITIES | |
| // ===================================================================== | |
| function clamp(v, min, max) { | |
| return Math.max(min, Math.min(max, v)); | |
| } | |
| function random(min, max) { | |
| return Math.random() * (max - min) + min; | |
| } | |
| function randomInt(min, max) { | |
| return Math.floor(Math.random() * (max - min + 1)) + min; | |
| } | |
| function randomChoice(arr) { | |
| return arr[Math.floor(Math.random() * arr.length)]; | |
| } | |
| function base26Name(n) { | |
| let letters = []; | |
| let n0 = n; | |
| while (true) { | |
| const div = Math.floor(n0 / 26); | |
| const rem = n0 % 26; | |
| letters.push(String.fromCharCode(65 + rem)); | |
| if (div === 0) break; | |
| n0 = div - 1; | |
| } | |
| return letters.reverse().join(''); | |
| } | |
| function noise(x, y, scale, offset) { | |
| const ox = offset[0]; | |
| const oy = offset[1]; | |
| return (Math.sin((x + ox) * 0.15 * scale) + | |
| Math.cos((y + oy) * 0.13 * scale) + | |
| Math.sin(((x + ox) + (y + oy)) * 0.07 * scale)) * 0.333; | |
| } | |
| function stripLeadingDigits(name) { | |
| let i = 0; | |
| while (i < name.length && /\d/.test(name[i])) { | |
| i++; | |
| } | |
| return name.substring(i); | |
| } | |
| // ===================================================================== | |
| // NEURAL NETWORK CLASSES v 2.00 | |
| // ===================================================================== | |
| class NetworkParameters { | |
| constructor(hiddenNeurons = 10) { | |
| this.networkName = "Neuraxon NxEr"; | |
| this.numInputNeurons = 6; // UPDATED: Hunger, Vision, Smell added | |
| this.numHiddenNeurons = hiddenNeurons; | |
| this.numOutputNeurons = 5; // UPDATED: Give Food added | |
| this.dt = 1.0; | |
| this.simulationSteps = randomInt(20, 50); | |
| this.connectionProbability = random(0.10, 0.25); | |
| this.smallWorldK = 6; | |
| this.smallWorldRewireProb = 0.15; | |
| this.membraneTimeConstant = random(10.0, 40.0); | |
| this.firingThresholdExcitatory = random(0.6, 1.5); | |
| this.firingThresholdInhibitory = random(-1.5, -0.6); | |
| this.adaptationRate = random(0.01, 0.15); | |
| this.spontaneousFireRate = random(0.01, 0.05); | |
| this.neuronHealthDecay = random(0.0005, 0.005); | |
| this.numDendriticBranches = 3; | |
| this.branchThreshold = 0.6; | |
| this.plateauDecay = 500.0; | |
| this.tauFast = random(3.0, 8.0); | |
| this.tauSlow = random(30.0, 80.0); | |
| this.tauMeta = random(800.0, 2000.0); | |
| this.tauLTP = 15.0; | |
| this.tauLTD = 35.0; | |
| this.wFastInitMin = -1.0; | |
| this.wFastInitMax = 1.0; | |
| this.wSlowInitMin = -0.5; | |
| this.wSlowInitMax = 0.5; | |
| this.wMetaInitMin = -0.3; | |
| this.wMetaInitMax = 0.3; | |
| this.learningRate = random(0.005, 0.03); | |
| this.stdpWindow = random(15.0, 35.0); | |
| this.plasticityThreshold = 0.5; | |
| this.associativityStrength = random(0.05, 0.15); | |
| this.synapseIntegrityThreshold = 0.1; | |
| this.synapseFormationProb = random(0.01, 0.05); | |
| this.synapseDeathProb = random(0.005, 0.02); | |
| this.neuronDeathThreshold = 0.1; | |
| this.dopamineBaseline = random(0.08, 0.20); | |
| this.serotoninBaseline = random(0.08, 0.20); | |
| this.acetylcholineBaseline = random(0.08, 0.20); | |
| this.norepinephrineBaseline = random(0.08, 0.20); | |
| this.neuromodDecayRate = 0.1; | |
| this.energyBaseline = 100.0; | |
| this.firingEnergyCost = random(3.0, 8.0); | |
| this.plasticityEnergyCost = 10.0; | |
| this.metabolicRate = random(0.8, 1.3); | |
| this.recoveryRate = 0.5; | |
| this.targetFiringRate = 0.1; | |
| this.homeostaticPlasticityRate = 0.001; | |
| this.oscillatorLowFreq = 0.05; | |
| this.oscillatorMidFreq = 0.5; | |
| this.oscillatorHighFreq = 4.0; | |
| this.oscillatorStrength = 0.15; | |
| this.phaseCouplingStrength = 0.1; | |
| this.maxAxonalDelay = 10.0; | |
| } | |
| clone() { | |
| const p = new NetworkParameters(this.numHiddenNeurons); | |
| Object.assign(p, this); | |
| return p; | |
| } | |
| } | |
| class DendriticBranch { | |
| constructor(branchId, parentNeuronId, params) { | |
| this.branchId = branchId; | |
| this.parentNeuronId = parentNeuronId; | |
| this.params = params; | |
| this.branchPotential = 0.0; | |
| this.plateauPotential = 0.0; | |
| this.localSpikeHistory = []; | |
| } | |
| integrateInputs(synapticInputs, dt) { | |
| if (!synapticInputs || synapticInputs.length === 0) { | |
| this.plateauPotential += dt / this.params.plateauDecay * (-this.plateauPotential); | |
| this.branchPotential += dt / (this.params.membraneTimeConstant * 0.5) * (-this.branchPotential); | |
| return this.branchPotential + this.plateauPotential; | |
| } | |
| const linearSum = synapticInputs.reduce((sum, val) => sum + val, 0); | |
| const branchSignal = Math.tanh(linearSum); | |
| if (Math.abs(branchSignal) > this.params.branchThreshold) { | |
| this.plateauPotential = branchSignal * 0.8; | |
| } | |
| const tauBranch = Math.max(1.0, this.params.membraneTimeConstant * 0.3); | |
| this.branchPotential += dt / tauBranch * (branchSignal - this.branchPotential); | |
| this.localSpikeHistory.push(Math.abs(this.branchPotential) > this.params.branchThreshold ? 1.0 : 0.0); | |
| if (this.localSpikeHistory.length > 10) this.localSpikeHistory.shift(); | |
| return this.branchPotential + this.plateauPotential; | |
| } | |
| } | |
| class Synapse { | |
| constructor(preId, postId, params) { | |
| this.preId = preId; | |
| this.postId = postId; | |
| this.params = params; | |
| this.wFast = random(params.wFastInitMin, params.wFastInitMax); | |
| this.wSlow = random(params.wSlowInitMin, params.wSlowInitMax); | |
| this.wMeta = random(params.wMetaInitMin, params.wMetaInitMax); | |
| this.isSilent = Math.random() < 0.1; | |
| this.isModulatory = Math.random() < 0.2; | |
| this.integrity = 1.0; | |
| this.axonalDelay = random(0, params.maxAxonalDelay); | |
| this.preTrace = 0.0; | |
| this.postTrace = 0.0; | |
| this.preTraceLTD = 0.0; | |
| this.learningRateMod = 1.0; | |
| this.neighborSynapses = []; | |
| this.potentialDeltaW = 0.0; | |
| } | |
| computeInput(preState) { | |
| if (this.isSilent) return 0.0; | |
| const delayFactor = Math.max(0.0, 1.0 - this.axonalDelay / 10.0); | |
| return (this.wFast + this.wSlow) * preState * delayFactor; | |
| } | |
| calculateDeltaW(preState, postState, neuromodulators, dt) { | |
| this.preTrace += (-this.preTrace / this.params.tauLTP + (preState === 1 ? 1 : 0)) * dt; | |
| this.preTraceLTD += (-this.preTraceLTD / this.params.tauLTD + (preState === 1 ? 1 : 0)) * dt; | |
| this.postTrace += (-this.postTrace / this.params.tauLTP + (postState === 1 ? 1 : 0)) * dt; | |
| const da = neuromodulators.dopamine || 0.5; | |
| const daHigh = da > 0.01 ? 1.0 : 0.0; | |
| const daLow = da > 1.0 ? 1.0 : 1.0; | |
| this.learningRateMod = 1.0 + (daHigh * 0.5) + (daLow * 0.2); | |
| let deltaW = 0; | |
| if (preState === 1 && postState === 1) { | |
| deltaW = this.params.learningRate * this.learningRateMod * daHigh * this.preTrace; | |
| } else if (preState === 1 && postState === -1) { | |
| deltaW = -this.params.learningRate * this.learningRateMod * daLow * this.preTraceLTD; | |
| } | |
| return deltaW; | |
| } | |
| applyUpdate(dt, neuromodulators, neighborDeltas) { | |
| let deltaW = this.potentialDeltaW; | |
| if (neighborDeltas && neighborDeltas.length > 0) { | |
| const assoc = this.params.associativityStrength * | |
| neighborDeltas.slice(0, 3).reduce((sum, dw, i) => sum + dw / (i + 1), 0); | |
| deltaW += assoc; | |
| } | |
| const hFast = this.preTrace; | |
| const hSlow = 0.5 * this.preTrace + 0.5 * this.postTrace; | |
| this.wFast += dt / this.params.tauFast * (-this.wFast + hFast + deltaW * 0.3); | |
| this.wFast = clamp(this.wFast, -1.0, 1.0); | |
| this.wSlow += dt / this.params.tauSlow * (-this.wSlow + hSlow + deltaW * 0.1); | |
| this.wSlow = clamp(this.wSlow, -1.0, 1.0); | |
| const serotonin = neuromodulators.serotonin || 0.5; | |
| const metaTarget = deltaW * 0.05 * (serotonin > 0.5 ? 1.0 : 0.5); | |
| this.wMeta += dt / this.params.tauMeta * (-this.wMeta + metaTarget); | |
| this.wMeta = clamp(this.wMeta, -0.5, 0.5); | |
| const activity = Math.abs(this.wFast) + Math.abs(this.wSlow); | |
| if (activity < 0.01) this.integrity -= this.params.synapseDeathProb * dt; | |
| else this.integrity = Math.min(1.0, this.integrity + 0.001 * dt * activity); | |
| if (this.isSilent && this.preTrace > 0.5 && Math.random() < 0.01) this.isSilent = false; | |
| } | |
| getModulatoryEffect() { return this.isModulatory ? this.wMeta * 0.5 : 0.0; } | |
| } | |
| class Neuraxon { | |
| constructor(id, type, params) { | |
| this.id = id; | |
| this.type = type; | |
| this.params = params; | |
| this.membranePotential = 0.0; | |
| this.trinaryState = 0; | |
| this.adaptation = 0.0; | |
| this.autoreceptor = 0.0; | |
| this.health = 1.0; | |
| this.isActive = true; | |
| this.dendriticBranches = []; | |
| for (let i = 0; i < params.numDendriticBranches; i++) { | |
| this.dendriticBranches.push(new DendriticBranch(i, id, params)); | |
| } | |
| this.energyLevel = params.energyBaseline; | |
| this.lastFiringTime = -1000.0; | |
| this.phase = Math.random() * 2 * Math.PI; | |
| this.naturalFrequency = random(0.5, 2.0); | |
| this.stateHistory = []; | |
| this.intrinsicTimescale = params.membraneTimeConstant; | |
| } | |
| nonlinearDendriticIntegration(synapticInputs, modulatoryInputs, dt) { | |
| const branchOutputs = []; | |
| let totalSynaptic = 0.0; | |
| for (let i = 0; i < this.dendriticBranches.length; i++) { | |
| const branch = this.dendriticBranches[i]; | |
| const branchSynInputs = []; | |
| for (let j = i; j < synapticInputs.length; j += this.dendriticBranches.length) { | |
| branchSynInputs.push(synapticInputs[j]); | |
| } | |
| const branchOut = branch.integrateInputs(branchSynInputs, dt); | |
| branchOutputs.push(branchOut); | |
| totalSynaptic += branchOut; | |
| } | |
| const totalModulatory = modulatoryInputs.reduce((sum, val) => sum + val, 0); | |
| totalSynaptic *= (1.0 + totalModulatory * 0.2); | |
| return [totalSynaptic, branchOutputs]; | |
| } | |
| updatePhaseOscillator(dt, globalOsc) { | |
| const dPhase = 2 * Math.PI * this.naturalFrequency * dt; | |
| const coupling = this.params.phaseCouplingStrength * Math.sin(globalOsc - this.phase) * dt; | |
| this.phase += dPhase + coupling; | |
| this.phase %= 2 * Math.PI; | |
| } | |
| updateEnergy(activity, plasticityCost, dt) { | |
| if (!this.isActive) return; | |
| const consumption = this.params.metabolicRate * (this.params.firingEnergyCost * activity + this.params.plasticityEnergyCost * plasticityCost) * dt; | |
| const recovery = this.params.recoveryRate * (1.0 - activity) * dt; | |
| this.energyLevel += (recovery - consumption); | |
| this.energyLevel = clamp(this.energyLevel, 0, this.params.energyBaseline * 1.5); | |
| if (this.energyLevel < 10.0) { | |
| this.health -= this.params.neuronHealthDecay * dt * 2.0; | |
| this.membranePotential *= 0.9; | |
| } | |
| } | |
| updateAutocorrelation() { | |
| if (this.stateHistory.length < 10) return; | |
| const states = this.stateHistory.slice(-10); | |
| let sum1 = 0, sum2 = 0; | |
| for (let i = 0; i < states.length - 1; i++) { | |
| sum1 += states[i] * states[i + 1]; | |
| sum2 += states[i] * states[i]; | |
| } | |
| const autocorr = sum2 > 0 ? sum1 / sum2 : 0; | |
| if (!isNaN(autocorr)) this.intrinsicTimescale = this.params.membraneTimeConstant * (1.0 + Math.abs(autocorr) * 2.0); | |
| } | |
| update(synapticInputs, modulatoryInputs, externalInput, neuromodulators, dt, globalOsc) { | |
| if (!this.isActive || this.energyLevel <= 0) return; | |
| this.updatePhaseOscillator(dt, globalOsc); | |
| const [totalSynaptic, branchOutputs] = this.nonlinearDendriticIntegration(synapticInputs, modulatoryInputs, dt); | |
| let spontaneous = 0.0; | |
| const spontProb = this.params.spontaneousFireRate * dt * (1.0 + Math.cos(this.phase) * 0.3); | |
| if (Math.random() < spontProb) spontaneous = random(-0.5, 0.5); | |
| const acetylcholine = neuromodulators.acetylcholine || 0.5; | |
| const norepi = neuromodulators.norepinephrine || 0.5; | |
| const thresholdMod = (acetylcholine - 0.5) * 0.5 + modulatoryInputs.reduce((sum, val) => sum + val, 0) * 0.3; | |
| const gain = 1.0 + (norepi - 0.5) * 0.4; | |
| const drive = (totalSynaptic + externalInput + spontaneous) * gain; | |
| const tauEff = Math.max(1.0, this.intrinsicTimescale); | |
| this.membranePotential += dt / tauEff * (-this.membranePotential + drive - this.adaptation); | |
| this.adaptation += dt / 100.0 * (-this.adaptation + 0.1 * Math.abs(this.trinaryState)); | |
| this.autoreceptor += dt / 200.0 * (-this.autoreceptor + 0.2 * this.trinaryState); | |
| const thetaExc = this.params.firingThresholdExcitatory - thresholdMod - 0.1 * this.autoreceptor; | |
| const thetaInh = this.params.firingThresholdInhibitory - thresholdMod + 0.1 * this.autoreceptor; | |
| const prevState = this.trinaryState; | |
| if (this.membranePotential > thetaExc) this.trinaryState = 1; | |
| else if (this.membranePotential < thetaInh) this.trinaryState = -1; | |
| else this.trinaryState = 0; | |
| this.stateHistory.push(this.trinaryState); | |
| if (this.stateHistory.length > 50) this.stateHistory.shift(); | |
| this.updateAutocorrelation(); | |
| const activityLevel = Math.abs(this.trinaryState); | |
| if (activityLevel < 0.01) this.health -= this.params.neuronHealthDecay * dt; | |
| else this.health = Math.min(1.0, this.health + 0.0005 * dt); | |
| const plasticityCost = Math.abs(this.trinaryState - prevState) * 0.1; | |
| this.updateEnergy(activityLevel, plasticityCost, dt); | |
| if (this.type === 'hidden' && (this.health < this.params.neuronDeathThreshold || this.energyLevel < 1.0)) { | |
| if (Math.random() < 0.001) this.isActive = false; | |
| } | |
| } | |
| setState(state) { | |
| if ([-1, 0, 1].includes(state)) { | |
| this.trinaryState = state; | |
| this.membranePotential = state * this.params.firingThresholdExcitatory; | |
| } | |
| } | |
| } | |
| class NeuraxonNetwork { | |
| constructor(params) { | |
| this.params = params || new NetworkParameters(); | |
| this.inputNeurons = []; | |
| this.hiddenNeurons = []; | |
| this.outputNeurons = []; | |
| this.allNeurons = []; | |
| this.synapses = []; | |
| this.neuromodulators = { | |
| dopamine: this.params.dopamineBaseline, | |
| serotonin: this.params.serotoninBaseline, | |
| acetylcholine: this.params.acetylcholineBaseline, | |
| norepinephrine: this.params.norepinephrineBaseline | |
| }; | |
| this.time = 0.0; | |
| this.stepCount = 0; | |
| this.totalEnergyConsumed = 0.0; | |
| this.oscillatorPhaseOffsets = [Math.random() * 2 * Math.PI, Math.random() * 2 * Math.PI, Math.random() * 2 * Math.PI]; | |
| this.activationHistory = []; | |
| this.branchingRatio = 1.0; | |
| this.initializeNeurons(); | |
| this.initializeSynapses(); | |
| } | |
| initializeNeurons() { | |
| let nid = 0; | |
| for (let i = 0; i < this.params.numInputNeurons; i++) { | |
| const n = new Neuraxon(nid++, 'input', this.params); | |
| this.inputNeurons.push(n); | |
| this.allNeurons.push(n); | |
| } | |
| for (let i = 0; i < this.params.numHiddenNeurons; i++) { | |
| const n = new Neuraxon(nid++, 'hidden', this.params); | |
| this.hiddenNeurons.push(n); | |
| this.allNeurons.push(n); | |
| } | |
| for (let i = 0; i < this.params.numOutputNeurons; i++) { | |
| const n = new Neuraxon(nid++, 'output', this.params); | |
| this.outputNeurons.push(n); | |
| this.allNeurons.push(n); | |
| } | |
| } | |
| initializeSynapses() { | |
| const numNeurons = this.allNeurons.length; | |
| if (numNeurons <= 1) return; | |
| const k = Math.max(2, Math.min(numNeurons - 1, this.params.smallWorldK)); | |
| const rewireP = clamp(this.params.smallWorldRewireProb, 0, 1); | |
| const existing = new Set(); | |
| for (let idx = 0; idx < numNeurons; idx++) { | |
| const pre = this.allNeurons[idx]; | |
| for (let offset = 1; offset <= Math.floor(k / 2); offset++) { | |
| let j = (idx + offset) % numNeurons; | |
| let post = this.allNeurons[j]; | |
| if (Math.random() < rewireP) { | |
| const candidates = this.allNeurons.filter(n => n.id !== pre.id); | |
| if (candidates.length > 0) post = randomChoice(candidates); | |
| } | |
| if (pre.id === post.id) continue; | |
| if (pre.type === 'output' && post.type === 'input') continue; | |
| const key = `${pre.id},${post.id}`; | |
| if (existing.has(key)) continue; | |
| const syn = new Synapse(pre.id, post.id, this.params); | |
| this.synapses.push(syn); | |
| existing.add(key); | |
| } | |
| } | |
| for (const pre of this.allNeurons) { | |
| for (const post of this.allNeurons) { | |
| if (pre.id === post.id) continue; | |
| if (pre.type === 'output' && post.type === 'input') continue; | |
| if (Math.random() < this.params.connectionProbability * 0.25) { | |
| const key = `${pre.id},${post.id}`; | |
| if (existing.has(key)) continue; | |
| const syn = new Synapse(pre.id, post.id, this.params); | |
| this.synapses.push(syn); | |
| existing.add(key); | |
| } | |
| } | |
| } | |
| for (const syn of this.synapses) { | |
| syn.neighborSynapses = this.synapses.filter(s => s.preId === syn.preId && s.postId !== syn.postId); | |
| } | |
| } | |
| globalOscillatoryDrive() { | |
| const t = this.time; | |
| const low = Math.sin(2.0 * Math.PI * this.params.oscillatorLowFreq * t + this.oscillatorPhaseOffsets[0]); | |
| const mid = Math.sin(2.0 * Math.PI * this.params.oscillatorMidFreq * t + this.oscillatorPhaseOffsets[1]); | |
| const high = Math.sin(2.0 * Math.PI * this.params.oscillatorHighFreq * t + this.oscillatorPhaseOffsets[2]); | |
| return this.params.oscillatorStrength * (low * 0.5 + low * mid * 0.3 + mid * high * 0.2); | |
| } | |
| simulateStep(externalInputs = {}) { | |
| const dt = this.params.dt; | |
| const oscDrive = this.globalOscillatoryDrive(); | |
| const synInputs = {}; | |
| const modInputs = {}; | |
| this.allNeurons.forEach(n => { synInputs[n.id] = []; modInputs[n.id] = []; }); | |
| for (const s of this.synapses) { | |
| if (s.integrity <= 0) continue; | |
| const pre = this.allNeurons[s.preId]; | |
| if (!pre || !pre.isActive) continue; | |
| synInputs[s.postId].push(s.computeInput(pre.trinaryState)); | |
| const mod = s.getModulatoryEffect(); | |
| if (mod !== 0) modInputs[s.postId].push(mod); | |
| } | |
| for (const n of this.allNeurons) { | |
| if (!n.isActive) continue; | |
| const ext = externalInputs[n.id] || 0.0; | |
| n.update(synInputs[n.id], modInputs[n.id], ext + oscDrive, this.neuromodulators, dt, oscDrive); | |
| this.activationHistory.push(Math.abs(n.trinaryState)); | |
| } | |
| if (this.activationHistory.length > 1000) this.activationHistory.shift(); | |
| for (const s of this.synapses) { | |
| if (s.integrity <= 0) continue; | |
| const pre = this.allNeurons[s.preId]; | |
| const post = this.allNeurons[s.postId]; | |
| if (pre && pre.isActive && post && post.isActive) { | |
| s.potentialDeltaW = s.calculateDeltaW(pre.trinaryState, post.trinaryState, this.neuromodulators, dt); | |
| } | |
| } | |
| for (const s of this.synapses) { | |
| if (s.integrity <= 0) continue; | |
| const pre = this.allNeurons[s.preId]; | |
| const post = this.allNeurons[s.postId]; | |
| if (pre && pre.isActive && post && post.isActive) { | |
| s.applyUpdate(dt, this.neuromodulators, s.neighborSynapses.map(ns => ns.potentialDeltaW)); | |
| } | |
| } | |
| this.neuromodulators.dopamine = clamp(this.neuromodulators.dopamine + oscDrive * 0.02, 0, 2); | |
| this.neuromodulators.serotonin = clamp(this.neuromodulators.serotonin + oscDrive * 0.01, 0, 2); | |
| this.neuromodulators.acetylcholine = clamp(this.neuromodulators.acetylcholine + oscDrive * 0.01, 0, 2); | |
| this.neuromodulators.norepinephrine = clamp(this.neuromodulators.norepinephrine + oscDrive * 0.015, 0, 2); | |
| if (this.stepCount % 100 === 0) { | |
| for (const neuron of this.hiddenNeurons) { | |
| if (neuron.stateHistory.length === 0) continue; | |
| const recentActivity = neuron.stateHistory.reduce((sum, s) => sum + Math.abs(s), 0) / neuron.stateHistory.length; | |
| if (recentActivity > this.params.targetFiringRate * 1.2) neuron.params.firingThresholdExcitatory += this.params.homeostaticPlasticityRate; | |
| else if (recentActivity < this.params.targetFiringRate * 0.8) neuron.params.firingThresholdExcitatory -= this.params.homeostaticPlasticityRate; | |
| } | |
| } | |
| this.synapses = this.synapses.filter(s => s.integrity > this.params.synapseIntegrityThreshold); | |
| if (Math.random() < this.params.synapseFormationProb) { | |
| const active = this.allNeurons.filter(n => n.isActive && n.energyLevel > 20); | |
| if (active.length >= 2) { | |
| const pre = randomChoice(active); | |
| const post = randomChoice(active); | |
| if (pre.id !== post.id && !(pre.type === 'output' && post.type === 'input')) { | |
| if (!this.synapses.some(s => s.preId === pre.id && s.postId === post.id)) { | |
| this.synapses.push(new Synapse(pre.id, post.id, this.params)); | |
| } | |
| } | |
| } | |
| } | |
| if (this.activationHistory.length > 2) { | |
| const activations = this.activationHistory.slice(-100); | |
| const denom = activations.slice(0, -1).reduce((sum, val) => sum + val, 0); | |
| if (denom > 0) this.branchingRatio = clamp(activations.slice(1).reduce((sum, val) => sum + val, 0) / denom, 0.1, 10.0); | |
| } | |
| this.time += dt; | |
| this.stepCount++; | |
| } | |
| setInputStates(states) { states.forEach((state, i) => { if (i < this.inputNeurons.length) this.inputNeurons[i].setState(state); }); } | |
| getOutputStates() { return this.outputNeurons.map(n => n.isActive ? n.trinaryState : 0); } | |
| getEnergyStatus() { | |
| const activeNeurons = this.allNeurons.filter(n => n.isActive); | |
| if (activeNeurons.length === 0) return { totalEnergy: 0, averageEnergy: 0, efficiency: 0, branchingRatio: this.branchingRatio }; | |
| const totalEnergy = activeNeurons.reduce((sum, n) => sum + n.energyLevel, 0); | |
| return { totalEnergy, averageEnergy: totalEnergy / activeNeurons.length, efficiency: this.stepCount > 0 ? this.totalEnergyConsumed / this.stepCount : 0, branchingRatio: this.branchingRatio }; | |
| } | |
| clone() { return new NeuraxonNetwork(this.params.clone()); } | |
| } | |
| // ===================================================================== | |
| // WORLD GENERATION (Data-Based Earth Map) | |
| // ===================================================================== | |
| const TERRAIN_SEA = 0; | |
| const TERRAIN_LAND = 1; | |
| const TERRAIN_ROCK = 2; | |
| class World { | |
| constructor(N, seaPct, rockPct, useEarth = false) { | |
| this.N = N; | |
| this.grid = Array(N).fill(null).map(() => Array(N).fill(TERRAIN_LAND)); | |
| this.noiseOffsets = [ | |
| [Math.random() * 1000, Math.random() * 1000], | |
| [Math.random() * 1000, Math.random() * 1000], | |
| [Math.random() * 1000, Math.random() * 1000], | |
| [Math.random() * 1000, Math.random() * 1000], | |
| [Math.random() * 1000, Math.random() * 1000] | |
| ]; | |
| if (useEarth) { | |
| this.generateEarth(); | |
| } else { | |
| this.generate(seaPct, rockPct); | |
| } | |
| } | |
| // --- REAL EARTH DATA MAP --- | |
| generateEarth() { | |
| // . = Sea | |
| // : = Land | |
| // ^ = Mountains (Andes, Rockies, Himalayas, Alps) | |
| // = = Ice (polar regions) | |
| const earthMap = [ | |
| "................................................................................................................................................................................................................................................", | |
| "................................................................................................................................................................................................................................................", | |
| "................................................................................................................................................................................................................................................", | |
| "................................................................................................................................................................................................................................................", | |
| "..............................................................................................^^^^^^^^^.........................................................................................................................................", | |
| "...........................................................^^^^^^^^^^^^^^^^^..^^^^^^^^^^^^^^^^^^^^^^^^^^^...^^^.................................................................................................................................", | |
| "........................................................^^^^^^...^^^^^^^^..^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^........................^^^^^^.............^^............................^^^^^......................................................", | |
| ".............................................^....^^^.^^.^^^^.^^^^^^^....^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.....................^^^^^......................................................^^^..................................................", | |
| "........................................^^...................^^^^^^.......^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^......................^^^........................................................^^..................................................", | |
| "..........................................^^^^.^^...^^^...^^....^...............^^^^^^^^^^^^^^^^^^^^^^^^^^^....................................................^^^^...................^^^^^^^^^^^^^^................^^^^........................", | |
| ".....................................^^^..........................................^^^^^^^^^^^^^^^^^^^^^^^^...................................................^^...................^^^^^^^^^^^^^^^...............................................", | |
| ".....................................^^^^.^^^.^.^^..^^^.^^..^^^^^^.................^^^^^^^^^^^^^^^^^^^^^^..................................................^^.........^^......^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.......^^.........................", | |
| "^^.............^......................^...^^^^^^^^......^^..^^^^^^^^^^^^.............^^^^^^^^^^^^^^^^^^^^..................................................^^^.......^^^.^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^..^^^^^^^^^^^^^..................^", | |
| "...........^^^^^^^^^^^^^^^......^^^^..^...^^^^^^^^^^^.^.^^.....^....^^^^^^^.........^^^^^^^^^^^^^^^^^^^^.............................^^^^^^^^^..................^....^^^.^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.......^^^^^..", | |
| "^^.......^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^....^^....^^.^^^^..^^......^^^^..........^^^^^^^^^^^^^^^..............................^^^^^^^^^^^^^^^^...^^...^^.^^^^^^^^.^^^.^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^", | |
| "^^^.^^......^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^......^^^^^^^^.....^^^^^^^^^^^^^................................^^^^^^^^^^^^^^^^^^...^^^^^^^^^^^^^^^^^^.^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^", | |
| "...^^....^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.^....^^^^^^^..^.......^^^^^^^^............^^^^^^.................^^^^^^...^^^^^^....^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^", | |
| ".............^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^..^^.^^.....^^^^^.........^^^^^^^.............^^..................^^^^^^^..^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.", | |
| "..........^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^......^...^^^...^^...........^^^^^................................^^^^^^^^..^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.^^^^^^^^..", | |
| "..........^^^^^^^^^^^.^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...........^^^^^^...............^^................................^^^^^^^^^..^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.....^^^^^......", | |
| "............:::::..........::::::::::::::::::::::::::::::...........::::::..::..............................................::::::::....::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::..:..::....::...........", | |
| "...............:.:.............:::::::::::::::::::::::::::...........::::::::::.....................................:...........:::.....:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::............:::............", | |
| ".............:..................::::::::::::::::::::::::::::::.......::::::::::.....................................::.......::.:::...::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::............:::::...........", | |
| "..........:......................::::::::::::::::::::::::::::::::..:::::::::::::::.................................:.::.......:....:.:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::..............::::............", | |
| "...............................:..:::::::::::::::::::::::::::::::..::::::::::::::::...............................::.:::...::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.........:::.............", | |
| "...................................:::::::::::::::::::::::::::::::.:::::::::::::::...................................::::..:::::::::::^^:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.........:...............", | |
| "...................................:::::::::::::::::::::::::::::::::::::::::..::....................................::...::::::::::::::^^:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.:........................", | |
| "....................................::::::::::::::::::::::::::::::::::::::.::....:::.................................:.:::::::::::::::^^^^:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::...........................", | |
| ".....................................::::::::::::::::::::::::::::::::::::::::.......:.................................:::::::::::::::::^^::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::...........................", | |
| ".....................................::::::^^:::::::::::::::::::::::::::::::::::.......................................:::::::::::::::::::::..:..:::::::...:::::::::::::::::::::::::::::::::::::::::::::::::::::::::............................", | |
| ".....................................::::::^^:::::::::::::::::::::::::::::..:.........................................^^::::::::..:::::::::.......:::::...:::::::::::::::::::::::::::::::::::::::::::::::::::::::::...::........................", | |
| ".....................................::::::^^::::::::::::::::::::::::::::.........................................::::::::.....::...:::::::.........::::...:::::::::::::::::::::::::::::::::::::::::::::::::::::.....:::........................", | |
| ".....................................:::::::^^::::::::::::::::::::::::::..........................................:::::::........::..::::::..::::..::::::...::::::::::::::::::::::::::::::::::::::::::::::::::..................................", | |
| ".....................................::::::::^^::::::::::::::::::::::::...........................................::::::..........:..::...:::::::::::::::...::::::::::::::::::::::::::::::::::::::::::::.:.::........::.........................", | |
| "......................................:::::::^^:::::::::::::::::::::::............................................::::::..........:...::..:::::::::::::::...:::::::::::::::::::::::::::::::::::::::::::....:::.......:..........................", | |
| ".......................................::::::::::::::::::::::::::::::.......................................................:::...........:::::::::::::::::::::::::::::^^^^^^^^^^^^^^^:::::::::::::::::::...::.....:::..........................", | |
| ".......................................::::::::::::::::::::::::::::::...............................................:::::::::::.........::....:.:::::::::::::::::::::::::^^^^^^^^^^^^^^:::::::::::::::::....::..::::::..........................", | |
| ".........................................:::::::::::::::::::::::::::...............................................::::::::::::.................::::::::::::::::::::::::::^^^^^^^^^^^^^:::::::::::::::::........:::.............................", | |
| "..........................................::::::::::::::::::::::::................................................::::::::::::::::...::........:::::::::::::::::::::::::::::::::^^^^^^^^:::::::::::::::::......:................................", | |
| "...........................................:.:::::::::::::::::::::................................................::::::::::::::::::.::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::.......................................", | |
| "...........................................:.::::::::::::..:.....:...............................................:::::::::::::::::::::::::::::::::::::::..:::::::::::::::::::::::::::::::::::::::::::::::.......................................", | |
| "...........................................::.:::::::::..........:.............................................:::::::::::::::::::::::::::::::..:::::::::..::::::::::::::::::::::::::::::::::::::::::::::.......................................", | |
| ".............................................:.::::::::..........::.:.........................................:::::::::::::::::::::::::::::::::.:::::::::....:::::::::::::::::::::::::::::::::::::::::::........................................", | |
| ".............................................:..:::::::.......................................................:::::::::::::::::::::::::::::::::..:::::::::..::.......::::::::::::::::::::::::::::::::::.........................................", | |
| ".................................................::::::......................................................:::::::::::::::::::::::::::::::::::..:::::::::::::.......::::::::::::::::::::::::::::::::..:.......................................", | |
| "..................................................:::::............:.........................................::::::::::::::::::::::::::::::::::::.::::::::::::::......:::::::::::::..::::::::::::::.............................................", | |
| "..................................................:::::.....::......:::......................................::::::::::::::::::::::::::::::::::::..::::::::::::..........:::::::::....:::::::::.................................................", | |
| "...................................................:::::...:::..........::...................................::::::::::::::::::::::::::::::::::::..:::::::::::...........:::::::.......::::::::.::..............................................", | |
| ".....................................................::::::::................................................:::::::::::::::::::::::::::::::::::::..:::::::::............::::::........::::::::.........::......................................", | |
| ".......................................................:::::::::.............................................:::::::::::::::::::::::::::::::::::::..:::::::..............:::::.........:.:::::::........:.......................................", | |
| "...........................................................::::::............................................:::::::::::::::::::::::::::::::::::::::.::::.................:::............::::::::.......:.......................................", | |
| ".............................................................:::.............................................:::::::::::::::::::::::::::::::::::::::::....................:::.............:::::::.........:.....................................", | |
| "..............................................................:::......:.:....................................:::::::::::::::::::::::::::::::::::::::...::.................::................:::.........:.:....................................", | |
| "..............................................................:::::::::::::::::................................:::::::::::::::::::::::::::::::::::::::::::.................::.................:.................................................", | |
| ".................................................................:::::::::::::::...............................::::::::::::::::::::::::::::::::::::::::::....................:...........::...............::....................................", | |
| "....................................................................:::::::::::::...............................:::::::::::::::::::::::::::::::::::::::::....................:.............:...............:....................................", | |
| "....................................................................:::::::::::::::::.............................::::::...:::::::::::::::::::::::::::::................................:..::........::.........................................", | |
| ".....................................................................:::::::::::::::::........................................::::::::::::::::::::::::::................................::.::.......::..........................................", | |
| "....................................................................::^^::::::::::::::........................................:::::::::::::::::::::::::..................................::.:.....:::::.........................................", | |
| "...................................................................::::^^::::::::::::::.......................................:::::::::::::::::::::::.....................................:::....::::::.:::..:..................................", | |
| "..................................................................:::::^^::::::::::::::::.....................................::::::::::::::::::::::.......................................::....:::::.......:..:...............................", | |
| "..................................................................:::::^^:::::::::::::::::....................................:::::::::::::::::::::........................................:::...:::::.::........:.::...........................", | |
| "..................................................................:::::^^:::::::::::::::::::::.................................::::::::::::::::::::.........................................:::.....:....:..:....:::::::........................", | |
| "..................................................................:::::^^:::::::::::::::::::::::................................::::::::::::::::::...........................................::.....................:::::.......................", | |
| "..................................................................:::::^^::::::::::::::::::::::::...............................::::::::::::::::::............................................::..:..............:..::::::......:...............", | |
| "...................................................................:::::^^:::::::::::::::::::::::................................:::::::::::::::::................................................:::...............::::.::.......:.............", | |
| "....................................................................::::^^::::::::::::::::::::::.................................::::::::::::::::::.......................................................:...............::....................", | |
| "....................................................................::::^^:::::::::::::::::::::..................................::::::::::::::::::.............................................................................................", | |
| ".....................................................................::::^^::::::::::::::::::::..................................::::::::::::::::::............................................................::::....:........................", | |
| ".....................................................................::::^^^::::::::::::::::::..................................:::::::::::::::::::.....:...................................................:.:::::...::........................", | |
| "......................................................................:::::^^::::::::::::::::..................................:::::::::::::::::::....:::.................................................::::::::...:::.......................", | |
| "........................................................................:::^^:::::::::::::::::..................................:::::::::::::::::....::::.................................................:::::::::::.:::.......................", | |
| ".........................................................................::^^:::::::::::::::::..................................::::::::::::::::.....::::................................................:::::::::::::::::......................", | |
| ".........................................................................::^^::::::::::::::::....................................::::::::::::::.......::...............................................::::::::::::::::::::..........:..........", | |
| ".........................................................................::^^::::::::::::::::....................................:::::::::::::::.....:::.............................................:::::::::::::::::::::::..........:.........", | |
| ".........................................................................::^^::::::::::::::.......................................::::::::::::::.....:::............................................:::::::::::::::::::::::::...................", | |
| ".........................................................................::^^:::::::::::..........................................:::::::::::::......::.............................................::::::::::::::::::::::::::..................", | |
| ".........................................................................::^^:::::::::::..........................................::::::::::::......................................................::::::::::::::::::::::::::..................", | |
| ".........................................................................:^^:::::::::::..........................................::::::::::::......................................................::::::::::::::::::::::::::..................", | |
| "........................................................................::^^:::::::::::............................................::::::::::........................................................:::::::::::::::::::::::::..................", | |
| "........................................................................::^^::::::::::..............................................::::::::.........................................................:::::::::::::::::::::::::..................", | |
| "........................................................................::^^:::::::::...............................................:::::::..........................................................::::::::....:::::::::::::..................", | |
| "........................................................................:^::::::::::................................................:::::............................................................::::::.......:.:::::::::...................", | |
| "........................................................................:^::::::::..................................................................................................................................::::::::...............:....", | |
| ".......................................................................::^::::::::...................................................................................................................................:::::::................:...", | |
| ".......................................................................::^:::::::.....................................................................................................................................::.:..................:::.", | |
| ".......................................................................::^:::::.............................................................................................................................................................::..", | |
| ".......................................................................::^:::............................................................................................................................................::.................:...", | |
| ".......................................................................::^:::............................................................................................................................................::...............::....", | |
| "......................................................................::::::............................................................................................................................................................::......", | |
| "......................................................................:::::............................................................................................................................................................:::......", | |
| "......................................................................::::::....................................................................................................................................................................", | |
| "......................................................................:::::.....................................................................................................................................................................", | |
| "......................................................................::::......................................................................................................................................................................", | |
| "......................................................................::::.....::...............................................................................................................................................................", | |
| ".......................................................................::::.....................................................................................................................................................................", | |
| "........................................................................:::::...................................................................................................................................................................", | |
| "................................................................................................................................................................................................................................................", | |
| "................................................................................................................................................................................................................................................", | |
| "................................................................................................................................................................................................................................................", | |
| "................................................................................................................................................................................................................................................", | |
| "................................................................................................................................................................................................................................................", | |
| "................................................................................^^..............................................................................................................................................................", | |
| ".............................................................................^..................................................................................................................................................................", | |
| "...........................................................................^^.............................................................................^^^^....................^........^^^^..^^^^^...^^^^^^^^^^.............................", | |
| "...........................................................................^.........................................................................^^^^^^^^^^^^^^^^^.......^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.....................", | |
| "........................................................................^^^^^^...............................................................^^^^^^^^^^^^^^^^^^^^^^^^^....^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^..............", | |
| ".......................................................................^^^.^^^^....................................^..^^.^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^..^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.......", | |
| "............................................................^..............^^^^................................^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^......", | |
| ".........................................^^^^........^^^^^^^^^^^^^^^^^^^^^^^^^^...............................^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.........", | |
| "......................^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...............................^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...........", | |
| "..............^.^^..^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^..............................^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...........", | |
| "...........^^...^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...................^^^^.....^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.........", | |
| "......................^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.........^....^^^^^^...........^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.............", | |
| ".................^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.........^^...^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^............", | |
| "..................^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^........", | |
| "^^^^^^^^^^...............^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^", | |
| "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^", | |
| "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^", | |
| "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" | |
| ]; | |
| const mapH = earthMap.length; | |
| const mapW = earthMap[0].length; | |
| // Use the N from the constructor (which we set to 150 in HTML now) | |
| const N = this.N; | |
| for (let y = 0; y < N; y++) { | |
| for (let x = 0; x < N; x++) { | |
| // Nearest Neighbor Scaling | |
| const mapY = Math.floor((y / N) * mapH); | |
| const mapX = Math.floor((x / N) * mapW); | |
| // Safety check to prevent crashing if mapX is out of bounds | |
| const char = (earthMap[mapY] && earthMap[mapY][mapX]) ? earthMap[mapY][mapX] : '.'; | |
| if (char === '^' || char === '=') { | |
| this.grid[y][x] = TERRAIN_ROCK; | |
| } else if (char === ':') { | |
| this.grid[y][x] = TERRAIN_LAND; | |
| } else { | |
| this.grid[y][x] = TERRAIN_SEA; | |
| } | |
| } | |
| } | |
| } | |
| // --- STANDARD GENERATION --- | |
| generate(seaPct, rockPct) { | |
| const N = this.N; | |
| const elevation = Array(N).fill(null).map(() => Array(N).fill(0)); | |
| const rockDensity = Array(N).fill(null).map(() => Array(N).fill(0)); | |
| for (let y = 0; y < N; y++) { | |
| for (let x = 0; x < N; x++) { | |
| const r = Math.hypot(x - N/2, y - N/2) / (N * 0.5); | |
| let h = noise(x, y, 0.5, this.noiseOffsets[0]) + | |
| noise(x, y, 1.0, this.noiseOffsets[1]) + | |
| noise(x, y, 2.0, this.noiseOffsets[2]) * 0.5; | |
| h = h / 2.5 - Math.pow(r, 2) * 0.8; | |
| elevation[y][x] = h; | |
| rockDensity[y][x] = noise(x, y, 3.0, this.noiseOffsets[4]); | |
| } | |
| } | |
| const flat = elevation.flat().sort((a, b) => a - b); | |
| const seaThresh = flat[Math.floor(flat.length * seaPct)] || flat[0]; | |
| for (let y = 0; y < N; y++) { | |
| for (let x = 0; x < N; x++) { | |
| this.grid[y][x] = elevation[y][x] <= seaThresh ? TERRAIN_SEA : TERRAIN_LAND; | |
| } | |
| } | |
| // Hydraulic Rivers | |
| const numRivers = Math.floor(N/2); | |
| for(let i=0; i<numRivers; i++) { | |
| let cx=Math.floor(Math.random()*N), cy=Math.floor(Math.random()*N); | |
| if(this.grid[cy][cx]!==TERRAIN_LAND) continue; | |
| let steps=0; | |
| while(steps++ < N*2) { | |
| let minH = elevation[cy][cx], bx=-1, by=-1; | |
| for(let dy=-1; dy<=1; dy++) for(let dx=-1; dx<=1; dx++) { | |
| if(dx===0 && dy===0) continue; | |
| const nx=(cx+dx+N)%N, ny=cy+dy; | |
| if(ny>=0 && ny<N && elevation[ny][nx]<minH) { minH=elevation[ny][nx]; bx=nx; by=ny; } | |
| } | |
| if(bx!==-1) { | |
| cx=bx; cy=by; this.grid[cy][cx]=TERRAIN_SEA; | |
| if(this.grid[cy][cx]===TERRAIN_SEA && Math.random()<0.1) break; | |
| } else break; | |
| } | |
| } | |
| // Rocks | |
| const landCells = []; | |
| for(let y=0; y<N; y++) for(let x=0; x<N; x++) if(this.grid[y][x]===TERRAIN_LAND) landCells.push({x,y,v:rockDensity[y][x]}); | |
| landCells.sort((a,b)=>b.v - a.v); | |
| const nR = Math.floor(landCells.length*rockPct); | |
| for(let i=0; i<nR; i++) this.grid[landCells[i].y][landCells[i].x] = TERRAIN_ROCK; | |
| } | |
| terrain(x, y) { | |
| x = ((x % this.N) + this.N) % this.N; | |
| if (y < 0 || y >= this.N) return TERRAIN_SEA; | |
| return this.grid[y][x]; | |
| } | |
| } | |
| class NxEr { | |
| constructor(config) { | |
| Object.assign(this, config); | |
| this.alive = true; | |
| // UPDATED: Now 6 inputs | |
| this.lastInputs = [0, 0, 0, 0, 0, 0]; | |
| this.tickAccum = 0; | |
| this.lastPos = [...this.pos]; | |
| this.matingWith = null; | |
| this.matingEndTick = null; | |
| this.matingIntentUntilTick = 0; | |
| this.mateCooldownUntilTick = 0; | |
| this.parents = config.parents || [null, null]; | |
| this.stats = config.stats || { foodFound: 0, foodTaken: 0, explored: 0, timeLived: 0, matesPerformed: 0, energyEfficiency: 0, temporalSyncScore: 0, fitness: 0 }; | |
| this.visited = new Set([`${this.pos[0]},${this.pos[1]}`]); | |
| this.dopamineBoostTicks = 0; | |
| this.lastO4 = 0; | |
| // NEW: Sensory & Clan attributes | |
| this.visionRange = config.visionRange || randomInt(2, 15); | |
| this.smellRadius = config.smellRadius || randomInt(2, 5); | |
| this.heading = config.heading !== undefined ? config.heading : randomInt(0, 7); // 0=NW...7=W | |
| this.clanId = config.clanId || null; | |
| } | |
| updateStats(dt) { | |
| this.stats.timeLived += dt; | |
| this.stats.explored = this.visited.size; | |
| const energyStatus = this.net.getEnergyStatus(); | |
| this.stats.energyEfficiency = energyStatus.efficiency || 0; | |
| this.stats.temporalSyncScore = energyStatus.branchingRatio || 1.0; | |
| this.stats.fitness = (Math.min(this.stats.foodFound/100, 1)*0.3 + Math.min(this.stats.explored/1000, 1)*0.2 + Math.min(this.stats.timeLived/1000, 1)*0.2 + Math.min(this.stats.energyEfficiency/10, 1)*0.15 + Math.min(this.stats.temporalSyncScore/2, 1)*0.15); | |
| } | |
| } | |
| class Food { | |
| constructor(id, pos) { | |
| this.id = id; | |
| this.anchor = [...pos]; | |
| this.pos = [...pos]; | |
| this.alive = true; | |
| this.respawnAtTick = null; | |
| this.remaining = 25; | |
| this.progress = {}; | |
| } | |
| } | |
| // ===================================================================== | |
| // GAME ENGINE | |
| // ===================================================================== | |
| class GameEngine { | |
| constructor(config) { | |
| this.config = config; | |
| this.world = new World(config.worldSize, config.seaPct / 100, config.rockPct / 100, config.useEarth); | |
| this.nxers = new Map(); | |
| this.foods = new Map(); | |
| this.occupied = new Set(); | |
| this.effects = []; | |
| this.stepTick = 0; | |
| this.birthsCount = 0; | |
| this.deathsCount = 0; | |
| this.gameIndex = 1; | |
| this.paused = true; | |
| this.gameOver = false; | |
| this.usedColors = new Set(); | |
| this.championCounts = {}; | |
| this.allTimeBest = { foodFound: [], foodTaken: [], explored: [], timeLived: [], matesPerformed: [], fitness: [] }; | |
| // NEW: Clan Counter | |
| this.nextClanId = 1; | |
| this.initializeFood(); | |
| this.initializeNxErs(); | |
| } | |
| mate(a, b) { | |
| const dur = Math.max(a.net.params.simulationSteps, b.net.params.simulationSteps); | |
| a.matingWith = b.id; | |
| b.matingWith = a.id; | |
| a.matingEndTick = this.stepTick + dur; | |
| b.matingEndTick = this.stepTick + dur; | |
| // Cost | |
| a.food -= 3; | |
| b.food -= 3; | |
| a.stats.matesPerformed++; | |
| b.stats.matesPerformed++; | |
| this.effects.push({ type: 'heart', pos: [...a.pos], startTick: this.stepTick }); | |
| this.spawnChild(a, b, a.pos); | |
| } | |
| initializeFood() { | |
| const count = Math.floor(this.config.maxFood / 2); | |
| for (let i = 0; i < count; i++) { | |
| const pos = this.findFreePosition(true, true); | |
| if (pos) this.foods.set(i, new Food(i, pos)); | |
| } | |
| } | |
| initializeNxErs() { | |
| for (let i = 0; i < this.config.startingNxErs; i++) { | |
| const nxer = this.createNxEr(i); | |
| this.nxers.set(nxer.id, nxer); | |
| this.occupied.add(`${nxer.pos[0]},${nxer.pos[1]}`); | |
| } | |
| } | |
| createNxEr(id, params = null) { | |
| const netParams = params || new NetworkParameters(randomInt(1, this.config.maxNeurons)); | |
| const net = new NeuraxonNetwork(netParams); | |
| const pos = this.findFreePosition(true, true) || [ | |
| randomInt(0, this.world.N - 1), | |
| randomInt(0, this.world.N - 1) | |
| ]; | |
| const terrain = this.world.terrain(pos[0], pos[1]); | |
| let canLand, canSea; | |
| if (terrain === TERRAIN_LAND) { canLand = true; canSea = false; } | |
| else if (terrain === TERRAIN_SEA) { canLand = false; canSea = true; } | |
| else { canLand = true; canSea = false; } | |
| const color = this.randomColor(); | |
| const isMale = Math.random() < 0.5; | |
| const ticksPerAction = Math.max(2, Math.floor((this.config.globalTimeSteps * 1.2) / Math.max(1, netParams.simulationSteps))); | |
| // NEW: Init Vision, Smell, Heading | |
| const vision = randomInt(2, 15); | |
| const smell = randomInt(2, 5); | |
| const heading = randomInt(0, 7); | |
| return new NxEr({ | |
| id, | |
| name: base26Name(id), | |
| color, | |
| pos, | |
| canLand, | |
| canSea, | |
| net, | |
| food: this.config.startFood, | |
| isMale, | |
| ticksPerAction, | |
| bornTick: this.stepTick, | |
| lastMoveTick: this.stepTick, | |
| visionRange: vision, | |
| smellRadius: smell, | |
| heading: heading, | |
| clanId: null // No clan for generated starters | |
| }); | |
| } | |
| spawnChild(A, B, nearPos) { | |
| if (this.nxers.size >= this.config.maxNxErs) return null; | |
| const childId = this.nxers.size > 0 ? Math.max(...this.nxers.keys()) + 1 : 0; | |
| const baseParams = Math.random() < 0.5 ? A.net.params.clone() : B.net.params.clone(); | |
| baseParams.numHiddenNeurons = randomInt( | |
| Math.max(1, baseParams.numHiddenNeurons - 2), | |
| Math.min(this.config.maxNeurons, baseParams.numHiddenNeurons + 2) | |
| ); | |
| let canLand, canSea; | |
| const isAMarine = A.canSea && !A.canLand; | |
| const isALand = A.canLand && !A.canSea; | |
| const isBMarine = B.canSea && !B.canLand; | |
| const isBLand = B.canLand && !B.canSea; | |
| if ((isAMarine && isBLand) || (isALand && isBMarine)) { | |
| canLand = true; canSea = true; | |
| } else { | |
| canLand = A.canLand || B.canLand; | |
| canSea = A.canSea || B.canSea; | |
| } | |
| const pos = this.findFreePosition(canSea, canLand, nearPos, 3) || nearPos; | |
| const transfer = Math.min(5.0, Math.min(A.food / 2, B.food / 2)); | |
| A.food -= transfer; B.food -= transfer; | |
| // NEW: Clan Inheritance Logic | |
| let familyClan = null; | |
| if (A.clanId === null && B.clanId === null) { | |
| familyClan = this.nextClanId++; | |
| A.clanId = familyClan; | |
| B.clanId = familyClan; | |
| } else if (A.clanId !== null) { | |
| familyClan = A.clanId; | |
| if (B.clanId === null) B.clanId = familyClan; | |
| } else if (B.clanId !== null) { | |
| familyClan = B.clanId; | |
| if (A.clanId === null) A.clanId = familyClan; | |
| } | |
| // NEW: Inherit sensory traits | |
| const vision = randomChoice([A.visionRange, B.visionRange, randomInt(2, 15)]); | |
| const smell = randomChoice([A.smellRadius, B.smellRadius, randomInt(2, 5)]); | |
| const heading = randomInt(0, 7); | |
| const child = new NxEr({ | |
| id: childId, | |
| name: `${stripLeadingDigits(A.name).replace(/^-+/, '')}-${stripLeadingDigits(B.name).replace(/^-+/, '')}`, | |
| color: this.randomColor(), | |
| pos, | |
| canLand, | |
| canSea, | |
| net: new NeuraxonNetwork(baseParams), | |
| food: transfer * 2, | |
| isMale: Math.random() < 0.5, | |
| ticksPerAction: Math.max(2, Math.floor((this.config.globalTimeSteps * 1.2) / Math.max(1, baseParams.simulationSteps))), | |
| parents: [A.id, B.id], | |
| bornTick: this.stepTick, | |
| lastMoveTick: this.stepTick, | |
| // Pass new props | |
| visionRange: vision, | |
| smellRadius: smell, | |
| heading: heading, | |
| clanId: familyClan | |
| }); | |
| child.stats.fitness = (A.stats.fitness + B.stats.fitness) / 2; | |
| this.nxers.set(childId, child); | |
| this.occupied.add(`${pos[0]},${pos[1]}`); | |
| A.stats.matesPerformed++; | |
| B.stats.matesPerformed++; | |
| this.birthsCount++; | |
| return child; | |
| } | |
| findFreePosition(allowSea = true, allowLand = true, near = null, searchRadius = 5) { | |
| const N = this.world.N; | |
| if (near) { | |
| for (let r = 0; r < searchRadius; r++) { | |
| const candidates = []; | |
| for (let dy = -r; dy <= r; dy++) { | |
| for (let dx = -r; dx <= r; dx++) { | |
| // X Wraps | |
| const x = (near[0] + dx + N) % N; | |
| // Y Clamps (No Wrap) | |
| const y = near[1] + dy; | |
| if (y < 0 || y >= N) continue; // Skip out of bounds | |
| const key = `${x},${y}`; | |
| if (this.occupied.has(key)) continue; | |
| const terrain = this.world.terrain(x, y); | |
| if (terrain === TERRAIN_ROCK) continue; | |
| if (terrain === TERRAIN_LAND && !allowLand) continue; | |
| if (terrain === TERRAIN_SEA && !allowSea) continue; | |
| candidates.push([x, y]); | |
| } | |
| } | |
| if (candidates.length > 0) { | |
| return randomChoice(candidates); | |
| } | |
| } | |
| } | |
| for (let i = 0; i < 100; i++) { | |
| const x = randomInt(0, N - 1); | |
| const y = randomInt(0, N - 1); // Random pick is always safe | |
| const key = `${x},${y}`; | |
| if (this.occupied.has(key)) continue; | |
| const terrain = this.world.terrain(x, y); | |
| if (terrain === TERRAIN_ROCK) continue; | |
| if (terrain === TERRAIN_LAND && !allowLand) continue; | |
| if (terrain === TERRAIN_SEA && !allowSea) continue; | |
| return [x, y]; | |
| } | |
| return null; | |
| } | |
| randomColor() { | |
| const r = randomInt(30, 235), g = randomInt(30, 235), b = randomInt(30, 235); | |
| return `rgb(${r},${g},${b})`; | |
| } | |
| step() { | |
| if (this.paused || this.gameOver) return; | |
| this.stepTick++; | |
| const dt = 1.0 / this.config.globalTimeSteps; | |
| // Pre-calc maps for sensory efficiency | |
| const occupantAt = new Map(); | |
| this.nxers.forEach(n => { if(n.alive) occupantAt.set(`${n.pos[0]},${n.pos[1]}`, n); }); | |
| const foodAt = new Set(); | |
| this.foods.forEach(f => { if(f.alive) foodAt.add(`${f.pos[0]},${f.pos[1]}`); }); | |
| // Direction Vectors (0=NW to 7=W) | |
| const DIR_OFFSETS = [ | |
| [-1, -1], [0, -1], [1, -1], [1, 0], | |
| [1, 1], [0, 1], [-1, 1], [-1, 0] | |
| ]; | |
| for (const nxer of this.nxers.values()) { | |
| if (!nxer.alive) continue; | |
| nxer.updateStats(dt); | |
| nxer.food -= 0.01 * dt; | |
| if (nxer.food <= 0 || this.stepTick - nxer.lastMoveTick > 10 * this.config.globalTimeSteps) this.killNxEr(nxer); | |
| if (nxer.matingWith !== null && this.stepTick >= nxer.matingEndTick) { | |
| nxer.matingWith = null; | |
| nxer.matingEndTick = null; | |
| nxer.mateCooldownUntilTick = this.stepTick + this.config.mateCooldown * this.config.globalTimeSteps; | |
| } | |
| } | |
| const processNxErs = Array.from(this.nxers.values()).filter(n => n.alive && n.matingWith === null); | |
| processNxErs.forEach(nxer => { | |
| nxer.tickAccum++; | |
| if (nxer.tickAccum >= nxer.ticksPerAction) { | |
| nxer.tickAccum = 0; | |
| // --- SENSORY LOGIC --- | |
| // 4. Hunger | |
| const hungerVal = nxer.food < (this.config.startFood * 0.2) ? -1 : 0; | |
| // 5. Vision (Raycast in Heading) | |
| let sightVal = -1; // -1=Nothing/Enemy, 0=Clan, 1=Food | |
| const dir = DIR_OFFSETS[nxer.heading]; | |
| let foundObj = false; | |
| for (let d = 1; d <= nxer.visionRange; d++) { | |
| const tx = (nxer.pos[0] + (dir[0] * d) + this.world.N) % this.world.N; | |
| const ty = nxer.pos[1] + (dir[1] * d); // Y doesn't wrap in lookups usually, but safe check | |
| if (ty < 0 || ty >= this.world.N) break; | |
| const k = `${tx},${ty}`; | |
| if (this.world.terrain(tx, ty) === TERRAIN_ROCK) break; // Blocked | |
| if (foodAt.has(k)) { sightVal = 1; foundObj = true; break; } | |
| if (occupantAt.has(k)) { | |
| const other = occupantAt.get(k); | |
| if (other.clanId !== null && nxer.clanId !== null && other.clanId === nxer.clanId) { | |
| sightVal = 0; // Friendly | |
| } else { | |
| sightVal = -1; // Neutral/Enemy | |
| } | |
| foundObj = true; break; | |
| } | |
| } | |
| // 6. Smell (Square Radius) | |
| let smellVal = -1; | |
| let foundFoodSmell = false, foundNxerSmell = false; | |
| for(let dy = -nxer.smellRadius; dy <= nxer.smellRadius; dy++) { | |
| for(let dx = -nxer.smellRadius; dx <= nxer.smellRadius; dx++) { | |
| if(dx===0 && dy===0) continue; | |
| const sx = (nxer.pos[0] + dx + this.world.N) % this.world.N; | |
| const sy = nxer.pos[1] + dy; | |
| if(sy < 0 || sy >= this.world.N) continue; | |
| const k = `${sx},${sy}`; | |
| if(foodAt.has(k)) foundFoodSmell = true; | |
| if(occupantAt.has(k)) foundNxerSmell = true; | |
| } | |
| } | |
| if(foundFoodSmell) smellVal = 1; | |
| else if(foundNxerSmell) smellVal = 0; | |
| // Construct full inputs (Last 3 + 3 New) | |
| const inputs = [...nxer.lastInputs.slice(0, 3), hungerVal, sightVal, smellVal]; | |
| const steps = Math.max(1, Math.floor(nxer.net.params.simulationSteps / this.config.globalTimeSteps)); | |
| if (nxer.dopamineBoostTicks > 0) { | |
| nxer.net.neuromodulators.dopamine = Math.max(nxer.net.neuromodulators.dopamine, 0.9); | |
| nxer.dopamineBoostTicks--; | |
| } | |
| for(let i=0; i<steps; i++) { nxer.net.setInputStates(inputs); nxer.net.simulateStep(); } | |
| // Get Outputs (Now 5) | |
| // O1=X, O2=Y, O3=Steal, O4=Mate, O5=GiveFood | |
| let outs = nxer.net.getOutputStates(); // returns array | |
| while(outs.length < 5) outs.push(0); | |
| let [O1, O2, O3, O4, O5] = outs; | |
| if (O1 === 0 && O2 === 0 && Math.random() < 0.4) { | |
| if(Math.random() < 0.5) O1 = Math.random()<0.5?-1:1; else O2 = Math.random()<0.5?-1:1; | |
| } | |
| if (O4 === 0 && Math.random() < 0.08) O4 = Math.random() < 0.5 ? -1 : 1; | |
| // --- OUTPUT 5: GIVE FOOD (Altruism) --- | |
| if (O5 >= 0 && nxer.food > this.config.startFood) { | |
| // Check neighbors | |
| for(let ndy=-1; ndy<=1; ndy++) { | |
| for(let ndx=-1; ndx<=1; ndx++) { | |
| if(ndx===0 && ndy===0) continue; | |
| const tx = (nxer.pos[0] + ndx + this.world.N) % this.world.N; | |
| const ty = nxer.pos[1] + ndy; | |
| if(ty < 0 || ty >= this.world.N) continue; | |
| const rec = occupantAt.get(`${tx},${ty}`); | |
| // Only give to Clan members | |
| if(rec && nxer.clanId !== null && rec.clanId === nxer.clanId) { | |
| if(rec.food < this.config.startFood) { | |
| const amt = (O5 === 1) ? 5.0 : 2.0; | |
| if(nxer.food > amt) { | |
| nxer.food -= amt; | |
| rec.food += amt; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| const dx = -O1; | |
| const dy = -O2; | |
| // Update Heading based on move | |
| if(dx !== 0 || dy !== 0) { | |
| // Map dx,dy to 0-7 | |
| if(dx===-1 && dy===-1) nxer.heading=0; | |
| else if(dx===0 && dy===-1) nxer.heading=1; | |
| else if(dx===1 && dy===-1) nxer.heading=2; | |
| else if(dx===1 && dy===0) nxer.heading=3; | |
| else if(dx===1 && dy===1) nxer.heading=4; | |
| else if(dx===0 && dy===1) nxer.heading=5; | |
| else if(dx===-1 && dy===1) nxer.heading=6; | |
| else if(dx===-1 && dy===0) nxer.heading=7; | |
| } | |
| nxer.pendingMove = {dx, dy, O3, O4}; | |
| } | |
| }); | |
| this.processMovements(processNxErs.filter(n => n.pendingMove)); | |
| this.tryRespawnFood(); | |
| this.effects = this.effects.filter(e => this.stepTick - e.startTick < this.config.globalTimeSteps); | |
| if (Array.from(this.nxers.values()).filter(n => n.alive).length === 0) this.gameOver = true; | |
| } | |
| processMovements(nxers) { | |
| const intents = []; | |
| for (const nxer of nxers) { | |
| if (!nxer.pendingMove) continue; | |
| const {dx, dy, O3, O4} = nxer.pendingMove; delete nxer.pendingMove; | |
| if (dx===0 && dy===0) { | |
| nxer.lastInputs = [-1, 0, this.world.terrain(nxer.pos[0], nxer.pos[1]) === TERRAIN_LAND ? 1 : 0]; | |
| continue; | |
| } | |
| // Cylinder Topology (Wrap X, Clamp Y) | |
| const tx = (nxer.pos[0] + dx + this.world.N) % this.world.N; | |
| const rawTy = nxer.pos[1] + dy; | |
| if (rawTy < 0 || rawTy >= this.world.N) { | |
| nxer.lastInputs = [-1, 0, -1]; // Wall hit | |
| continue; | |
| } | |
| const ty = rawTy; | |
| intents.push({nxer, target: [tx, ty], O3, O4}); | |
| } | |
| const byPos = {}; | |
| intents.forEach(i => { | |
| const k = `${i.target[0]},${i.target[1]}`; | |
| if(!byPos[k]) byPos[k] = []; | |
| byPos[k].push(i); | |
| }); | |
| const occupants = {}; this.nxers.forEach(n => { if(n.alive) occupants[`${n.pos[0]},${n.pos[1]}`] = n; }); | |
| const foods = {}; this.foods.forEach(f => { if(f.alive) foods[`${f.pos[0]},${f.pos[1]}`] = f; }); | |
| for (const [key, list] of Object.entries(byPos)) { | |
| const [tx, ty] = key.split(',').map(Number); | |
| const terrain = this.world.terrain(tx, ty); | |
| if (terrain === TERRAIN_ROCK) { | |
| list.forEach(i => i.nxer.lastInputs = [-1, 0, -1]); | |
| continue; | |
| } | |
| const valid = list.filter(i => { | |
| const n = i.nxer; | |
| const ot = this.world.terrain(n.pos[0], n.pos[1]); | |
| if ((ot===1 && terrain===0) || (ot===0 && terrain===1)) return true; | |
| if (terrain===1 && !n.canLand) { n.lastInputs = [-1,0,1]; return false; } | |
| if (terrain===0 && !n.canSea) { n.lastInputs = [-1,0,0]; return false; } | |
| return true; | |
| }); | |
| if (!valid.length) continue; | |
| // Food Interaction | |
| if (foods[key]) { | |
| const f = foods[key]; | |
| valid.forEach(i => { | |
| i.nxer.dopamineBoostTicks = 30; | |
| if(f.remaining>0) { f.remaining--; i.nxer.food++; i.nxer.stats.foodFound++; } | |
| i.nxer.lastInputs = [1,0,terrain===1?1:0]; | |
| }); | |
| if(f.remaining <= 0) { | |
| const winner = valid.sort((a,b) => a.nxer.ticksPerAction - b.nxer.ticksPerAction)[0].nxer; | |
| this.moveNxEr(winner, [tx, ty]); | |
| f.alive = false; f.respawnAtTick = this.stepTick + this.config.foodRespawn * this.config.globalTimeSteps; | |
| } | |
| continue; | |
| } | |
| // Occupied Interaction | |
| if (occupants[key]) { | |
| const occ = occupants[key]; | |
| valid.forEach(i => { | |
| const n = i.nxer; | |
| if (n.id === occ.id) return; | |
| n.lastInputs = [-1, 1, terrain===1?1:0]; | |
| // --- TUNED MATING LOGIC --- | |
| // 1. Explicit Desire (Neural Output) OR Very Low Chance (0.2%) | |
| // 2. Must have significant food reserves ( > 15) to prioritize survival over reproduction | |
| const naturalUrge = Math.random() < 0.002; | |
| const wantsMate = (i.O4 === 1 || naturalUrge) && n.food > 15; | |
| if (wantsMate && this.canMate(n, occ)) { | |
| this.mate(n, occ); | |
| } else if ((i.O4 === -1 || i.O3 === -1) && occ.food > 0) { | |
| occ.food--; n.food++; n.stats.foodTaken++; | |
| } | |
| n.lastO4 = i.O4; | |
| }); | |
| continue; | |
| } | |
| // Empty Cell Meeting | |
| const mates = valid.filter(i => | |
| (i.O4===1 || Math.random() < 0.002) && | |
| i.nxer.food >= 15 && | |
| !i.nxer.matingWith | |
| ); | |
| if(mates.length >= 2) { | |
| const a = mates[0].nxer; | |
| const b = mates[1].nxer; | |
| if(this.canMate(a, b)) this.mate(a, b); | |
| valid.forEach(i => { i.nxer.lastInputs = [-1, 1, terrain===1?1:0]; i.nxer.lastO4 = i.O4; }); | |
| continue; | |
| } | |
| // Move Winner | |
| const winner = valid.sort((a,b) => a.nxer.ticksPerAction - b.nxer.ticksPerAction)[0].nxer; | |
| this.moveNxEr(winner, [tx, ty]); | |
| valid.forEach(i => { if(i.nxer.id !== winner.id) i.nxer.lastInputs=[-1,1,terrain===1?1:0]; i.nxer.lastO4 = i.O4; }); | |
| } | |
| } | |
| canMate(A, B) { | |
| // 1. Self Check | |
| if (A.id === B.id) return false; | |
| // 2. Life Check | |
| if (!A.alive || !B.alive) return false; | |
| // 3. Gender Check (Must be opposites) | |
| if (A.isMale === B.isMale) return false; | |
| // 4. Status Check (Must be free) | |
| if (A.matingWith !== null || B.matingWith !== null) return false; | |
| // 5. Energy Check (Must have enough food) | |
| if (A.food < 5 || B.food < 5) return false; | |
| // 6. Cooldown Check | |
| if (this.stepTick < A.mateCooldownUntilTick || this.stepTick < B.mateCooldownUntilTick) return false; | |
| // 7. Incest Check | |
| if (A.parents.includes(B.id) || B.parents.includes(A.id)) return false; | |
| return true; | |
| } | |
| moveNxEr(nxer, target, terrain) { | |
| this.occupied.delete(`${nxer.pos[0]},${nxer.pos[1]}`); | |
| if (nxer.pos[0] !== target[0] || nxer.pos[1] !== target[1]) { | |
| nxer.lastPos = [...nxer.pos]; nxer.lastMoveTick = this.stepTick; | |
| } | |
| nxer.pos = [...target]; | |
| this.occupied.add(`${target[0]},${target[1]}`); | |
| nxer.visited.add(`${target[0]},${target[1]}`); | |
| nxer.lastInputs = [-1, 0, terrain === TERRAIN_LAND ? 1 : 0]; | |
| nxer.food -= 0.1; | |
| if (nxer.food <= 0) this.killNxEr(nxer); | |
| } | |
| killNxEr(nxer) { | |
| if (!nxer.alive) return; | |
| nxer.alive = false; this.deathsCount++; | |
| this.occupied.delete(`${nxer.pos[0]},${nxer.pos[1]}`); | |
| this.effects.push({type: 'skull', pos: [...nxer.pos], startTick: this.stepTick}); | |
| } | |
| tryRespawnFood() { | |
| const aliveCount = Array.from(this.foods.values()).filter(f => f.alive).length; | |
| if (aliveCount >= this.config.maxFood) return; | |
| for (const f of this.foods.values()) { | |
| if (!f.alive && f.respawnAtTick && this.stepTick >= f.respawnAtTick) { | |
| const pos = this.findFreePosition(true, true, f.anchor, 6); | |
| if (pos) { f.pos = pos; f.alive = true; f.respawnAtTick = null; f.remaining = 25; } | |
| if (Array.from(this.foods.values()).filter(x => x.alive).length >= this.config.maxFood) break; | |
| } | |
| } | |
| } | |
| updateAllTimeBest() { | |
| const current = Array.from(this.nxers.values()); | |
| if (!current.length) return; | |
| const cats = { foodFound: 'foodFound', foodTaken: 'foodTaken', explored: 'explored', timeLived: 'timeLived', matesPerformed: 'matesPerformed', fitness: 'fitness' }; | |
| for (const [key, prop] of Object.entries(cats)) { | |
| const combined = [...this.allTimeBest[key], ...current.slice().sort((a, b) => b.stats[prop] - a.stats[prop]).slice(0, 3)]; | |
| const seen = new Set(), unique = []; | |
| combined.forEach(n => { const b = stripLeadingDigits(n.name).replace(/^-+/, '') || n.name; if(!seen.has(b)) { seen.add(b); unique.push(n); } }); | |
| this.allTimeBest[key] = unique.sort((a, b) => b.stats[prop] - a.stats[prop]).slice(0, 5); | |
| } | |
| } | |
| restartWithChampions() { | |
| this.updateAllTimeBest(); | |
| const champions = []; | |
| const addChamp = (list) => { if(list && list.length) champions.push(list[0]); }; | |
| Object.values(this.allTimeBest).forEach(list => addChamp(list)); | |
| this.gameIndex++; | |
| this.stepTick = 0; | |
| this.birthsCount = 0; | |
| this.deathsCount = 0; | |
| this.effects = []; | |
| this.paused = false; | |
| this.gameOver = false; | |
| this.world = new World(this.config.worldSize, this.config.seaPct/100, this.config.rockPct/100, this.config.useEarth); | |
| this.nxers.clear(); | |
| this.foods.clear(); | |
| this.occupied.clear(); | |
| this.initializeFood(); | |
| let id = 0; | |
| champions.slice(0, 10).forEach(c => { | |
| const name = stripLeadingDigits(c.name).replace(/^-+/, '') || c.name; | |
| this.championCounts[name] = (this.championCounts[name]||0)+1; | |
| const pos = this.findFreePosition(c.canSea, c.canLand) || [0,0]; | |
| const p = c.net.params.clone(); | |
| // Inherit new sensory stats or randomize if champion is from old version | |
| const vision = c.visionRange || 5; | |
| const smell = c.smellRadius || 3; | |
| const n = new NxEr({ | |
| id: id++, | |
| name: `${this.championCounts[name]}${name}`, | |
| color: this.randomColor(), | |
| pos, | |
| canLand: c.canLand, | |
| canSea: c.canSea, | |
| net: new NeuraxonNetwork(p), | |
| food: this.config.startFood * 1.1, | |
| isMale: c.isMale, | |
| bornTick: 0, | |
| lastMoveTick: 0, | |
| ticksPerAction: Math.max(2, Math.floor((this.config.globalTimeSteps * 1.2) / Math.max(1, p.simulationSteps))), | |
| visionRange: vision, | |
| smellRadius: smell, | |
| heading: randomInt(0, 7), | |
| clanId: null // Reset clan for fresh game start | |
| }); | |
| this.nxers.set(n.id, n); | |
| this.occupied.add(`${pos[0]},${pos[1]}`); | |
| }); | |
| while (this.nxers.size < this.config.startingNxErs) { | |
| const n = this.createNxEr(id++); | |
| this.nxers.set(n.id, n); | |
| this.occupied.add(`${n.pos[0]},${n.pos[1]}`); | |
| } | |
| } | |
| getStats() { | |
| const alive = Array.from(this.nxers.values()).filter(n => n.alive); | |
| let avgE = 0, avgB = 0; | |
| if (alive.length) { | |
| avgE = alive.reduce((s, n) => s + n.net.getEnergyStatus().averageEnergy, 0) / alive.length; | |
| avgB = alive.reduce((s, n) => s + n.net.branchingRatio, 0) / alive.length; | |
| } | |
| return { alive: alive.length, dead: this.deathsCount, born: this.birthsCount, avgEnergy: avgE.toFixed(1), avgBranching: avgB.toFixed(2) }; | |
| } | |
| } | |
| // ===================================================================== | |
| // 3D RENDERER (THREE.JS) | |
| // ===================================================================== | |
| class Renderer3D { | |
| constructor(canvas) { | |
| this.canvas = canvas; | |
| this.scene = new THREE.Scene(); | |
| this.scene.background = new THREE.Color(0x101015); | |
| this.scene.fog = new THREE.Fog(0x101015, 500, 2000); | |
| this.camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| this.camera.position.set(25, 40, 50); | |
| this.webgl = new THREE.WebGLRenderer({ canvas: this.canvas, antialias: true }); | |
| this.webgl.setSize(window.innerWidth, window.innerHeight); | |
| this.webgl.shadowMap.enabled = true; | |
| this.controls = new OrbitControls(this.camera, this.canvas); | |
| this.controls.enableDamping = true; | |
| this.controls.dampingFactor = 0.05; | |
| const ambientLight = new THREE.AmbientLight(0x404040, 3.5); | |
| this.scene.add(ambientLight); | |
| const hemiLight = new THREE.HemisphereLight(0xffffff, 0x222222, 2.0); | |
| this.scene.add(hemiLight); | |
| const dirLight = new THREE.DirectionalLight(0xffffff, 3.0); | |
| dirLight.position.set(100, 50, 100); | |
| dirLight.castShadow = true; | |
| dirLight.shadow.mapSize.width = 4096; | |
| dirLight.shadow.mapSize.height = 4096; | |
| const d = 200; | |
| dirLight.shadow.camera.left = -d; dirLight.shadow.camera.right = d; | |
| dirLight.shadow.camera.top = d; dirLight.shadow.camera.bottom = -d; | |
| dirLight.shadow.camera.near = 0.5; dirLight.shadow.camera.far = 500; | |
| this.scene.add(dirLight); | |
| this.nxerMeshes = new Map(); | |
| this.foodMeshes = new Map(); | |
| this.effectMeshes = []; | |
| this.terrainMesh = null; | |
| // NEW: Visual helpers for selection | |
| this.selectionHelpers = { | |
| sightLine: null, | |
| smellRing: null, | |
| group: new THREE.Group() | |
| }; | |
| this.scene.add(this.selectionHelpers.group); | |
| this.raycaster = new THREE.Raycaster(); | |
| this.mouse = new THREE.Vector2(); | |
| this.selectedNxerId = null; | |
| window.addEventListener('resize', () => this.onWindowResize()); | |
| } | |
| // ... [Keep onWindowResize, getSpherePos, createEmojiTexture, initTerrain as they were] ... | |
| onWindowResize() { | |
| this.camera.aspect = window.innerWidth / window.innerHeight; | |
| this.camera.updateProjectionMatrix(); | |
| this.webgl.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| getSpherePos(x, y, worldN, radius) { | |
| const phi = (x / worldN) * Math.PI * 2; | |
| const v = y / worldN; | |
| const theta = (v * 0.99 + 0.005) * Math.PI; | |
| const px = radius * Math.sin(theta) * Math.cos(phi); | |
| const py = radius * Math.cos(theta); | |
| const pz = radius * Math.sin(theta) * Math.sin(phi); | |
| return new THREE.Vector3(px, py, pz); | |
| } | |
| createEmojiTexture(emoji) { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = 64; canvas.height = 64; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.font = '48px "Segoe UI Emoji", sans-serif'; | |
| ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; | |
| ctx.fillText(emoji, 32, 32); | |
| return new THREE.CanvasTexture(canvas); | |
| } | |
| initTerrain(world) { | |
| // [Code same as provided in previous turn] | |
| if (this.terrainMesh) { | |
| if(this.terrainMesh.geometry) this.terrainMesh.geometry.dispose(); | |
| if(this.terrainMesh.material) this.terrainMesh.material.dispose(); | |
| this.scene.remove(this.terrainMesh); | |
| } | |
| if (this.coreSphere) this.scene.remove(this.coreSphere); | |
| this.planetRadius = (world.N * 0.8) / 2; | |
| const coreGeo = new THREE.SphereGeometry(this.planetRadius, 64, 64); | |
| const coreMat = new THREE.MeshStandardMaterial({ color: 0x1964c8, roughness: 0.2, metalness: 0.1 }); | |
| this.coreSphere = new THREE.Mesh(coreGeo, coreMat); | |
| this.coreSphere.receiveShadow = true; | |
| this.scene.add(this.coreSphere); | |
| const geometry = new THREE.IcosahedronGeometry(this.planetRadius, 40); | |
| const count = geometry.attributes.position.count; | |
| const posAttr = geometry.attributes.position; | |
| const colAttr = new THREE.BufferAttribute(new Float32Array(count * 3), 3); | |
| const vertex = new THREE.Vector3(); | |
| const color = new THREE.Color(); | |
| for (let i = 0; i < count; i++) { | |
| vertex.fromBufferAttribute(posAttr, i); | |
| const normal = vertex.clone().normalize(); | |
| let phi = Math.atan2(vertex.z, vertex.x); | |
| if (phi < 0) phi += Math.PI * 2; | |
| const u = 1.0 - (phi / (Math.PI * 2)); | |
| const theta = Math.acos(Math.max(-1, Math.min(1, vertex.y / this.planetRadius))); | |
| const v = theta / Math.PI; | |
| const uOffset = (u + 0.25) % 1.0; | |
| const gx = Math.floor(uOffset * world.N) % world.N; | |
| const gy = Math.floor(v * world.N) % world.N; | |
| const t = world.terrain(gx, gy); | |
| let h = -0.1; | |
| if (t === 1) { h = 0.3; color.setHex(0x28b43c); } | |
| else if (t === 2) { h = 1.0; color.setHex(0x6e6e6e); } | |
| else { color.setHex(0x1964c8); } | |
| vertex.copy(normal).multiplyScalar(this.planetRadius + h); | |
| posAttr.setXYZ(i, vertex.x, vertex.y, vertex.z); | |
| colAttr.setXYZ(i, color.r, color.g, color.b); | |
| } | |
| geometry.setAttribute('color', colAttr); | |
| geometry.computeVertexNormals(); | |
| const mat = new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.8, flatShading: false }); | |
| this.terrainMesh = new THREE.Mesh(geometry, mat); | |
| this.terrainMesh.castShadow = true; | |
| this.terrainMesh.receiveShadow = true; | |
| this.scene.add(this.terrainMesh); | |
| this.controls.target.set(0, 0, 0); | |
| this.camera.position.set(0, 0, this.planetRadius * 2.5); | |
| this.controls.minPolarAngle = 0; | |
| this.controls.maxPolarAngle = Math.PI; | |
| this.controls.minDistance = this.planetRadius * 1.2; | |
| this.controls.maxDistance = this.planetRadius * 6; | |
| } | |
| clearEntities() { | |
| this.nxerMeshes.forEach(m => this.scene.remove(m)); this.nxerMeshes.clear(); | |
| this.foodMeshes.forEach(m => this.scene.remove(m)); this.foodMeshes.clear(); | |
| this.effectMeshes.forEach(e => this.scene.remove(e.mesh)); this.effectMeshes = []; | |
| this.clearSelectionVisuals(); | |
| } | |
| // NEW: Clears visual helpers | |
| clearSelectionVisuals() { | |
| // Remove Line | |
| if (this.selectionHelpers.sightLine) { | |
| this.selectionHelpers.group.remove(this.selectionHelpers.sightLine); | |
| if (this.selectionHelpers.sightLine.geometry) this.selectionHelpers.sightLine.geometry.dispose(); | |
| if (this.selectionHelpers.sightLine.material) this.selectionHelpers.sightLine.material.dispose(); | |
| this.selectionHelpers.sightLine = null; | |
| } | |
| // Hide Ring (don't dispose geometry, just hide/reset) | |
| if (this.selectionHelpers.smellRing) { | |
| this.selectionHelpers.smellRing.visible = false; | |
| } | |
| } | |
| updateSelectionVisuals(nxer, world) { | |
| if (!nxer || !nxer.alive) { | |
| this.clearSelectionVisuals(); | |
| return; | |
| } | |
| // --- 1. SETUP HELPERS IF MISSING --- | |
| if (!this.selectionHelpers.smellRing) { | |
| // TUBE THICKNESS INCREASED: 0.03 -> 0.1 for visibility | |
| const geometry = new THREE.TorusGeometry(1, 0.1, 16, 64); | |
| // DOUBLE SIDE & DEPTH TEST FALSE ensures it's seen even if slightly underground | |
| const material = new THREE.MeshBasicMaterial({ | |
| color: 0xffff00, | |
| transparent: true, | |
| opacity: 0.6, | |
| side: THREE.DoubleSide, | |
| depthTest: false // Optional: makes it always render on top | |
| }); | |
| const ring = new THREE.Mesh(geometry, material); | |
| // Important: renderOrder ensures it draws on top of terrain | |
| ring.renderOrder = 999; | |
| this.selectionHelpers.group.add(ring); | |
| this.selectionHelpers.smellRing = ring; | |
| } | |
| // --- 2. CALCULATE START POSITION --- | |
| const t = world.terrain(nxer.pos[0], nxer.pos[1]); | |
| // INCREASED OFFSET: Lifted higher (+0.8) to avoid z-fighting with mountains | |
| const hOffset = (t === 1 ? 0.3 : (t === 2 ? 1.0 : 0.0)) + 0.8; | |
| const startPos = this.getSpherePos(nxer.pos[0], nxer.pos[1], world.N, this.planetRadius + hOffset); | |
| // --- 3. UPDATE SMELL RING (Yellow) --- | |
| const ring = this.selectionHelpers.smellRing; | |
| ring.visible = true; | |
| ring.position.copy(startPos); | |
| // Orient ring tangent to sphere surface | |
| ring.lookAt(0, 0, 0); | |
| // Calculate Scale | |
| const gridUnitSize = (2 * Math.PI * this.planetRadius) / world.N; | |
| const ringRadius = (nxer.smellRadius || 3) * gridUnitSize; | |
| ring.scale.set(ringRadius, ringRadius, 1); | |
| // --- 4. UPDATE SIGHT LINE (Blue Curve) --- | |
| if (this.selectionHelpers.sightLine) { | |
| this.selectionHelpers.group.remove(this.selectionHelpers.sightLine); | |
| if(this.selectionHelpers.sightLine.geometry) this.selectionHelpers.sightLine.geometry.dispose(); | |
| this.selectionHelpers.sightLine = null; | |
| } | |
| const DIR_OFFSETS = [ | |
| [-1, -1], [0, -1], [1, -1], [1, 0], | |
| [1, 1], [0, 1], [-1, 1], [-1, 0] | |
| ]; // 0=NW to 7=W | |
| // Fallback if heading is undefined | |
| const headingIndex = (nxer.heading !== undefined) ? nxer.heading : 0; | |
| const dir = DIR_OFFSETS[headingIndex] || [0,0]; | |
| // Calculate Target | |
| const vRange = nxer.visionRange || 5; | |
| // Handle wrapping for X, clamping for Y | |
| let tx = (nxer.pos[0] + (dir[0] * vRange)); | |
| tx = ((tx % world.N) + world.N) % world.N; | |
| let ty = nxer.pos[1] + (dir[1] * vRange); | |
| ty = clamp(ty, 0, world.N - 1); | |
| const tt = world.terrain(tx, ty); | |
| const hOffsetT = (tt === 1 ? 0.3 : (tt === 2 ? 1.0 : 0.0)) + 0.8; | |
| const endPos = this.getSpherePos(tx, ty, world.N, this.planetRadius + hOffsetT); | |
| const midPos = startPos.clone().add(endPos).normalize().multiplyScalar(this.planetRadius + hOffset + (vRange * 0.15)); | |
| const curve = new THREE.QuadraticBezierCurve3(startPos, midPos, endPos); | |
| const points = curve.getPoints(12); | |
| const lineGeo = new THREE.BufferGeometry().setFromPoints(points); | |
| const lineMat = new THREE.LineBasicMaterial({ | |
| color: 0x00ffff, | |
| linewidth: 2, | |
| depthTest: false // Always visible | |
| }); | |
| const line = new THREE.Line(lineGeo, lineMat); | |
| line.renderOrder = 999; // Draw on top | |
| this.selectionHelpers.sightLine = line; | |
| this.selectionHelpers.group.add(line); | |
| } | |
| updateNxErs(nxers, world) { | |
| const N = world.N; | |
| for(const [id, m] of this.nxerMeshes) { | |
| if(!nxers.has(id) || !nxers.get(id).alive) { this.scene.remove(m); this.nxerMeshes.delete(id); } | |
| } | |
| for(const nxer of nxers.values()) { | |
| if(!nxer.alive) continue; | |
| let mesh = this.nxerMeshes.get(nxer.id); | |
| const t = world.terrain(nxer.pos[0], nxer.pos[1]); | |
| let hOffset = 0.0; | |
| if (t === 1) hOffset = 0.3; else if (t === 2) hOffset = 1.0; | |
| const targetPos = this.getSpherePos(nxer.pos[0], nxer.pos[1], N, this.planetRadius + hOffset + 0.35); | |
| if(!mesh) { | |
| const g = new THREE.SphereGeometry(0.35, 16, 16); | |
| const m = new THREE.MeshStandardMaterial({ color: nxer.color }); | |
| mesh = new THREE.Mesh(g, m); mesh.castShadow = true; | |
| mesh.userData = { id: nxer.id, type: 'nxer' }; | |
| const r = new THREE.Mesh(new THREE.TorusGeometry(0.5, 0.03, 8, 24), new THREE.MeshBasicMaterial({ color: 0xff0000 })); | |
| r.rotation.x = Math.PI/2; mesh.add(r); | |
| this.scene.add(mesh); this.nxerMeshes.set(nxer.id, mesh); | |
| mesh.position.copy(targetPos); | |
| } | |
| const dist = mesh.position.distanceTo(targetPos); | |
| if(dist > this.planetRadius) { | |
| mesh.position.copy(targetPos); | |
| mesh.visible = false; | |
| } else { | |
| mesh.visible = true; | |
| const c = mesh.position.clone().normalize(); | |
| const t = targetPos.clone().normalize(); | |
| c.lerp(t, 0.15); | |
| mesh.position.copy(c.multiplyScalar(this.planetRadius + hOffset + 0.35)); | |
| } | |
| const up = new THREE.Vector3(0,1,0); | |
| const norm = mesh.position.clone().normalize(); | |
| mesh.setRotationFromQuaternion(new THREE.Quaternion().setFromUnitVectors(up, norm)); | |
| const ring = mesh.children[0]; | |
| if(ring) { | |
| const es = nxer.net.getEnergyStatus(); | |
| const s = Math.max(0.1, Math.min(1.5, es.averageEnergy/80)); | |
| ring.scale.set(s,s,s); | |
| // Highlight selected | |
| ring.material.color.setHex(nxer.id === this.selectedNxerId ? 0xffffff : 0xff0000); | |
| } | |
| } | |
| } | |
| // [updateFood, updateEffects, handleClick remain same] | |
| updateFood(foods, world) { | |
| for(const [id, m] of this.foodMeshes) if(!foods.has(id) || !foods.get(id).alive) { this.scene.remove(m); this.foodMeshes.delete(id); } | |
| for(const food of foods.values()) { | |
| if(!food.alive) continue; | |
| let mesh = this.foodMeshes.get(food.id); | |
| if(!mesh) { | |
| const g = new THREE.ConeGeometry(0.3, 0.9, 4); | |
| const m = new THREE.MeshStandardMaterial({ color: 0xdc2828, emissive: 0x500000 }); | |
| mesh = new THREE.Mesh(g, m); mesh.castShadow = true; | |
| this.scene.add(mesh); this.foodMeshes.set(food.id, mesh); | |
| } | |
| const t = world.terrain(food.pos[0], food.pos[1]); | |
| let h = 0.0; if(t===1) h=0.3; else if(t===2) h=1.0; | |
| const pos = this.getSpherePos(food.pos[0], food.pos[1], world.N, this.planetRadius + h + 0.45); | |
| mesh.position.copy(pos); | |
| const up = new THREE.Vector3(0,1,0); | |
| const norm = pos.clone().normalize(); | |
| mesh.setRotationFromQuaternion(new THREE.Quaternion().setFromUnitVectors(up, norm)); | |
| mesh.rotateY(0.02); | |
| } | |
| } | |
| updateEffects(engine) { | |
| this.effectMeshes = this.effectMeshes.filter(ef => { | |
| const age = engine.stepTick - ef.startTick; | |
| const upVec = ef.mesh.position.clone().normalize(); | |
| ef.mesh.position.add(upVec.multiplyScalar(0.15)); | |
| ef.mesh.material.opacity = 1.0 - (age / 60.0); | |
| if (age >= 60) { | |
| this.scene.remove(ef.mesh); | |
| if(ef.mesh.material.map) ef.mesh.material.map.dispose(); | |
| ef.mesh.material.dispose(); | |
| return false; | |
| } | |
| return true; | |
| }); | |
| if (!this.processedEffects) this.processedEffects = new Set(); | |
| engine.effects.forEach(e => { | |
| const eid = `${e.type}-${e.pos[0]}-${e.pos[1]}-${e.startTick}`; | |
| if (!this.processedEffects.has(eid)) { | |
| this.processedEffects.add(eid); | |
| const icon = e.type === 'heart' ? '❤️' : '💀'; | |
| const map = this.createEmojiTexture(icon); | |
| const mat = new THREE.SpriteMaterial({ map: map, transparent: true }); | |
| const sprite = new THREE.Sprite(mat); | |
| const pos = this.getSpherePos(e.pos[0], e.pos[1], engine.world.N, this.planetRadius + 1.5); | |
| sprite.position.copy(pos); | |
| sprite.scale.set(1.5, 1.5, 1.5); | |
| this.scene.add(sprite); | |
| this.effectMeshes.push({ mesh: sprite, startTick: e.startTick }); | |
| } | |
| }); | |
| if (this.processedEffects.size > 100) this.processedEffects.clear(); | |
| } | |
| handleClick(x, y) { | |
| this.mouse.x = (x/window.innerWidth)*2-1; this.mouse.y = -(y/window.innerHeight)*2+1; | |
| this.raycaster.setFromCamera(this.mouse, this.camera); | |
| const hits = this.raycaster.intersectObjects(this.scene.children, true); | |
| for(const hit of hits) { | |
| let o = hit.object; | |
| while(o) { if(o.userData && o.userData.type==='nxer') return o.userData.id; o = o.parent; } | |
| } | |
| return null; | |
| } | |
| render(engine) { | |
| this.updateNxErs(engine.nxers, engine.world); | |
| this.updateFood(engine.foods, engine.world); | |
| this.updateEffects(engine); | |
| // NEW: Update Selection Visuals every frame | |
| if (this.selectedNxerId !== null && engine.nxers.has(this.selectedNxerId)) { | |
| this.updateSelectionVisuals(engine.nxers.get(this.selectedNxerId), engine.world); | |
| } else if (this.selectionHelpers.sightLine || this.selectionHelpers.smellRing) { | |
| this.clearSelectionVisuals(); | |
| } | |
| this.controls.update(); | |
| this.webgl.render(this.scene, this.camera); | |
| } | |
| } | |
| // ===================================================================== | |
| // UI CONTROLLER | |
| // ===================================================================== | |
| class UIController { | |
| constructor(engine, renderer) { | |
| this.engine = engine; | |
| this.renderer = renderer; | |
| this.hud = document.getElementById('hud'); | |
| this.hudTitle = document.getElementById('hudTitle'); | |
| this.hudContent = document.getElementById('hudContent'); | |
| this.hudStats = document.getElementById('hudStats'); | |
| this.buttonGroup = document.getElementById('buttonGroup'); | |
| this.detailPanel = document.getElementById('detailPanel'); | |
| this.detailTitle = document.getElementById('detailTitle'); | |
| this.detailContent = document.getElementById('detailContent'); | |
| this.gameOverModal = document.getElementById('gameOverModal'); | |
| this.gameOverOverlay = document.getElementById('gameOverOverlay'); | |
| this.controlsHint = document.getElementById('controlsHint'); | |
| this.setupButtons(); | |
| this.setupKeyboard(); | |
| this.setupMouse(); | |
| } | |
| setupButtons() { | |
| this.buttonGroup.innerHTML = ` | |
| <div class="button-row"><button class="hud-button" id="pauseBtn">Play</button></div> | |
| <div class="button-row"><button class="hud-button" id="saveBestBtn">Save Bests</button></div> | |
| <div class="button-row"><button class="hud-button" id="exitBtn">Exit</button></div> | |
| `; | |
| document.getElementById('pauseBtn').addEventListener('click', () => { | |
| this.engine.paused = !this.engine.paused; | |
| document.getElementById('pauseBtn').textContent = this.engine.paused ? 'Play' : 'Pause'; | |
| }); | |
| document.getElementById('exitBtn').addEventListener('click', () => { if (confirm('Return to menu?')) location.reload(); }); | |
| document.getElementById('saveBestBtn').addEventListener('click', () => this.saveBestChampions()); | |
| document.getElementById('restartYes').addEventListener('click', () => { | |
| this.engine.restartWithChampions(); | |
| this.renderer.clearEntities(); | |
| this.renderer.initTerrain(this.engine.world); | |
| this.gameOverModal.classList.add('hidden'); | |
| this.gameOverOverlay.classList.add('hidden'); | |
| }); | |
| document.getElementById('restartNo').addEventListener('click', () => location.reload()); | |
| document.getElementById('hideControlsHint').addEventListener('click', () => this.controlsHint.classList.add('hidden')); | |
| } | |
| setupKeyboard() { | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === ' ') { | |
| e.preventDefault(); | |
| if (!this.engine.gameOver) { | |
| this.engine.paused = !this.engine.paused; | |
| document.getElementById('pauseBtn').textContent = this.engine.paused ? 'Play' : 'Pause'; | |
| } | |
| } else if (e.key.toLowerCase() === 'h') { | |
| e.preventDefault(); | |
| this.hud.classList.toggle('hidden'); | |
| } | |
| }); | |
| } | |
| setupMouse() { | |
| this.renderer.canvas.addEventListener('click', (e) => { | |
| // Don't trigger if dragging | |
| const nxerId = this.renderer.handleClick(e.clientX, e.clientY); | |
| this.updateDetailPanel(nxerId); | |
| }); | |
| } | |
| update() { | |
| // 1. Update Title | |
| this.hudTitle.textContent = `Metrics: Round #${this.engine.gameIndex}`; | |
| // 2. Update Rankings | |
| const rankings = this.getRankings(); | |
| let html = ''; | |
| for (const [title, data] of Object.entries(rankings)) { | |
| html += `<div class="hud-section"><div class="hud-section-title">${title} (${data.best.toFixed(1)})</div>`; | |
| for (const entry of data.entries) { | |
| html += `<div class="hud-entry" data-nxer-id="${entry.id}"><span><span class="hud-dot" style="background:${entry.color}"></span>${entry.name}</span><span>${entry.value}</span></div>`; | |
| } | |
| html += `</div>`; | |
| } | |
| this.hudContent.innerHTML = html; | |
| // Re-attach click listeners for HUD items | |
| this.hudContent.querySelectorAll('.hud-entry').forEach(entry => { | |
| entry.addEventListener('click', () => { | |
| const nxerId = parseInt(entry.dataset.nxerId); | |
| this.renderer.selectedNxerId = nxerId; | |
| this.updateDetailPanel(nxerId); | |
| }); | |
| }); | |
| // 3. Stats Display | |
| const stats = this.engine.getStats(); | |
| this.hudStats.innerHTML = ` | |
| <div class="hud-stats">Alive: ${stats.alive}</div> | |
| <div class="hud-stats">Dead: ${stats.dead}</div> | |
| <div class="hud-stats">Born: ${stats.born}</div> | |
| <div class="hud-stats" style="margin-top: 8px;">Avg Energy: ${stats.avgEnergy}</div> | |
| <div class="hud-stats">Branching: ${stats.avgBranching}</div> | |
| `; | |
| // 4. Game Over Modal | |
| if (this.engine.gameOver) { | |
| this.gameOverModal.classList.remove('hidden'); | |
| this.gameOverOverlay.classList.remove('hidden'); | |
| } else { | |
| this.gameOverModal.classList.add('hidden'); | |
| this.gameOverOverlay.classList.add('hidden'); | |
| } | |
| // 5. REAL-TIME DETAIL UPDATE | |
| // This ensures the numbers and the 3D visuals update even when not paused | |
| if (this.renderer.selectedNxerId !== null) { | |
| this.updateDetailPanel(this.renderer.selectedNxerId); | |
| } | |
| } | |
| getRankings() { | |
| const all = Array.from(this.engine.nxers.values()); | |
| if (all.length === 0) return {}; | |
| // Sorters | |
| const byFood = all.slice().sort((a, b) => b.stats.foodFound - a.stats.foodFound); | |
| const byFoodTaken = all.slice().sort((a, b) => b.stats.foodTaken - a.stats.foodTaken); | |
| const byExplored = all.slice().sort((a, b) => b.stats.explored - a.stats.explored); | |
| const byLived = all.slice().sort((a, b) => b.stats.timeLived - a.stats.timeLived); | |
| const byMates = all.slice().sort((a, b) => b.stats.matesPerformed - a.stats.matesPerformed); | |
| const byFitness = all.slice().sort((a, b) => b.stats.fitness - a.stats.fitness); | |
| // Helper to format entries | |
| const format = (arr, key) => arr.slice(0, 3).map(n => ({ | |
| name: n.alive ? n.name : `${n.name}†`, | |
| value: typeof n.stats[key] === 'number' ? n.stats[key].toFixed(1) : '0.0', | |
| color: n.color, | |
| id: n.id | |
| })); | |
| // Helper to get best historical score | |
| const getBestScore = (category, arr) => { | |
| const allCandidates = [...arr, ...(this.engine.allTimeBest[category] || [])]; | |
| if (allCandidates.length === 0) return 0; | |
| return Math.max(...allCandidates.map(n => n.stats[category] || 0)); | |
| }; | |
| return { | |
| 'Food Found': { | |
| entries: format(byFood, 'foodFound'), | |
| best: getBestScore('foodFound', byFood) | |
| }, | |
| 'Food Stolen': { | |
| entries: format(byFoodTaken, 'foodTaken'), | |
| best: getBestScore('foodTaken', byFoodTaken) | |
| }, | |
| 'Explored': { | |
| entries: format(byExplored, 'explored'), | |
| best: getBestScore('explored', byExplored) | |
| }, | |
| 'Time Lived': { | |
| entries: format(byLived, 'timeLived'), | |
| best: getBestScore('timeLived', byLived) | |
| }, | |
| 'Mates': { | |
| entries: format(byMates, 'matesPerformed'), | |
| best: getBestScore('matesPerformed', byMates) | |
| }, | |
| 'Fitness': { | |
| entries: format(byFitness, 'fitness'), | |
| best: getBestScore('fitness', byFitness) | |
| } | |
| }; | |
| } | |
| updateDetailPanel(nxerId) { | |
| // Check if valid | |
| if (nxerId === null || !this.engine.nxers.has(nxerId)) { | |
| this.detailPanel.classList.add('hidden'); | |
| this.renderer.clearSelectionVisuals(); | |
| return; | |
| } | |
| const nxer = this.engine.nxers.get(nxerId); | |
| this.detailPanel.classList.remove('hidden'); | |
| // Update Title | |
| const gender = nxer.isMale ? 'Male' : 'Female'; | |
| this.detailTitle.textContent = `${nxer.name} (id ${nxer.id}) - ${gender}`; | |
| // Helper strings | |
| const terrain = nxer.canLand && !nxer.canSea ? 'Land' : | |
| nxer.canSea && !nxer.canLand ? 'Sea' : 'Both'; | |
| const energyStatus = nxer.net.getEnergyStatus(); | |
| const params = nxer.net.params; | |
| const clanDisplay = nxer.clanId !== null ? `Clan #${nxer.clanId}` : "No Clan"; | |
| // Convert Heading Integer to String | |
| const headings = ["NW", "N", "NE", "E", "SE", "S", "SW", "W"]; | |
| const headingStr = headings[nxer.heading] || "?"; | |
| this.detailContent.innerHTML = ` | |
| <div class="detail-info">Color: <span style="display:inline-block;width:10px;height:10px;background:${nxer.color};border-radius:50%"></span> ${nxer.color}</div> | |
| <div class="detail-info">Pos: [${nxer.pos[0]}, ${nxer.pos[1]}] Food: ${nxer.food.toFixed(1)}</div> | |
| <div class="detail-info">Alive: ${nxer.alive} Terrain: ${terrain}</div> | |
| <div class="detail-info"><strong>${clanDisplay}</strong></div> | |
| <div class="detail-section" style="border-top: 1px solid #444; margin-top: 5px; padding-top: 5px;"> | |
| <div style="color: #4a9eff; font-weight: bold; margin-bottom: 2px;">Sensory Data:</div> | |
| <div class="detail-info">👁️ Vision Range: ${nxer.visionRange}</div> | |
| <div class="detail-info">🧭 Facing: ${headingStr}</div> | |
| <div class="detail-info">👃 Smell Radius: ${nxer.smellRadius}</div> | |
| </div> | |
| <div class="detail-section"> | |
| <div class="detail-info">Lived: ${nxer.stats.timeLived.toFixed(1)}s</div> | |
| <div class="detail-info">Found: ${nxer.stats.foodFound.toFixed(1)} Stolen: ${nxer.stats.foodTaken.toFixed(1)}</div> | |
| <div class="detail-info">Mates: ${nxer.stats.matesPerformed} Explored: ${nxer.stats.explored}</div> | |
| <div class="detail-info">Energy: ${energyStatus.averageEnergy.toFixed(1)} Fitness: ${nxer.stats.fitness.toFixed(3)}</div> | |
| <div class="detail-info">Branching: ${energyStatus.branchingRatio.toFixed(2)}</div> | |
| </div> | |
| <div class="detail-section"> | |
| <div style="font-size: 14px; color: #c8c8c8; margin-bottom: 6px;">Network Parameters:</div> | |
| <div class="detail-info" style="font-size: 11px;">inputs=${params.numInputNeurons} hidden=${params.numHiddenNeurons} outputs=${params.numOutputNeurons}</div> | |
| <div class="detail-info" style="font-size: 11px;">conn=${params.connectionProbability.toFixed(2)} steps=${params.simulationSteps}</div> | |
| <div class="detail-info" style="font-size: 11px;">τ_fast=${params.tauFast.toFixed(1)} τ_slow=${params.tauSlow.toFixed(1)} τ_meta=${params.tauMeta.toFixed(0)}</div> | |
| <div class="detail-info" style="font-size: 11px;">θ_exc=${params.firingThresholdExcitatory.toFixed(2)} θ_inh=${params.firingThresholdInhibitory.toFixed(2)}</div> | |
| <div class="detail-info" style="font-size: 11px;">learn=${params.learningRate.toFixed(3)} stdp=${params.stdpWindow.toFixed(1)}</div> | |
| </div> | |
| `; | |
| // Re-bind save button if it exists | |
| const btn = document.getElementById('saveSingleBtn'); | |
| if(btn) btn.onclick = () => this.saveNxEr(nxer); | |
| } | |
| saveNxEr(nxer) { | |
| const data = { | |
| meta: { | |
| created: new Date().toISOString(), | |
| type: 'NxEr', | |
| engine: 'Neuraxon 3D v2' | |
| }, | |
| nxer: { | |
| name: nxer.name, | |
| color: nxer.color, | |
| canLand: nxer.canLand, | |
| canSea: nxer.canSea, | |
| isMale: nxer.isMale, | |
| // NEW: Save Clan and Senses | |
| visionRange: nxer.visionRange, | |
| smellRadius: nxer.smellRadius, | |
| heading: nxer.heading, | |
| clanId: nxer.clanId, | |
| stats: nxer.stats, | |
| networkParams: nxer.net.params | |
| } | |
| }; | |
| const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `nxer_${nxer.name}_3d.json`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| } | |
| saveBestChampions() { | |
| this.engine.updateAllTimeBest(); | |
| const data = { meta: { created: new Date().toISOString(), type: 'Neuraxon3D' }, champions: {} }; | |
| for (const [cat, nxers] of Object.entries(this.engine.allTimeBest)) { | |
| data.champions[cat] = nxers.map(n => ({ name: n.name, color: n.color, stats: n.stats, params: n.net.params })); | |
| } | |
| const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); a.href = url; a.download = `neuraxon_3d_bests.json`; a.click(); | |
| } | |
| } | |
| // ===================================================================== | |
| // MAIN APPLICATION | |
| // ===================================================================== | |
| class Application { | |
| constructor() { | |
| this.setupSplashScreen(); | |
| } | |
| setupSplashScreen() { | |
| const splash = document.getElementById('splashScreen'); | |
| splash.addEventListener('click', () => { | |
| splash.classList.add('hidden'); | |
| this.showConfigScreen(); | |
| }); | |
| } | |
| showConfigScreen() { | |
| const configScreen = document.getElementById('configScreen'); | |
| configScreen.classList.remove('hidden'); | |
| const sliders = ['worldSize','seaPct','rockPct','startingNxErs','maxNxErs','maxFood','foodRespawn','startFood','maxNeurons','globalTimeSteps','mateCooldown']; | |
| sliders.forEach(id => { | |
| const el = document.getElementById(id); | |
| const span = document.getElementById(id+'Value'); | |
| el.addEventListener('input', () => span.textContent = el.value + (id.includes('Pct')?'%':'')); | |
| }); | |
| document.getElementById('startButton').addEventListener('click', () => { | |
| const config = {}; | |
| sliders.forEach(id => config[id] = parseFloat(document.getElementById(id).value)); | |
| config.useEarth = document.getElementById('useEarth').checked; | |
| configScreen.classList.add('hidden'); | |
| document.getElementById('loader').style.display = 'block'; | |
| setTimeout(() => this.startGame(config), 100); | |
| }); | |
| } | |
| startGame(config) { | |
| const canvas = document.getElementById('gameCanvas'); | |
| canvas.classList.remove('hidden'); | |
| document.getElementById('hud').classList.remove('hidden'); | |
| document.getElementById('controlsHint').classList.remove('hidden'); | |
| document.getElementById('loader').style.display = 'none'; | |
| const engine = new GameEngine(config); | |
| const renderer = new Renderer3D(canvas); | |
| renderer.initTerrain(engine.world); | |
| const ui = new UIController(engine, renderer); | |
| let lastTime = performance.now(); | |
| let accumulator = 0; | |
| const fixedDt = 1.0 / 60.0; | |
| const gameLoop = (currentTime) => { | |
| const dt = Math.min((currentTime - lastTime) / 1000, 0.1); | |
| lastTime = currentTime; | |
| accumulator += dt; | |
| let steps = 0; | |
| while (accumulator >= fixedDt && steps < 10) { | |
| engine.step(); | |
| accumulator -= fixedDt; | |
| steps++; | |
| } | |
| if (steps >= 5) accumulator = 0; | |
| renderer.render(engine); | |
| ui.update(); | |
| requestAnimationFrame(gameLoop); | |
| }; | |
| requestAnimationFrame(gameLoop); | |
| } | |
| } | |
| new Application(); | |
| </script> | |
| </body> | |
| </html> |