NeuraxonLife / index.html
DavidVivancos's picture
Update index.html
f791bb4 verified
<!DOCTYPE html>
<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 !important; }
.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>