NeuraxonLife / index.html
DavidVivancos's picture
Update index.html
70faa4e verified
Raw History Blame Contribute Delete
266 kB
<!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 v5.0 Lite — CHC g-capable Multi-Sphere Brain + Best-g NxEr + Tonal Voices — By David Vivancos & Dr. Jose Sanchez for Qubic Science</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('GameOfLife5.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.82);
border: 1px solid #505050;
border-radius: 8px;
padding: 15px;
min-width: 340px;
max-width: 420px;
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; }
/* v4.0: Detail Panel Tabs */
.detail-tabs { display: flex; gap: 2px; margin-bottom: 8px; }
.detail-tab { flex: 1; padding: 5px 4px; text-align: center; font-size: 10px; font-weight: bold; color: #888; background: #1a1a22; border: 1px solid #333; border-bottom: 2px solid transparent; cursor: pointer; font-family: 'Consolas', monospace; border-radius: 4px 4px 0 0; }
.detail-tab.active { color: #40d0ff; border-bottom-color: #40d0ff; background: #1a1a2a; }
.detail-tab:hover { color: #ccc; }
.detail-tab-content { display: none; }
.detail-tab-content.active { display: block; }
/* v4.0: Sphere Cards in Detail */
.sphere-card { background: #12121a; border: 1px solid #2a2a3a; border-radius: 6px; padding: 6px 8px; margin-bottom: 4px; font-size: 11px; }
.sphere-card .sphere-header { display: flex; align-items: center; gap: 5px; margin-bottom: 3px; }
.sphere-card .sphere-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.sphere-card .sphere-name { font-weight: bold; color: #e0ecff; }
.sphere-card .sphere-type { font-size: 9px; color: #667; font-family: monospace; padding: 1px 4px; background: #0a0a12; border-radius: 3px; }
.sphere-card .sphere-stats { font-size: 10px; color: #999; font-family: monospace; }
/* v4.0: Link Cards */
.link-card { background: #12121a; border: 1px solid #2a2a3a; border-radius: 4px; padding: 4px 7px; margin-bottom: 3px; font-size: 10px; display: flex; align-items: center; gap: 4px; }
.link-card .link-type { font-family: monospace; font-size: 8px; padding: 1px 4px; border-radius: 3px; color: #000; font-weight: bold; }
.link-type-FF { background: #40d0ff; }
.link-type-FB { background: #ff5090; }
.link-type-LAT { background: #ffc040; }
/* v4.0: Oscillator Bars */
.osc-bars { display: flex; align-items: flex-end; gap: 2px; height: 24px; margin: 4px 0; }
.osc-bar { flex: 1; border-radius: 2px 2px 0 0; min-height: 2px; transition: height 0.15s; }
.osc-labels { display: flex; justify-content: space-between; font-size: 7px; color: #556; font-family: monospace; }
/* v4.0: Receptor Grid */
.receptor-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2px; }
.receptor-cell { padding: 2px 3px; border-radius: 3px; font-size: 8px; text-align: center; background: #0a0a15; border: 1px solid #222; font-family: monospace; }
.receptor-cell .rname { font-weight: bold; color: #e0ecff; }
.receptor-cell .rval { color: #40d0ff; }
/* v4.0: Neuromod Bars */
.nm-bar-row { display: flex; align-items: center; gap: 4px; margin-bottom: 3px; font-size: 10px; }
.nm-bar-label { width: 16px; text-align: center; }
.nm-bar-track { flex: 1; height: 5px; background: #1a1a2a; border-radius: 3px; overflow: hidden; }
.nm-bar-fill { height: 100%; border-radius: 3px; transition: width 0.2s; }
.nm-bar-val { width: 28px; text-align: right; font-family: monospace; font-size: 9px; color: #40d0ff; }
.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/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/",
"postprocessing": "https://unpkg.com/postprocessing@6.33.0/build/postprocessing.esm.js"
}
}
</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 5.0 Lite — CHC g-capable Multi-Sphere Brain</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">
<div class="slider-label">Day/Night Cycle Length (seconds per full cycle)</div>
<div class="slider-wrapper">
<input type="range" class="slider" id="dayNightCycle" min="60" max="600" value="180">
<span class="slider-value" id="dayNightCycleValue">180</span>
</div>
</div>
<div class="slider-container">
<div class="slider-label">Base Temperature (environment baseline °C)</div>
<div class="slider-wrapper">
<input type="range" class="slider" id="baseTemperature" min="15" max="35" value="25">
<span class="slider-value" id="baseTemperatureValue">25</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>
<div class="slider-container" style="margin-top: 12px; background: #101820; padding: 15px; border-radius: 8px; border: 1px solid #2a4a7a;">
<div class="slider-label" style="color: #40d0ff; font-weight: bold; margin-bottom: 8px;">🧠 Multi-Sphere Brain Architecture (v5.0 Lite)</div>
<div class="slider-wrapper" style="margin-bottom: 10px; align-items: flex-start;">
<span style="font-size: 13px; color: #b0c4de; min-width: 180px;">Brain Architecture</span>
<div style="flex:1;">
<select id="brainArch" style="width:100%; background:#0a0a12; color:#e0ecff; border:1px solid #2a4a7a; border-radius:4px; padding:5px 8px; font-size:13px;">
<option value="classic">Classic chain (2–5 spheres)</option>
<option value="chc6" selected>CHC g-capable (6-sphere — needed for g)</option>
</select>
<div id="brainArchHint" style="font-size:11px; color:#7799cc; margin-top:5px;">
CHC g-capable builds visual·auditory·intero → fluid·crystallised → motor with cross-battery (κ) coupling — the structure a general factor (g) needs to emerge.
</div>
</div>
</div>
<div class="slider-wrapper" style="margin-bottom: 10px;" id="brainSpheresRow">
<span style="font-size: 13px; color: #b0c4de; min-width: 180px;">Brain Spheres per NxEr</span>
<input type="range" class="slider" id="brainSpheres" min="2" max="5" value="3">
<span class="slider-value" id="brainSpheresValue">3</span>
</div>
<div class="slider-wrapper" style="margin-bottom: 10px;">
<span style="font-size: 13px; color: #b0c4de; min-width: 180px;">Neurons per Sphere</span>
<input type="range" class="slider" id="neuronsPerSphere" min="5" max="30" value="12">
<span class="slider-value" id="neuronsPerSphereValue">12</span>
</div>
<div class="slider-wrapper">
<span style="font-size: 13px; color: #b0c4de; min-width: 180px;">Oscillator Coupling</span>
<input type="range" class="slider" id="oscCoupling" min="1" max="20" value="10">
<span class="slider-value" id="oscCouplingValue">10</span>
</div>
</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/400868863" target="_blank">Neuraxon v2.0</a> Game of Life 5.0 Lite — CHC g-capable Brain + Best-g NxEr</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></strong></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>🌓 Day/Night cycle affects behavior & metabolism</div>
<div>🧠 Multi-Sphere Brain: Classic chain or CHC g-capable (6-sphere)</div>
<div>🧬 9 Receptor Subtypes · ChronoPlasticity · AGMP</div>
<div>🎼 Tonal voices · 🎤 Sing on food · 👂 Zoom in to hear</div>
<div>⌨️ H: Toggle HUD</div>
<div id="hideControlsHint" style="cursor: pointer; color: #4a9eff; text-decoration: underline; margin-top: 8px;">Hide Hints</div>
</div>
<!-- v4.5: Audio proximity badge -->
<div id="audioBadge" style="position:fixed;bottom:20px;right:20px;background:rgba(0,0,0,0.7);border:1px solid #3c3c3c;border-radius:20px;padding:8px 14px;font-size:12px;color:#aaa;z-index:500;font-family:'Consolas',monospace;display:none">
<span id="audioBadgeIcon">🔇</span>
<span id="audioBadgeText">Zoom in to hear NxErs sing</span>
</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';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.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);
}
// =====================================================================
// VOICE & AUDIO SYSTEM (v4.5) — Bio-inspired tonal hearing for NxErs
// =====================================================================
// Each NxEr has 6 harmonic tones forming its "voice".
// Voices can be learned during mating, inherited by children,
// attract clan members, and become audible only when the camera
// is zoomed in close to the population.
// ---------------------------------------------------------------------
// Musical scales (semitone offsets from a root note)
const MUSICAL_SCALES = {
major: [0, 2, 4, 5, 7, 9, 11, 12, 14, 16, 17, 19],
minor: [0, 2, 3, 5, 7, 8, 10, 12, 14, 15, 17, 19],
pentatonic: [0, 2, 4, 7, 9, 12, 14, 16, 19, 21, 24, 26],
harmonic: [0, 12, 19, 24, 28, 31, 34, 36, 38, 40, 42, 43] // overtone series
};
const SCALE_NAMES = Object.keys(MUSICAL_SCALES);
// Consonant intervals (mod 12) — octave, fifth, fourth, major/minor third, major sixth
const CONSONANT_SEMITONES = new Set([0, 3, 4, 5, 7, 8, 9, 12]);
function semitoneToFreq(rootHz, semitones) {
return rootHz * Math.pow(2, semitones / 12);
}
// Generate 6 harmonic tones based on a chosen root and scale
function generateHarmonicVoice(rootHz = null, scaleName = null) {
const root = rootHz || (160 + Math.random() * 280); // E3..F4 range
const sName = scaleName || SCALE_NAMES[Math.floor(Math.random() * SCALE_NAMES.length)];
const scale = MUSICAL_SCALES[sName];
const tones = [];
const usedIdx = new Set();
// Always include the root
tones.push(root);
usedIdx.add(0);
while (tones.length < 6) {
const idx = Math.floor(Math.random() * scale.length);
if (usedIdx.has(idx)) continue;
usedIdx.add(idx);
tones.push(semitoneToFreq(root, scale[idx]));
}
return tones.sort((a, b) => a - b);
}
// Score how musically pleasing the voice is (0 .. 1)
function musicalityScore(tones) {
if (!tones || tones.length < 2) return 0;
const sorted = [...tones].sort((a, b) => a - b);
let score = 0;
let count = 0;
// Pairwise interval analysis
for (let i = 0; i < sorted.length; i++) {
for (let j = i + 1; j < sorted.length; j++) {
const ratio = sorted[j] / sorted[i];
const semitones = 12 * Math.log2(ratio);
const semiRound = Math.round(semitones);
const closeness = Math.max(0, 1 - Math.abs(semitones - semiRound));
const consonant = CONSONANT_SEMITONES.has(semiRound % 12) ? 1.0 : 0.25;
score += closeness * consonant;
count++;
}
}
// Range penalty — too wide a range is less singable
const range = sorted[sorted.length - 1] / sorted[0];
const rangePenalty = range > 8 ? 0.6 : (range > 4 ? 0.85 : 1.0);
return Math.min(1, (score / Math.max(1, count)) * rangePenalty);
}
// Voice similarity: 0..1, used for "same tones tend to be closer"
function voiceSimilarity(voiceA, voiceB) {
if (!voiceA || !voiceB) return 0;
let matched = 0;
for (const a of voiceA) {
for (const b of voiceB) {
// Two tones count as "same" if within ~50 cents (half a semitone)
if (Math.abs(12 * Math.log2(a / b)) < 0.5) { matched++; break; }
}
}
return matched / voiceA.length;
}
// Pick N random elements (without replacement) from an array
function sampleFrom(arr, n) {
const a = [...arr];
const out = [];
while (out.length < n && a.length) {
out.push(a.splice(Math.floor(Math.random() * a.length), 1)[0]);
}
return out;
}
// =================================================================
// GENERAL FACTOR (g) APPARATUS — v5.0 Lite
// Pure-JS port of the Python neuraxon/gfactor.py methodology.
// No numpy: correlation matrix + PC1 via power iteration, second
// eigenvalue via Hotelling deflation.
//
// Seven CHC-style ability proxies are derived per living NxEr from
// the quantities the Lite sim already tracks (no new dynamics):
// Gs processing speed food acquired per second lived
// Gv visuospatial world cells explored per second
// Gf fluid reasoning closeness of branching ratio to the
// critical point 1.0 (adaptive dynamics)
// Glr learning/retrieval distinct food sources remembered
// Gc crystallised tones learned + musicality (knowledge)
// Gsm sensorimotor net energy efficiency (motor economy)
// Ga auditory songs sung × musicality (vocal skill)
//
// Population read-outs mirror the paper: PC1 variance fraction,
// positive-manifold %, mean off-diagonal r, and λ1/λ2. Each NxEr's
// own g (its PC1 factor score) is written to nxer.stats.g so it
// flows through the existing rankings / champions unchanged.
// Never throws; needs ≥ MIN_AGENTS_FOR_G living NxErs.
// =================================================================
const MIN_AGENTS_FOR_G = 8;
function _gProxies(n) {
const s = n.stats || {};
const tl = Math.max(1e-6, s.timeLived || 0);
const branch = s.temporalSyncScore || 1.0;
const knownFood = (n.knownFoodIds ? n.knownFoodIds.size : 0);
const musical = n.musicality || 0;
return [
(s.foodFound || 0) / tl, // Gs
(s.explored || 0) / tl, // Gv
1.0 - Math.min(1.0, Math.abs(branch - 1.0)), // Gf
knownFood, // Glr
(n.tonesLearned || 0) + musical, // Gc
s.energyEfficiency || 0, // Gsm
(n.songsSung || 0) * (0.5 + musical) // Ga
];
}
function _corrMatrix(cols) {
// cols: array of P proxy-vectors, each length N (population).
const P = cols.length, N = cols[0].length;
const mean = new Array(P), sd = new Array(P);
for (let p = 0; p < P; p++) {
let m = 0; for (let i = 0; i < N; i++) m += cols[p][i];
m /= N; mean[p] = m;
let v = 0; for (let i = 0; i < N; i++) { const d = cols[p][i] - m; v += d * d; }
sd[p] = Math.sqrt(v / N); // population std (matches numpy ddof=0)
}
const R = [];
for (let a = 0; a < P; a++) {
R.push(new Array(P).fill(0));
}
for (let a = 0; a < P; a++) {
for (let b = a; b < P; b++) {
if (sd[a] < 1e-12 || sd[b] < 1e-12) {
const v = (a === b) ? 1.0 : 0.0;
R[a][b] = R[b][a] = v;
continue;
}
let c = 0;
for (let i = 0; i < N; i++)
c += (cols[a][i] - mean[a]) * (cols[b][i] - mean[b]);
c = c / (N * sd[a] * sd[b]);
R[a][b] = R[b][a] = c;
}
}
return R;
}
function _powerIter(M, iters) {
const P = M.length;
let v = new Array(P).fill(1 / Math.sqrt(P));
let lambda = 0;
for (let it = 0; it < (iters || 100); it++) {
const w = new Array(P).fill(0);
for (let a = 0; a < P; a++) {
let s = 0;
for (let b = 0; b < P; b++) s += M[a][b] * v[b];
w[a] = s;
}
let norm = 0; for (let a = 0; a < P; a++) norm += w[a] * w[a];
norm = Math.sqrt(norm);
if (norm < 1e-12) break;
for (let a = 0; a < P; a++) w[a] /= norm;
lambda = norm;
let diff = 0; for (let a = 0; a < P; a++) diff += Math.abs(w[a] - v[a]);
v = w;
if (diff < 1e-10) break;
}
return { vec: v, val: lambda };
}
// Returns {pc1, posManifold, meanR, lambdaRatio, n} and writes each
// living NxEr's PC1 factor score to nxer.stats.g (0 if too few).
function computePopulationG(aliveNxers) {
try {
const pop = aliveNxers.filter(n => n && n.alive);
const N = pop.length;
if (N < MIN_AGENTS_FOR_G) {
pop.forEach(n => { n.stats.g = 0; });
return { pc1: 0, posManifold: 0, meanR: 0, lambdaRatio: 1, n: N };
}
const P = 7;
const raw = pop.map(_gProxies); // N × P
const cols = [];
for (let p = 0; p < P; p++) cols.push(raw.map(r => r[p]));
const R = _corrMatrix(cols);
// Off-diagonal stats (positive manifold + mean r).
let off = 0, pos = 0, sum = 0;
for (let a = 0; a < P; a++)
for (let b = 0; b < P; b++)
if (a !== b) { off++; sum += R[a][b]; if (R[a][b] > 0) pos++; }
const meanR = off ? sum / off : 0;
const posManifold = off ? pos / off : 0;
// λ1 via power iteration; λ2 via deflation.
const e1 = _powerIter(R, 200);
const lambda1 = e1.val;
const v1 = e1.vec;
const D = [];
for (let a = 0; a < P; a++) {
D.push(new Array(P));
for (let b = 0; b < P; b++)
D[a][b] = R[a][b] - lambda1 * v1[a] * v1[b];
}
const e2 = _powerIter(D, 200);
const lambda2 = Math.max(1e-9, e2.val);
const traceP = P; // trace of a correlation matrix = P
const pc1 = lambda1 / traceP;
const lambdaRatio = lambda1 / lambda2;
// Per-NxEr g = z-scored proxy vector projected on PC1.
const mean = new Array(P), sd = new Array(P);
for (let p = 0; p < P; p++) {
let m = 0; for (let i = 0; i < N; i++) m += cols[p][i];
m /= N; mean[p] = m;
let v = 0; for (let i = 0; i < N; i++) { const d = cols[p][i] - m; v += d * d; }
sd[p] = Math.sqrt(v / N) || 1;
}
for (let i = 0; i < N; i++) {
let g = 0;
for (let p = 0; p < P; p++)
g += ((raw[i][p] - mean[p]) / sd[p]) * v1[p];
pop[i].stats.g = g;
}
return { pc1, posManifold, meanR, lambdaRatio, n: N };
} catch (e) {
// Never let g measurement break the sim.
try { (aliveNxers || []).forEach(n => { if (n && n.stats && n.stats.g === undefined) n.stats.g = 0; }); } catch (_) {}
return { pc1: 0, posManifold: 0, meanR: 0, lambdaRatio: 1, n: 0 };
}
}
// ---------- AudioEngine (Web Audio) ----------
// Plays the NxEr voices as sine-tone melodies. Volume gated by camera zoom.
class AudioEngine {
constructor() {
this.ctx = null;
this.masterGain = null;
this.enabled = false;
this.targetVolume = 0;
this.scheduledUntil = 0;
this.maxConcurrent = 12; // Avoid clipping
this.activeVoices = 0;
}
init() {
if (this.ctx) return;
try {
const Ctx = window.AudioContext || window.webkitAudioContext;
if (!Ctx) return;
this.ctx = new Ctx();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0;
// Soft compressor to keep things gentle
this.compressor = this.ctx.createDynamicsCompressor();
this.compressor.threshold.value = -20;
this.compressor.knee.value = 12;
this.compressor.ratio.value = 4;
this.compressor.attack.value = 0.01;
this.compressor.release.value = 0.25;
this.masterGain.connect(this.compressor).connect(this.ctx.destination);
this.enabled = true;
} catch (e) {
console.warn('AudioEngine init failed', e);
}
}
resume() {
if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume();
}
// Camera proximity gate — silent when zoomed out
setProximity(p) {
this.targetVolume = Math.max(0, Math.min(1, p));
if (!this.masterGain) return;
const t = this.ctx.currentTime;
this.masterGain.gain.cancelScheduledValues(t);
this.masterGain.gain.linearRampToValueAtTime(this.targetVolume * 0.35, t + 0.2);
}
playNote(freq, duration = 0.4, volume = 0.15, when = 0, type = 'sine') {
if (!this.enabled || !this.ctx) return;
if (this.activeVoices >= this.maxConcurrent) return;
this.activeVoices++;
const start = this.ctx.currentTime + when;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.value = freq;
// Soft attack/decay envelope so notes don't click
gain.gain.setValueAtTime(0.0001, start);
gain.gain.exponentialRampToValueAtTime(volume, start + 0.04);
gain.gain.exponentialRampToValueAtTime(volume * 0.6, start + duration * 0.6);
gain.gain.exponentialRampToValueAtTime(0.0001, start + duration);
osc.connect(gain).connect(this.masterGain);
osc.start(start);
osc.stop(start + duration + 0.05);
osc.onended = () => { this.activeVoices = Math.max(0, this.activeVoices - 1); };
}
// Play a melodic phrase from a voice (sequence of tones)
playMelody(voice, foodBonus = 0, baseVolume = 0.1) {
if (!this.enabled || !voice || !voice.length) return;
// foodBonus shifts pitch upwards (in semitones, capped)
const semitoneShift = Math.min(6, foodBonus);
const shift = Math.pow(2, semitoneShift / 12);
const noteDur = 0.18;
voice.forEach((f, i) => {
this.playNote(f * shift, noteDur, baseVolume, i * noteDur * 0.85);
});
}
// Play a sustained chord (used when grouped clans sing together)
playChord(voice, duration = 0.8, baseVolume = 0.06, foodBonus = 0) {
if (!this.enabled || !voice || !voice.length) return;
const shift = Math.pow(2, Math.min(6, foodBonus) / 12);
voice.forEach(f => this.playNote(f * shift, duration, baseVolume, Math.random() * 0.05));
}
}
// Global audio engine instance
const audioEngine = new AudioEngine();
// =====================================================================
// NEURAL NETWORK CLASSES — Neuraxon v2.0 Multi-Sphere Architecture
// Features: ChronoPlasticity, DSN Dynamic Decay, CTSN Complement,
// AGMP Astrocyte-Gated Plasticity, 9 Receptor Subtypes,
// Multi-Band Oscillator Bank with PAC, Inter-Sphere Links
// =====================================================================
// --- Sphere Type Definitions ---
const SPHERE_TYPES = {
sensory: { hex: '#ddaa44', label: 'Sensory', emoji: '👁', layer: 'L0' },
association: { hex: '#7799cc', label: 'Association', emoji: '🔄', layer: 'L1' },
motor: { hex: '#cc66aa', label: 'Motor', emoji: '💪', layer: 'L2' },
executive: { hex: '#aa77dd', label: 'Executive', emoji: '🧠', layer: 'L3' },
prefrontal: { hex: '#55cc88', label: 'Prefrontal', emoji: '🧩', layer: 'L2' },
// v5.0 Lite — CHC g-capable functional spheres (mirrors the
// Python build_chc_multisphere: a broad sensory front-end, two
// association spheres carrying fluid vs crystallised ability,
// and a motor read-out. The lateral assoc_fluid↔assoc_cryst
// link is the cross-battery (κ) coupling that lets a general
// factor emerge across the proxy abilities.
visual: { hex: '#e0b040', label: 'Visual', emoji: '👁', layer: 'L0' },
auditory: { hex: '#d0a060', label: 'Auditory', emoji: '👂', layer: 'L0' },
intero: { hex: '#c08050', label: 'Interoceptive',emoji: '🫀', layer: 'L0' },
assoc_fluid: { hex: '#6fa0d8', label: 'Assoc·Fluid', emoji: '🌊', layer: 'L1' },
assoc_cryst: { hex: '#8f7fd0', label: 'Assoc·Cryst', emoji: '📚', layer: 'L1' }
};
// --- Receptor Subtype ---
class ReceptorSubtype {
constructor(name, parentMod, threshold, gain, isTonic) {
this.name = name;
this.parentMod = parentMod;
this.threshold = threshold;
this.gain = gain;
this.isTonic = isTonic;
this.activation = 0;
}
computeActivation(tonic, phasic) {
const conc = this.isTonic ? tonic : (tonic + phasic);
const k = this.isTonic ? 20 : 10;
this.activation = this.gain / (1 + Math.exp(-k * (conc - this.threshold)));
return this.activation;
}
}
// --- Multi-Band Oscillator Bank with PAC ---
class OscillatorBank {
constructor() {
this.bands = {
infraslow: { freq: 0.05, phase: Math.random() * 6.28, amp: 0.3 },
slow: { freq: 0.5, phase: Math.random() * 6.28, amp: 0.4 },
theta: { freq: 6, phase: Math.random() * 6.28, amp: 0.5 },
alpha: { freq: 10, phase: Math.random() * 6.28, amp: 0.4 },
gamma: { freq: 40, phase: Math.random() * 6.28, amp: 0.6 }
};
this.coupling = 0.1;
}
update(dt) {
for (const b of Object.values(this.bands)) {
b.phase = (b.phase + 2 * Math.PI * b.freq * dt / 1000) % (2 * Math.PI);
}
}
getDrive(neuronId, N) {
const phi = 2 * Math.PI * neuronId / Math.max(1, N);
const thetaGate = Math.max(0, Math.cos(this.bands.theta.phase + phi));
const gammaSig = this.bands.gamma.amp * thetaGate * Math.sin(this.bands.gamma.phase + 2 * phi);
const slowSig = this.bands.slow.amp * Math.sin(this.bands.slow.phase + 0.3 * phi);
const infraSig = this.bands.infraslow.amp * Math.sin(this.bands.infraslow.phase);
return this.coupling * (gammaSig + 0.5 * slowSig + 0.3 * infraSig);
}
getAmplitudes() {
const r = {};
for (const [name, b] of Object.entries(this.bands)) {
r[name] = Math.abs(Math.sin(b.phase)) * b.amp;
}
return r;
}
}
// --- Neuromodulator System with 9 Receptor Subtypes ---
class NeuromodulatorSystem {
constructor(baselines = {}) {
this.tonic = {
DA: baselines.DA || random(0.08, 0.20),
'5HT': baselines['5HT'] || random(0.08, 0.20),
ACh: baselines.ACh || random(0.08, 0.20),
NA: baselines.NA || random(0.08, 0.20)
};
this.phasic = { DA: 0, '5HT': 0, ACh: 0, NA: 0 };
this.receptors = {
D1: new ReceptorSubtype('D1', 'DA', 0.35, 1.0, false),
D2: new ReceptorSubtype('D2', 'DA', 0.25, 1.0, true),
'5HT1A': new ReceptorSubtype('5HT1A', '5HT', 0.05, 1.0, true),
'5HT2A': new ReceptorSubtype('5HT2A', '5HT', 0.30, 1.0, false),
'5HT4': new ReceptorSubtype('5HT4', '5HT', 0.20, 1.0, false),
M1: new ReceptorSubtype('M1', 'ACh', 0.30, 1.0, false),
M2: new ReceptorSubtype('M2', 'ACh', 0.10, 1.0, true),
beta1: new ReceptorSubtype('beta1', 'NA', 0.20, 1.0, false),
alpha2:new ReceptorSubtype('alpha2','NA', 0.08, 1.0, true)
};
this.activations = {};
}
update(activity, dt) {
const rel = 0.02;
// Tonic: decay toward baseline
for (const k of Object.keys(this.tonic)) {
this.tonic[k] = Math.max(0, Math.min(2, this.tonic[k]));
}
// Phasic: exponential decay + activity-driven release
for (const k of Object.keys(this.phasic)) {
this.phasic[k] *= Math.exp(-0.1 * dt);
}
this.phasic.DA += rel * (activity.stateChangeRate || 0) * dt;
this.phasic['5HT'] += rel * (activity.meanActivity || 0) * dt;
this.phasic.ACh += rel * (activity.excFraction || 0) * dt;
this.phasic.NA += rel * (activity.stateChangeRate || 0) * dt;
// Inter-modulator crosstalk
this.phasic.ACh *= (1 - 0.1 * this.phasic.DA);
this.tonic['5HT'] += 0.02 * (this.tonic.NA + this.phasic.NA) * dt;
// Compute receptor activations
for (const [name, r] of Object.entries(this.receptors)) {
r.computeActivation(this.tonic[r.parentMod], this.phasic[r.parentMod]);
this.activations[name] = r.activation;
}
}
}
class NetworkParameters {
constructor(neuronsPerSphere = 12, numSpheres = 3, brainArch = 'classic') {
this.networkName = "Neuraxon v2.0 Multi-Sphere";
this.numSpheres = numSpheres;
this.neuronsPerSphere = neuronsPerSphere;
this.brainArch = brainArch; // v5.0 Lite: 'classic' | 'chc6'
this.numInputNeurons = 10; // v4.0: +SphereSync input
this.numOutputNeurons = 6;
this.dt = 1.0;
this.simulationSteps = randomInt(20, 50);
this.connectionProbability = random(0.10, 0.25);
this.smallWorldK = 6;
this.smallWorldRewireProb = 0.35;
this.membraneTimeConstant = random(10.0, 40.0);
this.firingThresholdExcitatory = random(0.4, 1.2);
this.firingThresholdInhibitory = random(-1.2, -0.4);
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.oscillatorCoupling = random(0.05, 0.20);
this.maxAxonalDelay = 10.0;
// v4.0: ChronoPlasticity
this.chronoAlphaFast = 0.95;
this.chronoAlphaSlow = 0.99;
// v4.0: CTSN
this.ctsn_rho = 0.9;
// v4.0: AGMP
this.agmp_lambdaE = 0.95;
this.agmp_lambdaA = 0.999;
this.agmp_eta = 0.005;
// v4.0: Inter-sphere projection plasticity
this.projectionLR = 0.005;
}
clone() {
const p = new NetworkParameters(this.neuronsPerSphere, this.numSpheres, this.brainArch);
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;
}
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);
return this.branchPotential + this.plateauPotential;
}
}
// --- Synapse with ChronoPlasticity ---
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.neighborSynapses = [];
this.potentialDeltaW = 0.0;
// v4.0: ChronoPlasticity traces
this.chronoFast = 0;
this.chronoSlow = 0;
this.chronoOmega = 0.5;
// v4.0: AGMP eligibility trace
this.eligibility = 0;
}
computeInput(preState) {
if (this.isSilent) return 0.0;
const delayFactor = Math.max(0.0, 1.0 - this.axonalDelay / 10.0);
// v4.0: Include chrono traces in input
return (this.wFast + this.wSlow) * preState * delayFactor +
0.15 * this.wFast * this.chronoFast + 0.08 * this.wSlow * this.chronoSlow;
}
updateChronoPlasticity(preState, dt) {
const spike = Math.abs(preState) === 1 ? 1 : 0;
this.chronoFast = this.params.chronoAlphaFast * this.chronoFast + spike;
const omegaPower = Math.pow(this.params.chronoAlphaSlow, this.chronoOmega);
this.chronoSlow = omegaPower * this.chronoSlow + spike;
// Adaptive omega
const sig = 1 / (1 + Math.exp(-(2 * spike + 1.5 * this.chronoSlow - 1)));
this.chronoOmega = clamp(sig, 0.01, 0.99);
}
calculateDeltaW(preState, postState, receptorAct, 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;
// v4.0: Differential DA gating — D1 gates LTP, D2 gates LTD
const d1 = receptorAct.D1 || 0.5;
const d2 = receptorAct.D2 || 0.5;
let deltaW = 0;
if (preState === 1 && postState === 1) {
deltaW = this.params.learningRate * d1 * this.preTrace;
} else if (preState === 1 && postState === -1) {
deltaW = -this.params.learningRate * d2 * this.preTraceLTD;
}
// v4.0: AGMP eligibility update
const psi = (preState === 1 && postState === 1) ? 1 : ((preState === 1 && postState === -1) ? -0.5 : 0);
this.eligibility = this.params.agmp_lambdaE * this.eligibility + psi;
return deltaW;
}
applyUpdate(dt, receptorAct, neighborDeltas, astrocyteState, modulatorySignal) {
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;
}
// v4.0: AGMP — 4-factor rule: eligibility × modulation × astrocyte × learning
const agmpDelta = this.params.agmp_eta * (modulatorySignal || 0.5) * astrocyteState * this.eligibility;
deltaW += agmpDelta;
// Update weights with serotonin modulation
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 ht2a = receptorAct['5HT2A'] || 0.5;
const ht1a = receptorAct['5HT1A'] || 0.5;
const metaFactor = 0.5 * ht2a + 0.1 * (1 - ht1a);
this.wMeta += dt / this.params.tauMeta * (-this.wMeta + 0.05 * deltaW * metaFactor);
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; }
}
// --- Neuraxon v2.0 Unit with CTSN + AGMP ---
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;
// v4.0: CTSN complement pathway
this.complement = 0;
// v4.0: AGMP astrocyte state
this.astrocyteState = 0;
// v4.0: DSN dynamic decay (input-conditioned)
this.dynamicAlpha = 0.5;
// v4.0: Running-average firing rate for homeostasis
this.runningRate = 0.1;
}
setState(val) { this.trinaryState = val; this.membranePotential = val * 0.5; }
nonlinearDendriticIntegration(synapticInputs, modulatoryInputs, dt) {
let totalSynaptic = 0.0;
for (let i = 0; i < this.dendriticBranches.length; i++) {
const branchSynInputs = [];
for (let j = i; j < synapticInputs.length; j += this.dendriticBranches.length) {
branchSynInputs.push(synapticInputs[j]);
}
totalSynaptic += this.dendriticBranches[i].integrateInputs(branchSynInputs, dt);
}
const modEffect = (modulatoryInputs || []).reduce((s, v) => s + v, 0);
return totalSynaptic + modEffect * 0.3;
}
update(synInputs, modInputs, extInput, receptorAct, dt, oscDrive) {
if (!this.isActive) return;
const prevState = this.trinaryState;
if (this.type === 'input') {
if (extInput !== undefined && extInput !== 0) {
this.trinaryState = extInput > 0.25 ? 1 : (extInput < -0.25 ? -1 : 0);
this.membranePotential = extInput;
}
this.stateHistory.push(this.trinaryState);
if (this.stateHistory.length > 20) this.stateHistory.shift();
return;
}
const D = this.nonlinearDendriticIntegration(synInputs, modInputs, dt);
// v4.0: DSN dynamic decay (input-conditioned α)
const inputMag = Math.abs(D) + Math.abs(extInput || 0);
this.dynamicAlpha = 1 / (1 + Math.exp(-(inputMag * 2 - 1)));
this.dynamicAlpha = clamp(this.dynamicAlpha, 0.1, 0.95);
// NA modulation of gain
const gNA = 1 + 0.5 * (receptorAct.beta1 || 0) + 0.2 * (receptorAct.alpha2 || 0);
const spont = (this.params.spontaneousFireRate + 0.3 * (receptorAct.alpha2 || 0)) *
(Math.random() < 0.02 ? (Math.random() < 0.5 ? 1 : -1) : 0);
// DSN update: s_t = α*s_{t-1} + (1-α)*(input)
this.membranePotential = this.dynamicAlpha * this.membranePotential +
(1 - this.dynamicAlpha) * (gNA * D + (extInput || 0) + oscDrive - this.adaptation + spont);
// v4.0: CTSN complement pathway
this.complement = this.params.ctsn_rho * this.complement +
(1 - this.params.ctsn_rho) * Math.tanh(inputMag * 0.5);
const augmented = this.membranePotential + this.complement;
// v4.0: Saturated modulation & homeostatic threshold
const rawMod = 0.3 * (receptorAct.M1 || 0) - 0.2 * (receptorAct.M2 || 0) +
(modInputs || []).reduce((s, v) => s + v, 0);
const deltaMeta = 0.3 * Math.tanh(rawMod);
const deltaHomeo = this.params.homeostaticPlasticityRate * (this.runningRate - this.params.targetFiringRate);
const thetaEff1 = this.params.firingThresholdExcitatory - deltaMeta + deltaHomeo - 0.1 * this.autoreceptor;
const thetaEff2 = this.params.firingThresholdInhibitory - deltaMeta + deltaHomeo + 0.1 * this.autoreceptor;
// Trinary readout from augmented state
if (augmented > thetaEff1) this.trinaryState = 1;
else if (augmented < thetaEff2) this.trinaryState = -1;
else this.trinaryState = 0;
// Adaptation & autoreceptor
this.adaptation += dt / (this.params.membraneTimeConstant * 2) * (-this.adaptation + 0.1 * Math.abs(this.trinaryState));
this.autoreceptor += dt / (this.params.membraneTimeConstant * 4) * (-this.autoreceptor + 0.2 * this.trinaryState);
// v4.0: Update AGMP astrocyte state
this.astrocyteState = this.params.agmp_lambdaA * this.astrocyteState +
(1 - this.params.agmp_lambdaA) * Math.abs(augmented);
// Running-average firing rate
this.runningRate += 0.01 * (Math.abs(this.trinaryState) - this.runningRate) * dt;
// Energy
if (this.trinaryState !== 0) {
this.energyLevel -= this.params.firingEnergyCost * dt / this.params.membraneTimeConstant;
}
this.energyLevel = Math.max(0, Math.min(this.params.energyBaseline, this.energyLevel + this.params.recoveryRate * dt / this.params.membraneTimeConstant));
// Health decay for inactive neurons
if (this.type === 'hidden') {
const recent = this.stateHistory.slice(-20);
const activity = recent.length > 0 ? recent.reduce((s, v) => s + Math.abs(v), 0) / recent.length : 0;
if (activity < 0.01) this.health -= this.params.neuronHealthDecay * dt;
else this.health = Math.min(1.0, this.health + 0.0001 * dt);
if (this.health <= this.params.neuronDeathThreshold) this.isActive = false;
}
this.stateHistory.push(this.trinaryState);
if (this.stateHistory.length > 20) this.stateHistory.shift();
}
}
// --- Single Sphere (a self-contained Neuraxon v2.0 network) ---
class Sphere {
constructor(id, type, params, nI = 4, nH = 12, nO = 4) {
this.id = id;
this.type = type;
this.typeInfo = SPHERE_TYPES[type] || SPHERE_TYPES.association;
this.params = params;
this.nI = nI; this.nH = nH; this.nO = nO;
this.oscillators = new OscillatorBank();
this.oscillators.coupling = params.oscillatorCoupling || 0.1;
this.neuromodulators = { dopamine: params.dopamineBaseline, serotonin: params.serotoninBaseline, acetylcholine: params.acetylcholineBaseline, norepinephrine: params.norepinephrineBaseline };
this.inputNeurons = []; this.hiddenNeurons = []; this.outputNeurons = [];
let nid = 0;
for (let i = 0; i < nI; i++) this.inputNeurons.push(new Neuraxon(nid++, 'input', params));
for (let i = 0; i < nH; i++) this.hiddenNeurons.push(new Neuraxon(nid++, 'hidden', params));
for (let i = 0; i < nO; i++) this.outputNeurons.push(new Neuraxon(nid++, 'output', params));
this.allNeurons = [...this.inputNeurons, ...this.hiddenNeurons, ...this.outputNeurons];
this.synapses = [];
this.inPorts = this.inputNeurons.map(n => n.id);
this.outPorts = this.outputNeurons.map(n => n.id);
this.energy = 0;
this.time = 0;
this.stepCount = 0;
this.branchingRatio = 1.0;
this.activationHistory = [];
this.buildSmallWorld();
}
buildSmallWorld() {
const N = this.allNeurons.length;
const shuffled = [...Array(N).keys()].sort(() => Math.random() - 0.5);
const ring = shuffled.map(i => this.allNeurons[i]);
const k = Math.min(Math.max(4, Math.round(N * 0.3)), N - 1);
const hk = Math.max(Math.floor(k / 2), 1);
const edges = new Set();
for (let i = 0; i < N; i++) {
for (let j = 1; j <= hk; j++) {
for (const t of [(i + j) % N, (i - j + N) % N]) {
if (ring[i].type === 'output' && ring[t].type === 'input') continue;
if (i !== t) edges.add(`${ring[i].id},${ring[t].id}`);
}
}
}
const ea = [...edges];
for (const e of ea) {
if (Math.random() < this.params.smallWorldRewireProb) {
edges.delete(e);
const s2 = ring[Math.floor(Math.random() * N)];
const t2 = ring[Math.floor(Math.random() * N)];
if (s2.id !== t2.id && !edges.has(`${s2.id},${t2.id}`)) {
edges.add(`${s2.id},${t2.id}`);
} else edges.add(e);
}
}
for (const e of edges) {
const [a, b] = e.split(',').map(Number);
this.synapses.push(new Synapse(a, b, this.params));
}
}
simulateStep(externalInputs = {}, receptorAct = {}) {
const dt = this.params.dt;
this.oscillators.update(dt);
const synInputs = {}, 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;
s.updateChronoPlasticity(pre.trinaryState, dt);
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;
const osc = this.oscillators.getDrive(n.id, this.allNeurons.length);
n.update(synInputs[n.id], modInputs[n.id], ext + osc, receptorAct, dt, osc);
this.activationHistory.push(Math.abs(n.trinaryState));
}
if (this.activationHistory.length > 500) this.activationHistory.splice(0, 250);
// STDP + AGMP
for (const s of this.synapses) {
if (s.integrity <= 0) continue;
const pre = this.allNeurons[s.preId], post = this.allNeurons[s.postId];
if (pre && pre.isActive && post && post.isActive) {
s.potentialDeltaW = s.calculateDeltaW(pre.trinaryState, post.trinaryState, receptorAct, dt);
}
}
for (const s of this.synapses) {
if (s.integrity <= 0) continue;
const pre = this.allNeurons[s.preId], post = this.allNeurons[s.postId];
if (pre && pre.isActive && post && post.isActive) {
const neighborDW = s.neighborSynapses ? s.neighborSynapses.map(ns => ns.potentialDeltaW) : [];
const astState = post.astrocyteState || 0.5;
const modSig = 0.5 + (receptorAct.D1 || 0.5);
s.applyUpdate(dt, receptorAct, neighborDW, astState, modSig);
}
}
// Structural plasticity
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), 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));
}
}
}
}
// Branching ratio
if (this.activationHistory.length > 2) {
const acts = this.activationHistory.slice(-100);
const d = acts.slice(0, -1).reduce((s, v) => s + v, 0);
if (d > 0) this.branchingRatio = clamp(acts.slice(1).reduce((s, v) => s + v, 0) / d, 0.1, 10.0);
}
this.energy += 0.01 * this.allNeurons.filter(n => n.isActive && n.trinaryState !== 0).length * dt;
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); }
getActivityStats() {
const active = this.allNeurons.filter(n => n.isActive);
const exc = active.filter(n => n.trinaryState === 1).length;
const inh = active.filter(n => n.trinaryState === -1).length;
return { total: active.length, exc, inh, neutral: active.length - exc - inh, energy: this.energy };
}
}
// --- Inter-Sphere Projection ---
class InterSphereProjection {
constructor(srcSphereId, tgtSphereId, type, srcOutPorts, tgtInPorts) {
this.srcId = srcSphereId;
this.tgtId = tgtSphereId;
this.type = type; // 'FF', 'FB', 'LAT'
this.weights = [];
for (let r = 0; r < srcOutPorts.length; r++) {
const row = [];
for (let c = 0; c < tgtInPorts.length; c++) row.push((Math.random() - 0.3) * 0.5);
this.weights.push(row);
}
this.plasticityRate = 0.005;
}
computeGatedSignals(srcSphere, tgtSphere) {
let gate = 1;
const srcOsc = srcSphere.oscillators.bands;
const tgtOsc = tgtSphere.oscillators.bands;
if (this.type === 'FF') {
gate = 1 + 0.3 * Math.abs(Math.sin(srcOsc.gamma.phase)) * Math.max(0, Math.cos(srcOsc.gamma.phase));
} else if (this.type === 'FB') {
gate = 1 + 0.3 * Math.max(0, -Math.cos(tgtOsc.alpha.phase));
} else if (this.type === 'LAT') {
gate = 1 + 0.2 * Math.max(0, Math.cos(srcOsc.theta.phase - tgtOsc.theta.phase));
}
const signals = {};
const srcOuts = srcSphere.getOutputStates();
for (let r = 0; r < Math.min(srcOuts.length, this.weights.length); r++) {
for (let c = 0; c < this.weights[r].length; c++) {
const tgtPort = tgtSphere.inPorts[c];
if (tgtPort !== undefined) {
signals[tgtPort] = (signals[tgtPort] || 0) + gate * this.weights[r][c] * srcOuts[r];
}
}
}
return signals;
}
updatePlasticity(srcSphere, tgtSphere, globalDA, dt) {
const dg = 0.5 + globalDA;
const srcOuts = srcSphere.getOutputStates();
for (let r = 0; r < Math.min(srcOuts.length, this.weights.length); r++) {
for (let c = 0; c < this.weights[r].length; c++) {
const tgtNeuron = tgtSphere.allNeurons[tgtSphere.inPorts[c]];
if (!tgtNeuron) continue;
this.weights[r][c] = clamp(
this.weights[r][c] + this.plasticityRate * srcOuts[r] * tgtNeuron.trinaryState * dg * dt,
-1.5, 1.5
);
}
}
}
}
// --- Multi-Sphere Network (the "brain" of each NxEr) ---
class NeuraxonNetwork {
constructor(params) {
this.params = params;
this.neuromodSystem = new NeuromodulatorSystem({
DA: params.dopamineBaseline,
'5HT': params.serotoninBaseline,
ACh: params.acetylcholineBaseline,
NA: params.norepinephrineBaseline
});
// Legacy accessor for neuromodulators
this.neuromodulators = {
dopamine: params.dopamineBaseline,
serotonin: params.serotoninBaseline,
acetylcholine: params.acetylcholineBaseline,
norepinephrine: params.norepinephrineBaseline
};
// Build spheres based on numSpheres parameter
this.spheres = [];
this.projections = [];
const nS = params.numSpheres || 3;
const nPS = params.neuronsPerSphere || 12;
const nI = Math.max(2, Math.round(nPS * 0.3));
const nO = Math.max(2, Math.round(nPS * 0.25));
const nH = Math.max(3, nPS - nI - nO);
if (params.brainArch === 'chc6') {
// ----- v5.0 Lite: CHC g-capable 6-sphere topology -----
// Mirrors the Python build_chc_multisphere: a broad
// sensory front-end (visual / auditory / intero), two
// association spheres (fluid vs crystallised), and a
// motor read-out. Cross-battery (κ) coupling — the
// LATERAL assoc_fluid↔assoc_cryst link — is what lets a
// single general factor emerge across the proxies.
// Contract preserved: spheres[0] receives external
// inputs (setInputStates) and spheres[last] is motor
// (getOutputStates), so the rest of the engine is
// unchanged.
this.brainArch = 'chc6';
const S = (id, t, sI, sO) =>
new Sphere(id, t, params, sI, nH, sO);
const visual = S('S0_visual', 'visual',
Math.max(nI, params.numInputNeurons), nO);
const auditory= S('S1_auditory', 'auditory', nI, nO);
const intero = S('S2_intero', 'intero', nI, nO);
const aFluid = S('S3_fluid', 'assoc_fluid', nI, nO);
const aCryst = S('S4_cryst', 'assoc_cryst', nI, nO);
const motor = S('S5_motor', 'motor',
nI, Math.max(nO, params.numOutputNeurons));
this.spheres = [visual, auditory, intero, aFluid, aCryst, motor];
const P = (a, b, kind) => this.projections.push(
new InterSphereProjection(a.id, b.id, kind,
a.outPorts, b.inPorts));
// Sensory front-end → fluid/crystallised association
P(visual, aFluid, 'FF');
P(auditory, aFluid, 'FF');
P(intero, aCryst, 'FF');
P(visual, aCryst, 'FF');
// Cross-battery coupling (κ) — the lesionable link
P(aFluid, aCryst, 'LAT');
P(aCryst, aFluid, 'LAT');
// Association → motor read-out
P(aFluid, motor, 'FF');
P(aCryst, motor, 'FF');
// Top-down feedback motor → association
P(motor, aFluid, 'FB');
P(motor, aCryst, 'FB');
} else {
this.brainArch = 'classic';
const sphereTypes = nS === 2 ? ['sensory', 'motor'] :
nS === 3 ? ['sensory', 'association', 'motor'] :
nS === 4 ? ['sensory', 'association', 'prefrontal', 'motor'] :
['sensory', 'association', 'prefrontal', 'executive', 'motor'];
for (let i = 0; i < nS; i++) {
const t = sphereTypes[i] || 'association';
const sI = i === 0 ? Math.max(nI, params.numInputNeurons) : nI;
const sO = i === nS - 1 ? Math.max(nO, params.numOutputNeurons) : nO;
this.spheres.push(new Sphere(`S${i}`, t, params, sI, nH, sO));
}
// Build inter-sphere projections (feedforward chain + feedback)
for (let i = 0; i < this.spheres.length - 1; i++) {
const src = this.spheres[i], tgt = this.spheres[i + 1];
this.projections.push(new InterSphereProjection(src.id, tgt.id, 'FF', src.outPorts, tgt.inPorts));
// Feedback from next to current
if (i < this.spheres.length - 2) {
this.projections.push(new InterSphereProjection(tgt.id, src.id, 'FB', tgt.outPorts, src.inPorts));
}
}
// Lateral connections for spheres > 3
if (this.spheres.length >= 4) {
const mid = Math.floor(this.spheres.length / 2);
this.projections.push(new InterSphereProjection(
this.spheres[0].id, this.spheres[mid].id, 'LAT',
this.spheres[0].outPorts, this.spheres[mid].inPorts
));
}
}
this.time = 0;
this.stepCount = 0;
this.totalEnergyConsumed = 0;
this.branchingRatio = 1.0;
}
setInputStates(states) {
// Feed inputs to the first sphere (sensory)
this.spheres[0].setInputStates(states);
}
simulateStep() {
const dt = this.params.dt;
// Compute inter-sphere signals
const interSignals = {};
this.spheres.forEach(s => interSignals[s.id] = {});
for (const proj of this.projections) {
const src = this.spheres.find(s => s.id === proj.srcId);
const tgt = this.spheres.find(s => s.id === proj.tgtId);
if (!src || !tgt) continue;
const signals = proj.computeGatedSignals(src, tgt);
for (const [nid, val] of Object.entries(signals)) {
interSignals[tgt.id][nid] = (interSignals[tgt.id][nid] || 0) + val;
}
}
// Compute network activity for neuromodulator update
let totalAct = 0, totalN = 0, prevStates = new Map();
this.spheres.forEach(s => s.allNeurons.forEach(n => {
totalAct += Math.abs(n.trinaryState); totalN++;
prevStates.set(n, n.trinaryState);
}));
let stateChanges = 0;
const activity = {
meanActivity: totalN > 0 ? totalAct / totalN : 0,
excFraction: 0,
stateChangeRate: 0
};
// Update global neuromodulator system
this.neuromodSystem.update(activity, dt);
const rAct = this.neuromodSystem.activations;
// Sync legacy neuromodulators
this.neuromodulators.dopamine = this.neuromodSystem.tonic.DA + this.neuromodSystem.phasic.DA;
this.neuromodulators.serotonin = this.neuromodSystem.tonic['5HT'] + this.neuromodSystem.phasic['5HT'];
this.neuromodulators.acetylcholine = this.neuromodSystem.tonic.ACh + this.neuromodSystem.phasic.ACh;
this.neuromodulators.norepinephrine = this.neuromodSystem.tonic.NA + this.neuromodSystem.phasic.NA;
// Simulate each sphere
for (const sphere of this.spheres) {
sphere.simulateStep(interSignals[sphere.id] || {}, rAct);
}
// Update inter-sphere projection plasticity
const globalDA = this.neuromodSystem.tonic.DA + this.neuromodSystem.phasic.DA;
for (const proj of this.projections) {
const src = this.spheres.find(s => s.id === proj.srcId);
const tgt = this.spheres.find(s => s.id === proj.tgtId);
if (src && tgt) proj.updatePlasticity(src, tgt, globalDA, dt);
}
// Update branching ratio from last sphere
this.branchingRatio = this.spheres[this.spheres.length - 1].branchingRatio;
this.time += dt;
this.stepCount++;
}
getOutputStates() {
// Output from the last sphere (motor)
return this.spheres[this.spheres.length - 1].getOutputStates();
}
getEnergyStatus() {
let totalEnergy = 0, count = 0;
this.spheres.forEach(s => s.allNeurons.forEach(n => {
if (n.isActive) { totalEnergy += n.energyLevel; count++; }
}));
return {
totalEnergy, averageEnergy: count > 0 ? totalEnergy / count : 0,
efficiency: this.stepCount > 0 ? this.totalEnergyConsumed / this.stepCount : 0,
branchingRatio: this.branchingRatio
};
}
getSpheresSummary() {
return this.spheres.map(s => ({
id: s.id, type: s.type, label: s.typeInfo.label, color: s.typeInfo.hex,
stats: s.getActivityStats(),
oscAmps: s.oscillators.getAmplitudes()
}));
}
getProjectionsSummary() {
return this.projections.map(p => ({ src: p.srcId, tgt: p.tgtId, type: p.type }));
}
getReceptorActivations() { return { ...this.neuromodSystem.activations }; }
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;
// v4.0: Now 10 inputs (added SphereSync)
this.lastInputs = [0, 0, 0, 0, 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, g: 0 };
if (this.stats.g === undefined) this.stats.g = 0; // v5.0 Lite — best-g NxEr
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;
// v3.0: NEW - Temperature system (bioinspired thermoregulation)
this.bodyTemperature = config.bodyTemperature || 37.0; // Normal body temp
this.optimalTempLow = 35.0;
this.optimalTempHigh = 39.0;
// v3.0: NEW - Proprioception (rock hit counter for obstacle avoidance)
this.consecutiveRockHits = 0;
// v3.0: Track resting state for reduced metabolism
this.isResting = false;
// v4.5: VOICE & HEARING — bio-inspired tonal communication
// Each NxEr has 6 harmonic tones forming its voice. Voices can
// be inherited (children), learned (during mating), are heard
// by clan members nearby, and become more pleasing when paired
// with high musicality.
this.voice = config.voice || generateHarmonicVoice();
this.musicality = musicalityScore(this.voice);
this.lastSungTick = -10000;
this.singCooldownTicks = 30; // base cooldown between songs
this.knownFoodIds = new Set(); // for "first time finding food" trigger
this.songsSung = 0;
this.tonesLearned = 0;
this.lastHeardSongTick = -10000; // when this NxEr last heard a clan-mate sing
this.lastHeardSongPos = null; // attraction target
this.lastHeardSongFromId = 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 {
// v3.0: Day/Night cycle constants
static DAY_PHASE = 0;
static DUSK_PHASE = 1;
static NIGHT_PHASE = 2;
static DAWN_PHASE = 3;
// v3.0: Temperature constants
static TEMP_TRANSFER_RATE = 0.1;
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: [], g: [] };
// NEW: Clan Counter
this.nextClanId = 1;
// v3.0: Day/Night cycle state
this.dayNightPhase = 0; // 0=day, 1=dusk, 2=night, 3=dawn
this.dayNightProgress = 0.0; // 0.0 to 1.0 within phase
this.globalTimeOfDay = 0.0; // 0.0 to 1.0 full cycle
// v3.1: Sun position for spatial day/night (longitude angle)
this.sunLongitude = 0.0; // 0 to 2*PI - where the sun is pointing
this.environmentTemperature = config.baseTemperature || 25;
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++;
// v4.5: VOICE LEARNING during mating — each partner has a chance
// to learn one tone from the other. More musical voices are
// easier to learn from.
const learnChanceA = 0.3 + 0.4 * b.musicality;
const learnChanceB = 0.3 + 0.4 * a.musicality;
if (Math.random() < learnChanceA) {
const learnedTone = randomChoice(b.voice);
const replaceIdx = Math.floor(Math.random() * a.voice.length);
// Don't replace if the tone is already present (within 50 cents)
const exists = a.voice.some(t => Math.abs(12 * Math.log2(t / learnedTone)) < 0.5);
if (!exists) {
a.voice[replaceIdx] = learnedTone;
a.voice.sort((x, y) => x - y);
a.musicality = musicalityScore(a.voice);
a.tonesLearned++;
}
}
if (Math.random() < learnChanceB) {
const learnedTone = randomChoice(a.voice);
const replaceIdx = Math.floor(Math.random() * b.voice.length);
const exists = b.voice.some(t => Math.abs(12 * Math.log2(t / learnedTone)) < 0.5);
if (!exists) {
b.voice[replaceIdx] = learnedTone;
b.voice.sort((x, y) => x - y);
b.musicality = musicalityScore(b.voice);
b.tonesLearned++;
}
}
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 nPS = this.config.neuronsPerSphere || 12;
const arch = this.config.brainArch || 'classic'; // v5.0 Lite
// CHC g-capable brain is a fixed 6-sphere layout; classic
// chain keeps the user-selected sphere count.
const nSph = arch === 'chc6' ? 6 : (this.config.brainSpheres || 3);
const netParams = params || new NetworkParameters(nPS, nSph, arch);
netParams.oscillatorCoupling = (this.config.oscCoupling || 10) / 100;
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);
const nxer = 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
});
// v3.0: Initialize temperature based on terrain (terrain already computed above)
return nxer;
}
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();
// v4.0: Mutate neuronsPerSphere slightly
baseParams.neuronsPerSphere = randomInt(
Math.max(5, baseParams.neuronsPerSphere - 2),
Math.min(30, baseParams.neuronsPerSphere + 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);
// v4.5: VOICE INHERITANCE — child gets a mix of parent tones plus
// 1-2 of its own freshly generated tones (its "proper voice").
const fromA = sampleFrom(A.voice, 2 + (Math.random() < 0.5 ? 1 : 0));
const fromB = sampleFrom(B.voice, 2 + (Math.random() < 0.5 ? 1 : 0));
// Use the average of parent fundamentals as a tonal centre to keep the
// child's own tones harmonically related to its inherited tones.
const parentRoot = (A.voice[0] + B.voice[0]) / 2;
const ownScale = MUSICAL_SCALES[randomChoice(SCALE_NAMES)];
const ownTones = [];
const need = Math.max(1, 6 - fromA.length - fromB.length);
for (let i = 0; i < need; i++) {
ownTones.push(semitoneToFreq(parentRoot, randomChoice(ownScale)));
}
let childVoice = [...fromA, ...fromB, ...ownTones];
// Trim/pad to exactly 6 unique-ish tones
childVoice = childVoice.slice(0, 6);
while (childVoice.length < 6) {
childVoice.push(semitoneToFreq(parentRoot, randomChoice(ownScale)));
}
childVoice.sort((a, b) => a - b);
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,
bodyTemperature: (A.bodyTemperature + B.bodyTemperature) / 2 + random(-0.5, 0.5),
consecutiveRockHits: 0,
clanId: familyClan,
voice: childVoice // v4.5: inherited harmonic voice
});
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})`;
}
// v3.0: NEW - Update day/night cycle
updateDayNightCycle(dt) {
const cycleDuration = this.config.dayNightCycle || 180; // seconds
const cycleTickDuration = cycleDuration * this.config.globalTimeSteps;
this.globalTimeOfDay = (this.stepTick % cycleTickDuration) / cycleTickDuration;
// Determine phase: day (0-0.4), dusk (0.4-0.5), night (0.5-0.9), dawn (0.9-1.0)
if (this.globalTimeOfDay < 0.4) {
this.dayNightPhase = GameEngine.DAY_PHASE;
this.dayNightProgress = this.globalTimeOfDay / 0.4;
} else if (this.globalTimeOfDay < 0.5) {
this.dayNightPhase = GameEngine.DUSK_PHASE;
this.dayNightProgress = (this.globalTimeOfDay - 0.4) / 0.1;
} else if (this.globalTimeOfDay < 0.9) {
this.dayNightPhase = GameEngine.NIGHT_PHASE;
this.dayNightProgress = (this.globalTimeOfDay - 0.5) / 0.4;
} else {
this.dayNightPhase = GameEngine.DAWN_PHASE;
this.dayNightProgress = (this.globalTimeOfDay - 0.9) / 0.1;
}
// v3.1: Update sun position (rotates around planet)
this.sunLongitude = this.globalTimeOfDay * Math.PI * 2;
// v3.0: Update environment temperature based on day/night
const baseTemp = this.config.baseTemperature || 25;
const dayBonus = 8; // Warmer during day
const nightPenalty = -6; // Colder at night
if (this.dayNightPhase === GameEngine.DAY_PHASE) {
this.environmentTemperature = baseTemp + dayBonus * Math.sin(this.dayNightProgress * Math.PI);
} else if (this.dayNightPhase === GameEngine.NIGHT_PHASE) {
this.environmentTemperature = baseTemp + nightPenalty * Math.sin(this.dayNightProgress * Math.PI);
} else if (this.dayNightPhase === GameEngine.DUSK_PHASE) {
this.environmentTemperature = baseTemp + dayBonus * (1 - this.dayNightProgress);
} else { // DAWN
this.environmentTemperature = baseTemp + nightPenalty * (1 - this.dayNightProgress);
}
}
// v3.1: Get local daylight factor based on position on sphere
getLocalDaylightFactor(x, y) {
const N = this.world.N;
// Convert grid position to longitude (0 to 2*PI)
const longitude = (x / N) * Math.PI * 2;
// Calculate angle difference from sun position
let angleDiff = Math.abs(longitude - this.sunLongitude);
if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff;
// Daylight factor: 1.0 when facing sun, 0.0 when opposite
// Use cosine for smooth transition (terminator)
const rawFactor = Math.cos(angleDiff);
// Map from [-1, 1] to [0, 1] with sharper transition
const daylight = Math.max(0, Math.min(1, (rawFactor + 0.3) / 1.3));
return daylight;
}
// v3.1: Get local temperature based on position (sun exposure + latitude)
getLocalTemperature(x, y) {
const baseTemp = this.config.baseTemperature || 25;
const localDaylight = this.getLocalDaylightFactor(x, y);
// Latitude effect (equator warmer, poles colder)
const N = this.world.N;
const latitude = Math.abs(y - N/2) / (N/2); // 0 at equator, 1 at poles
const latitudePenalty = latitude * 8; // Up to 8°C colder at poles
// Sun exposure effect
const sunBonus = localDaylight * 12; // Up to 12°C warmer in direct sunlight
return baseTemp + sunBonus - latitudePenalty;
}
// v3.0: Get daylight factor for metabolic/behavior modulation
getDaylightFactor() {
// Returns 0.0 (full night) to 1.0 (full day)
if (this.dayNightPhase === GameEngine.DAY_PHASE) {
return 0.7 + 0.3 * Math.sin(this.dayNightProgress * Math.PI);
} else if (this.dayNightPhase === GameEngine.NIGHT_PHASE) {
return 0.2 - 0.15 * Math.sin(this.dayNightProgress * Math.PI);
} else if (this.dayNightPhase === GameEngine.DUSK_PHASE) {
return 0.7 - 0.5 * this.dayNightProgress;
} else { // DAWN
return 0.2 + 0.5 * this.dayNightProgress;
}
}
// v3.0: Bioinspired neurotransmitter modulation based on circadian rhythm
getCircadianNeuromodFactors() {
const daylight = this.getDaylightFactor();
return {
dopamineBoost: daylight * 0.3, // More active during day
serotoninBoost: (1 - daylight) * 0.25, // Melatonin-like, higher at night
norepinephrineBoost: Math.abs(daylight - 0.5) * 0.2 // Alert during transitions
};
}
step() {
if (this.paused || this.gameOver) return;
this.stepTick++;
const dt = 1.0 / this.config.globalTimeSteps;
// v3.0: Update day/night cycle
this.updateDayNightCycle(dt);
const daylightFactor = this.getDaylightFactor();
const circadianFactors = this.getCircadianNeuromodFactors();
// 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);
// v3.0: Metabolic rate affected by day/night and temperature
let metabolicMultiplier = 1.0;
// Slower metabolism at night (bioinspired circadian rhythm)
metabolicMultiplier *= (0.6 + 0.4 * daylightFactor);
// Temperature affects metabolism (colder = more energy to maintain temp)
const tempDiff = Math.abs(nxer.bodyTemperature - 37.0);
metabolicMultiplier *= (1.0 + tempDiff * 0.05);
// v3.0: Much lower metabolism when resting (not moving)
if (nxer.isResting) {
metabolicMultiplier *= 0.3; // 70% reduction when resting
}
nxer.food -= 0.01 * dt * metabolicMultiplier;
// v3.0: Apply circadian neuromodulator effects
nxer.net.neuromodulators.dopamine = clamp(
nxer.net.neuromodulators.dopamine + circadianFactors.dopamineBoost * dt * 0.1,
0, 2
);
nxer.net.neuromodulators.serotonin = clamp(
nxer.net.neuromodulators.serotonin + circadianFactors.serotoninBoost * dt * 0.1,
0, 2
);
nxer.net.neuromodulators.norepinephrine = clamp(
nxer.net.neuromodulators.norepinephrine + circadianFactors.norepinephrineBoost * dt * 0.1,
0, 2
);
// v3.1: Update local environment temperature based on position
const localTemp = this.getLocalTemperature(nxer.pos[0], nxer.pos[1]);
this.updateNxErTemperature(nxer, dt, occupantAt, localTemp);
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;
// v3.0: NEW INPUTS - DayNight, Temperature, Proprioception
// Input 7: Day/Night signal (-1=night, 0=transition, 1=day)
let dayNightInput = 0;
if (this.dayNightPhase === GameEngine.DAY_PHASE) {
dayNightInput = 1;
} else if (this.dayNightPhase === GameEngine.NIGHT_PHASE) {
dayNightInput = -1;
} else {
dayNightInput = 0; // Transitions (dusk/dawn)
}
// v3.1: Override with LOCAL daylight for this NxEr's position
const localDaylight = this.getLocalDaylightFactor(nxer.pos[0], nxer.pos[1]);
if (localDaylight > 0.6) dayNightInput = 1;
else if (localDaylight < 0.3) dayNightInput = -1;
else dayNightInput = 0;
// Input 8: Temperature comfort (-1=cold, 0=optimal, 1=hot)
let tempInput = 0;
if (nxer.bodyTemperature < nxer.optimalTempLow) {
tempInput = -1; // Too cold
} else if (nxer.bodyTemperature > nxer.optimalTempHigh) {
tempInput = 1; // Too hot
} else {
tempInput = 0; // Comfortable
}
// Input 9: Proprioception - obstacle awareness (-1=stuck, 0=clear, 1=recent hit)
let proprioInput = 0;
if (nxer.consecutiveRockHits >= 3) {
proprioInput = -1; // Stuck - need to change direction
// v3.0: Force direction change when stuck
nxer.heading = (nxer.heading + randomInt(2, 6)) % 8;
nxer.consecutiveRockHits = 0; // Reset after forced turn
// Stress response - increase norepinephrine
nxer.net.neuromodulators.norepinephrine = Math.min(2.0,
nxer.net.neuromodulators.norepinephrine + 0.3);
} else if (nxer.consecutiveRockHits > 0) {
proprioInput = 1; // Recent hit, be cautious
}
// v3.0: Night behavior - tendency to rest
let restTendency = 0;
if (this.dayNightPhase === GameEngine.NIGHT_PHASE && nxer.food > this.config.startFood * 0.5) {
// Well-fed NxErs rest more at night
restTendency = Math.random() < 0.3 ? 1 : 0;
}
// v4.0: Construct full inputs (Last 3 + 3 Sensory + 3 Environment + 1 SphereSync)
// Input 10: Sphere synchronization (average branching ratio signal)
const sphereSync = nxer.net.branchingRatio > 1.2 ? 1 : (nxer.net.branchingRatio < 0.8 ? -1 : 0);
const inputs = [...nxer.lastInputs.slice(0, 3), hungerVal, sightVal, smellVal, dayNightInput, tempInput, proprioInput, sphereSync];
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(); }
// v4.0: Get Outputs (6 outputs from motor sphere)
// O1=X, O2=Y, O3=Steal, O4=Mate, O5=GiveFood, O6=Rest
let outs = nxer.net.getOutputStates();
while(outs.length < 6) outs.push(0);
let [O1, O2, O3, O4, O5, O6] = outs;
// v3.0: Rest output - if resting, reduce movement
if (O6 === 1 || restTendency === 1) {
// Resting behavior - stay still, recover energy slightly
O1 = 0;
O2 = 0;
nxer.isResting = true;
nxer.food += 0.02 * dt; // Improved energy recovery from rest
// Resting helps regulate temperature
nxer.bodyTemperature += (37.0 - nxer.bodyTemperature) * 0.01;
} else {
nxer.isResting = false;
}
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;
// v3.0: Temperature-seeking behavior
if (tempInput === -1 && this.dayNightPhase === GameEngine.DAY_PHASE) {
// Cold NxEr during day - seek sunlight (move towards equator)
if (nxer.pos[1] < this.world.N / 2) O2 = 1; // Move south
else O2 = -1; // Move north
}
// --- 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;
// v4.5: HEARING-DRIVEN ATTRACTION
// If this NxEr recently heard a clan-mate sing, bias its
// movement toward the song's location. The bias is gentle
// (only when the brain hasn't decided something urgent),
// and decays with time since the song was heard.
let biasedDx = dx, biasedDy = dy;
if (nxer.lastHeardSongPos && (this.stepTick - nxer.lastHeardSongTick) < 4 * this.config.globalTimeSteps) {
const songFreshness = 1 - ((this.stepTick - nxer.lastHeardSongTick) / (4 * this.config.globalTimeSteps));
// Only override if not chasing food/mate (sightVal != 1, O4 != 1)
if (sightVal !== 1 && O4 !== 1 && Math.random() < 0.5 * songFreshness) {
const N = this.world.N;
// Wrapped X delta toward song
let dxToSong = nxer.lastHeardSongPos[0] - nxer.pos[0];
if (dxToSong > N/2) dxToSong -= N;
else if (dxToSong < -N/2) dxToSong += N;
const dyToSong = nxer.lastHeardSongPos[1] - nxer.pos[1];
if (Math.abs(dxToSong) <= 1 && Math.abs(dyToSong) <= 1) {
// Already next to the singer — clear the call
nxer.lastHeardSongPos = null;
} else {
biasedDx = Math.sign(dxToSong);
biasedDy = Math.sign(dyToSong);
// Tiny serotonin uplift while following the call
if (nxer.net && nxer.net.neuromodulators) {
nxer.net.neuromodulators.serotonin = Math.min(2.0,
(nxer.net.neuromodulators.serotonin || 0) + 0.005);
}
}
}
}
// v4.5: VOICE-SIMILARITY FOLLOWING
// NxErs with very similar voices tend to drift toward each
// other, producing visible "tone clans" within larger clans.
if (biasedDx === 0 && biasedDy === 0 && Math.random() < 0.15) {
let bestSim = 0, bestTarget = null;
for (const [k, occ] of occupantAt) {
if (occ.id === nxer.id || !occ.voice) continue;
const sim = voiceSimilarity(nxer.voice, occ.voice);
if (sim < 0.5) continue;
const N = this.world.N;
let ddx = occ.pos[0] - nxer.pos[0];
if (ddx > N/2) ddx -= N; else if (ddx < -N/2) ddx += N;
const ddy = occ.pos[1] - nxer.pos[1];
const d2 = ddx*ddx + ddy*ddy;
if (d2 > 25) continue; // within ~5 tiles
if (sim > bestSim) {
bestSim = sim;
bestTarget = [Math.sign(ddx), Math.sign(ddy)];
}
}
if (bestTarget) {
biasedDx = bestTarget[0];
biasedDy = bestTarget[1];
}
}
// Update Heading based on (biased) move
const finalDx = biasedDx, finalDy = biasedDy;
if(finalDx !== 0 || finalDy !== 0) {
// Map dx,dy to 0-7
if(finalDx===-1 && finalDy===-1) nxer.heading=0;
else if(finalDx===0 && finalDy===-1) nxer.heading=1;
else if(finalDx===1 && finalDy===-1) nxer.heading=2;
else if(finalDx===1 && finalDy===0) nxer.heading=3;
else if(finalDx===1 && finalDy===1) nxer.heading=4;
else if(finalDx===0 && finalDy===1) nxer.heading=5;
else if(finalDx===-1 && finalDy===1) nxer.heading=6;
else if(finalDx===-1 && finalDy===0) nxer.heading=7;
}
nxer.pendingMove = {dx: finalDx, dy: finalDy, O3, O4};
}
});
this.processMovements(processNxErs.filter(n => n.pendingMove));
this.tryRespawnFood();
// v4.5: VOICE / SINGING pass — process song triggers, broadcast to clan,
// and emit visual+audio effects.
this.processSinging(dt);
this.effects = this.effects.filter(e => this.stepTick - e.startTick < this.config.globalTimeSteps);
// v5.0 Lite — population g (best-g NxEr). Throttled to every
// 30 ticks (cheap: O(N·7) + a 7×7 power iteration). Writes
// each living NxEr's PC1 factor score to stats.g; the result
// flows through the existing rankings / champions unchanged.
if (this.stepTick % 30 === 0) {
const aliveArr = [];
for (const a of this.nxers.values()) if (a.alive) aliveArr.push(a);
this.lastG = computePopulationG(aliveArr);
}
if (Array.from(this.nxers.values()).filter(n => n.alive).length === 0) this.gameOver = true;
}
// v4.5: SINGING / HEARING SYSTEM
// Decides when an NxEr sings, what it sounds like, and which nearby
// clan-mates can hear it. Also handles "grouping when observed"
// — when zoomed in, musical clan-mates clustered together harmonise.
processSinging(dt) {
const SING_RADIUS = 6; // tiles within which a song is heard
const GROUP_RADIUS = 4; // tiles for clan grouping
const zoomedIn = this.cameraIsClose === true; // set by renderer each frame
const N = this.world.N;
// Build a quick spatial index of alive NxErs
const aliveList = [];
this.nxers.forEach(n => { if (n.alive) aliveList.push(n); });
for (const nxer of aliveList) {
if (!nxer.voice) continue;
// 1) Cooldown gate
const ticksSinceSong = this.stepTick - nxer.lastSungTick;
const onCooldown = ticksSinceSong < (nxer.singCooldownTicks * this.config.globalTimeSteps / 30);
// 2) Decide whether to sing this tick
let trigger = null;
if (nxer.wantsToSing === 'discovery' && !onCooldown) {
trigger = 'discovery'; // first-time food
} else if (zoomedIn && !onCooldown && nxer.musicality > 0.45) {
// Look for nearby clan-mates — group singing when observed
let clanNeighbours = 0;
for (const other of aliveList) {
if (other.id === nxer.id || other.clanId === null) continue;
if (other.clanId !== nxer.clanId) continue;
const dx = Math.min(Math.abs(nxer.pos[0] - other.pos[0]), N - Math.abs(nxer.pos[0] - other.pos[0]));
const dy = Math.abs(nxer.pos[1] - other.pos[1]);
if (dx <= GROUP_RADIUS && dy <= GROUP_RADIUS) clanNeighbours++;
}
if (clanNeighbours >= 1 && Math.random() < 0.04 * nxer.musicality) {
trigger = clanNeighbours >= 2 ? 'chorus' : 'solo';
}
} else if (!onCooldown && nxer.musicality > 0.7 && Math.random() < 0.002) {
// Rare spontaneous song from very musical individuals
trigger = 'solo';
}
if (!trigger) {
nxer.wantsToSing = null;
continue;
}
// 3) Compute pitch bonus from food (more food → higher tone)
const foodNorm = Math.min(1, Math.max(0, (nxer.food - this.config.startFood) / Math.max(1, this.config.startFood)));
const foodBonus = foodNorm * 5; // up to ~5 semitones higher
// 4) Emit song effect (visual notes + audio queue)
this.effects.push({
type: 'sing',
pos: [...nxer.pos],
startTick: this.stepTick,
voice: [...nxer.voice],
foodBonus,
musicality: nxer.musicality,
nxerId: nxer.id,
trigger
});
nxer.lastSungTick = this.stepTick;
nxer.songsSung++;
nxer.wantsToSing = null;
// 5) Broadcast to nearby clan-mates — they "hear" the song
for (const other of aliveList) {
if (other.id === nxer.id) continue;
// Only same clan hears as an attractive call
if (nxer.clanId === null || other.clanId !== nxer.clanId) continue;
const dx = Math.min(Math.abs(nxer.pos[0] - other.pos[0]), N - Math.abs(nxer.pos[0] - other.pos[0]));
const dy = Math.abs(nxer.pos[1] - other.pos[1]);
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist <= SING_RADIUS) {
other.lastHeardSongTick = this.stepTick;
other.lastHeardSongPos = [...nxer.pos];
other.lastHeardSongFromId = nxer.id;
// A pleasing song boosts serotonin slightly in the listener
if (other.net && other.net.neuromodulators) {
other.net.neuromodulators.serotonin = Math.min(2.0,
(other.net.neuromodulators.serotonin || 0) + 0.05 * nxer.musicality);
}
}
}
}
}
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];
nxer.isResting = true; // v3.0: Mark as resting when not moving
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
nxer.consecutiveRockHits++; // v3.0: Track wall hits too
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]);
list.forEach(i => i.nxer.consecutiveRockHits++); // v3.0: Track rock hits
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;
// v3.0: Clear rock hit counter on successful terrain validation
// 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];
// v4.5: First-time food discovery → sing!
if (!i.nxer.knownFoodIds.has(f.id)) {
i.nxer.knownFoodIds.add(f.id);
i.nxer.wantsToSing = 'discovery'; // flag picked up by step loop
}
});
// v3.0: Food consumption generates heat
valid.forEach(i => i.nxer.bodyTemperature += 0.1);
valid.forEach(i => i.nxer.consecutiveRockHits = 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]);
// v3.0: Successful movement clears rock hit counter
winner.consecutiveRockHits = 0;
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.isResting = false; // v3.0: Reset resting state on movement
}
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;
// v3.0: Movement generates heat
nxer.bodyTemperature += 0.02;
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', g: 'g' };
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]}`);
}
}
// v3.0: NEW - Temperature regulation system
updateNxErTemperature(nxer, dt, occupantAt, localEnvTemp = null) {
const baseTemp = 37.0; // Normal body temperature
// v3.1: Use local temperature if provided
const envTemp = localEnvTemp !== null ? localEnvTemp : this.environmentTemperature;
// Heat gain from food metabolism
const foodHeat = nxer.food > this.config.startFood ? 0.02 : -0.01;
// Heat gain from activity (movement increases temp)
const activityHeat = nxer.tickAccum > 0 ? 0.05 : 0.0;
// Environmental heat exchange (using local temp)
const envDiff = envTemp - nxer.bodyTemperature;
const envHeatExchange = envDiff * GameEngine.TEMP_TRANSFER_RATE * dt;
// Social thermoregulation - heat from nearby NxErs (bioinspired huddling)
let socialHeat = 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dx === 0 && dy === 0) continue;
const nx = (nxer.pos[0] + dx + this.world.N) % this.world.N;
const ny = nxer.pos[1] + dy;
if (ny < 0 || ny >= this.world.N) continue;
const neighbor = occupantAt.get(`${nx},${ny}`);
if (neighbor && neighbor.alive) {
// Heat transfer between NxErs
socialHeat += (neighbor.bodyTemperature - nxer.bodyTemperature) * 0.02;
}
}
}
// Update temperature
nxer.bodyTemperature += activityHeat + foodHeat + envHeatExchange + socialHeat;
// Homeostatic regulation (body tries to maintain 37°C)
const homeostaticCorrection = (baseTemp - nxer.bodyTemperature) * 0.05 * dt;
nxer.bodyTemperature += homeostaticCorrection;
// Clamp to survivable range
nxer.bodyTemperature = clamp(nxer.bodyTemperature, 30.0, 42.0);
// Extreme temperatures cause damage
if (nxer.bodyTemperature < 32.0 || nxer.bodyTemperature > 41.0) {
nxer.food -= 0.1 * dt; // Energy drain from thermal stress
nxer.net.neuromodulators.norepinephrine = Math.min(2.0,
nxer.net.neuromodulators.norepinephrine + 0.1); // Stress response
}
}
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;
}
// v3.0: Add day/night and temperature stats
const phaseNames = ['Day', 'Dusk', 'Night', 'Dawn'];
let avgTemp = 0;
if (alive.length) {
avgTemp = alive.reduce((s, n) => s + n.bodyTemperature, 0) / alive.length;
}
return {
alive: alive.length,
dead: this.deathsCount,
born: this.birthsCount,
avgEnergy: avgE.toFixed(1),
avgBranching: avgB.toFixed(2),
dayNightPhase: phaseNames[this.dayNightPhase],
envTemp: this.environmentTemperature.toFixed(1),
avgBodyTemp: avgTemp.toFixed(1)
};
}
}
// =====================================================================
// 3D RENDERER (THREE.JS)
// =====================================================================
// =====================================================================
// PARTICLE SYSTEM FOR LIFE EVENTS
// =====================================================================
class ParticleSystem {
constructor(scene) {
this.scene = scene;
this.particleGroups = [];
this.maxParticles = 5000;
// Shared geometries and materials for performance
this.sparkGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(this.maxParticles * 3);
const colors = new Float32Array(this.maxParticles * 3);
const sizes = new Float32Array(this.maxParticles);
const alphas = new Float32Array(this.maxParticles);
this.sparkGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
this.sparkGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
this.sparkGeometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
this.sparkGeometry.setAttribute('alpha', new THREE.BufferAttribute(alphas, 1));
this.sparkMaterial = new THREE.ShaderMaterial({
uniforms: { time: { value: 0 } },
vertexShader: `
attribute float size;
attribute float alpha;
attribute vec3 color;
varying vec3 vColor;
varying float vAlpha;
void main() {
vColor = color;
vAlpha = alpha;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size * (300.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
varying vec3 vColor;
varying float vAlpha;
void main() {
float r = distance(gl_PointCoord, vec2(0.5));
if (r > 0.5) discard;
float glow = 1.0 - smoothstep(0.0, 0.5, r);
gl_FragColor = vec4(vColor, vAlpha * glow);
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
});
this.particles = [];
this.activeCount = 0;
}
emit(type, position, count = 20) {
const configs = {
birth: { color: [0.3, 1.0, 0.5], size: 2.5, speed: 0.08, life: 80, gravity: -0.001 },
death: { color: [0.6, 0.4, 0.8], size: 1.8, speed: 0.04, life: 120, gravity: 0.002 },
mate: { color: [1.0, 0.3, 0.5], size: 2.0, speed: 0.05, life: 60, gravity: 0.0 },
eat: { color: [1.0, 0.8, 0.2], size: 1.5, speed: 0.03, life: 40, gravity: -0.001 }
};
const cfg = configs[type] || configs.birth;
for (let i = 0; i < count; i++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.random() * Math.PI;
const vel = new THREE.Vector3(
Math.sin(phi) * Math.cos(theta),
Math.sin(phi) * Math.sin(theta),
Math.cos(phi)
).multiplyScalar(cfg.speed * (0.5 + Math.random()));
this.particles.push({
pos: position.clone(),
vel: vel,
color: cfg.color.map(c => c * (0.8 + Math.random() * 0.4)),
size: cfg.size * (0.7 + Math.random() * 0.6),
life: cfg.life,
maxLife: cfg.life,
gravity: cfg.gravity
});
}
}
update() {
const positions = this.sparkGeometry.attributes.position.array;
const colors = this.sparkGeometry.attributes.color.array;
const sizes = this.sparkGeometry.attributes.size.array;
const alphas = this.sparkGeometry.attributes.alpha.array;
let idx = 0;
this.particles = this.particles.filter(p => {
p.life--;
if (p.life <= 0 || idx >= this.maxParticles) return false;
p.vel.y += p.gravity;
p.pos.add(p.vel);
const i3 = idx * 3;
positions[i3] = p.pos.x; positions[i3+1] = p.pos.y; positions[i3+2] = p.pos.z;
colors[i3] = p.color[0]; colors[i3+1] = p.color[1]; colors[i3+2] = p.color[2];
sizes[idx] = p.size * (p.life / p.maxLife);
alphas[idx] = p.life / p.maxLife;
idx++;
return true;
});
this.sparkGeometry.setDrawRange(0, idx);
this.sparkGeometry.attributes.position.needsUpdate = true;
this.sparkGeometry.attributes.color.needsUpdate = true;
this.sparkGeometry.attributes.size.needsUpdate = true;
this.sparkGeometry.attributes.alpha.needsUpdate = true;
}
getMesh() {
if (!this.mesh) {
this.mesh = new THREE.Points(this.sparkGeometry, this.sparkMaterial);
this.mesh.frustumCulled = false;
this.scene.add(this.mesh);
}
return this.mesh;
}
}
class Renderer3D {
constructor(canvas) {
this.canvas = canvas;
this.scene = new THREE.Scene();
// v3.0: Dynamic background for day/night
this.dayColor = new THREE.Color(0x2a3a55);
this.nightColor = new THREE.Color(0x030305);
this.currentBgColor = new THREE.Color(0x101015);
this.scene.background = this.currentBgColor;
this.scene.fog = new THREE.Fog(this.currentBgColor, 500, 2000);
this.ambientLight = null;
this.dirLight = null;
// Animation time tracking
this.clock = new THREE.Clock();
this.animTime = 0;
// v3.1: Sun position tracking
this.sunAngle = 0;
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.webgl.toneMapping = THREE.ACESFilmicToneMapping;
this.webgl.toneMappingExposure = 1.2;
// Post-processing setup
this.composer = new EffectComposer(this.webgl);
const renderPass = new RenderPass(this.scene, this.camera);
this.composer.addPass(renderPass);
// Bloom pass for glow effects
this.bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
0.8, // strength
0.4, // radius
0.85 // threshold
);
this.composer.addPass(this.bloomPass);
// Particle system
this.particles = new ParticleSystem(this.scene);
this.controls = new OrbitControls(this.camera, this.canvas);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
// v3.1: Lower ambient for more contrast between day/night sides
this.ambientLight = new THREE.AmbientLight(0x303040, 0.4);
this.scene.add(this.ambientLight);
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x111122, 1.0);
this.scene.add(hemiLight);
// v3.1: Main sun light - will orbit around planet
this.dirLight = new THREE.DirectionalLight(0xffffee, 4.0);
this.dirLight.position.set(200, 50, 0);
this.dirLight.castShadow = true;
this.dirLight.shadow.mapSize.width = 4096;
this.dirLight.shadow.mapSize.height = 4096;
const d = 200;
this.dirLight.shadow.camera.left = -d; this.dirLight.shadow.camera.right = d;
this.dirLight.shadow.camera.top = d; this.dirLight.shadow.camera.bottom = -d;
this.dirLight.shadow.camera.near = 0.5; this.dirLight.shadow.camera.far = 500;
this.scene.add(this.dirLight);
// Sun sphere (visible glowing sun)
const sunGeo = new THREE.SphereGeometry(8, 32, 32);
const sunMat = new THREE.MeshBasicMaterial({
color: 0xffdd44,
transparent: true,
opacity: 1.0
});
this.sunMesh = new THREE.Mesh(sunGeo, sunMat);
this.sunMesh.position.set(200, 50, 0);
this.sunMesh.layers.enable(1); // Bloom layer
this.scene.add(this.sunMesh);
// Atmosphere glow around planet
this.atmosphereGlow = null;
// v3.1: Add a subtle backlight for night side visibility
this.nightLight = new THREE.DirectionalLight(0x4455aa, 0.08);
this.nightLight.position.set(-200, 30, 0);
this.scene.add(this.nightLight);
// v3.1: Add point light at sun position for glow effect
this.sunGlow = new THREE.PointLight(0xffaa44, 2.0, 500);
this.sunGlow.position.set(200, 50, 0);
this.scene.add(this.sunGlow);
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);
this.composer.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);
// Atmosphere glow effect
if (this.atmosphereGlow) this.scene.remove(this.atmosphereGlow);
const atmosGeo = new THREE.SphereGeometry(this.planetRadius * 1.05, 64, 64);
const atmosMat = new THREE.ShaderMaterial({
uniforms: {
glowColor: { value: new THREE.Color(0x4488ff) },
viewVector: { value: this.camera.position }
},
vertexShader: `
varying vec3 vNormal;
void main() {
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`,
fragmentShader: `
uniform vec3 glowColor;
varying vec3 vNormal;
void main() {
float intensity = pow(0.7 - dot(vNormal, vec3(0.0, 0.0, 1.0)), 2.0);
gl_FragColor = vec4(glowColor, intensity * 0.4);
}`,
side: THREE.BackSide,
blending: THREE.AdditiveBlending,
transparent: true
});
this.atmosphereGlow = new THREE.Mesh(atmosGeo, atmosMat);
this.scene.add(this.atmosphereGlow);
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 delta = this.clock.getDelta();
this.animTime += delta;
let coldestId = null, hottestId = null, minTemp = Infinity, maxTemp = -Infinity;
for (const nx of nxers.values()) {
if (!nx.alive) continue;
if (nx.bodyTemperature < minTemp) { minTemp = nx.bodyTemperature; coldestId = nx.id; }
if (nx.bodyTemperature > maxTemp) { maxTemp = nx.bodyTemperature; hottestId = nx.id; }
}
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, 12, 12);
// Emissive material for glow effect
const baseColor = new THREE.Color(nxer.color);
const m = new THREE.MeshStandardMaterial({
color: nxer.color,
emissive: baseColor,
emissiveIntensity: 0.15,
roughness: 0.4,
metalness: 0.3
});
mesh = new THREE.Mesh(g, m);
mesh.castShadow = true;
mesh.userData = { id: nxer.id, type: 'nxer' };
// Energy ring with glow
const ringMat = new THREE.MeshBasicMaterial({
color: 0xff3333,
transparent: true,
opacity: 0.8
});
const r = new THREE.Mesh(new THREE.TorusGeometry(0.5, 0.04, 8, 24), ringMat);
r.rotation.x = Math.PI/2;
mesh.add(r);
// v4.0: Multi-sphere indicator rings (concentric colored rings for each sphere)
const sphereColors = nxer.net.spheres.map(s => {
const info = SPHERE_TYPES[s.type] || SPHERE_TYPES.association;
return new THREE.Color(info.hex);
});
for (let si = 0; si < Math.min(sphereColors.length, 5); si++) {
const sRing = new THREE.Mesh(
new THREE.TorusGeometry(0.3 + si * 0.12, 0.02, 8, 20),
new THREE.MeshBasicMaterial({ color: sphereColors[si], transparent: true, opacity: 0.5 })
);
sRing.rotation.x = Math.PI / 2;
sRing.userData.sphereRing = true;
sRing.userData.sphereIndex = si;
mesh.add(sRing);
}
// Gender indicator (small sphere on top)
const genderColor = nxer.isMale ? 0x4488ff : 0xff88aa;
const genderGeo = new THREE.SphereGeometry(0.08, 8, 8);
const genderMat = new THREE.MeshBasicMaterial({ color: genderColor });
const genderMarker = new THREE.Mesh(genderGeo, genderMat);
genderMarker.position.y = 0.4;
mesh.add(genderMarker);
const clanCanvas = document.createElement('canvas');
clanCanvas.width = 64; clanCanvas.height = 64;
const clanCtx = clanCanvas.getContext('2d');
const clanTexture = new THREE.CanvasTexture(clanCanvas);
const clanSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: clanTexture, transparent: true }));
clanSprite.scale.set(0.4, 0.4, 1);
clanSprite.position.y = 0.7;
mesh.add(clanSprite);
mesh.userData.clanSprite = clanSprite;
mesh.userData.clanCtx = clanCtx;
mesh.userData.clanTexture = clanTexture;
const tempCanvas = document.createElement('canvas');
tempCanvas.width = 64; tempCanvas.height = 64;
const tempCtx = tempCanvas.getContext('2d');
const tempTexture = new THREE.CanvasTexture(tempCanvas);
const tempSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: tempTexture, transparent: true }));
tempSprite.scale.set(0.5, 0.5, 1);
tempSprite.position.y = 1.0;
tempSprite.visible = false;
mesh.add(tempSprite);
mesh.userData.tempSprite = tempSprite;
mesh.userData.tempCtx = tempCtx;
mesh.userData.tempTexture = tempTexture;
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));
// Animated idle behavior - breathing/pulsing
const energyRatio = Math.max(0.3, Math.min(1.0, nxer.food / 100));
const breathe = 1.0 + Math.sin(this.animTime * 3 + nxer.id) * 0.05 * energyRatio;
mesh.scale.setScalar(breathe);
// Resting creatures bob slower and dimmer
if (nxer.isResting) {
mesh.scale.multiplyScalar(0.9);
mesh.material.emissiveIntensity = 0.05;
} else {
mesh.material.emissiveIntensity = 0.1 + energyRatio * 0.2;
}
// Low energy = more transparent/dimmer
if (nxer.food < 30) {
mesh.material.opacity = 0.5 + (nxer.food / 30) * 0.5;
mesh.material.transparent = true;
} else {
mesh.material.opacity = 1.0;
mesh.material.transparent = false;
}
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);
ring.rotation.z = this.animTime * 2; // Spinning ring
ring.material.color.setHex(nxer.id === this.selectedNxerId ? 0x00ffff : 0xff3333);
ring.material.opacity = 0.6 + Math.sin(this.animTime * 5) * 0.2;
}
// v4.0: Animate sphere indicator rings based on activity
mesh.children.forEach(child => {
if (child.userData && child.userData.sphereRing) {
const si = child.userData.sphereIndex;
const sphere = nxer.net.spheres[si];
if (sphere) {
const act = sphere.getActivityStats();
const actRatio = act.total > 0 ? (act.exc + act.inh) / act.total : 0;
child.rotation.z = this.animTime * (1.5 + si * 0.5);
child.material.opacity = 0.2 + actRatio * 0.6;
const pulse = 1.0 + Math.sin(this.animTime * 4 + si * 1.5) * 0.1 * actRatio;
child.scale.setScalar(pulse);
}
}
});
if (mesh.userData.clanSprite) {
if (nxer.clanId !== null) {
const ctx = mesh.userData.clanCtx;
ctx.clearRect(0, 0, 64, 64);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 40px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(nxer.clanId, 32, 32);
mesh.userData.clanTexture.needsUpdate = true;
mesh.userData.clanSprite.visible = true;
} else {
mesh.userData.clanSprite.visible = false;
}
}
if (mesh.userData.tempSprite) {
const ctx = mesh.userData.tempCtx;
ctx.clearRect(0, 0, 64, 64);
if (nxer.id === coldestId) {
ctx.font = '48px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('❄️', 32, 32);
mesh.userData.tempTexture.needsUpdate = true;
mesh.userData.tempSprite.visible = true;
} else if (nxer.id === hottestId) {
ctx.font = '48px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('🔥', 32, 32);
mesh.userData.tempTexture.needsUpdate = true;
mesh.userData.tempSprite.visible = true;
} else {
mesh.userData.tempSprite.visible = false;
}
}
}
}
// [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.OctahedronGeometry(0.4, 0);
const m = new THREE.MeshStandardMaterial({
color: 0xff4444,
emissive: 0xff2200,
emissiveIntensity: 0.4,
roughness: 0.3,
metalness: 0.5
});
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));
// Floating and rotating animation
mesh.rotateY(0.03);
const float = Math.sin(this.animTime * 2 + food.id * 0.5) * 0.1;
mesh.position.add(norm.multiplyScalar(float));
}
}
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.08));
ef.mesh.material.opacity = 1.0 - (age / 60.0);
ef.mesh.scale.multiplyScalar(1.02); // Growing effect
if (age >= 80) {
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);
// Emit particles for events
const pos3d = this.getSpherePos(e.pos[0], e.pos[1], engine.world.N, this.planetRadius + 1.0);
if (e.type === 'heart') {
this.particles.emit('birth', pos3d, 30);
this.particles.emit('mate', pos3d, 15);
} else if (e.type === 'skull') {
this.particles.emit('death', pos3d, 40);
} else if (e.type === 'sing') {
// v4.5: Musical note sprite + audio playback (gated by zoom)
const noteIcons = ['🎵', '🎶', '♪', '♫', '🎼'];
const numNotes = e.trigger === 'chorus' ? 3 : (e.trigger === 'discovery' ? 2 : 1);
for (let i = 0; i < numNotes; i++) {
const icon = noteIcons[Math.floor(Math.random() * noteIcons.length)];
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.2 + i * 0.2);
sprite.position.copy(pos);
// More musical voices = brighter/larger notes
const noteSize = 0.7 + (e.musicality || 0.5) * 0.7;
sprite.scale.set(noteSize, noteSize, noteSize);
// Slight horizontal drift per note
sprite.userData.drift = (Math.random() - 0.5) * 0.05;
this.scene.add(sprite);
this.effectMeshes.push({ mesh: sprite, startTick: e.startTick + i * 4 });
}
// Audio — only if camera is close enough (proximity > 0)
if (audioEngine.enabled && audioEngine.targetVolume > 0.05 && e.voice) {
if (e.trigger === 'chorus') {
audioEngine.playChord(e.voice, 0.9, 0.05, e.foodBonus || 0);
} else {
audioEngine.playMelody(e.voice, e.foodBonus || 0,
e.trigger === 'discovery' ? 0.13 : 0.10);
}
}
return; // sprite already added above
}
if (e.type === 'heart' || e.type === 'skull') {
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();
// Update particle system
this.particles.update();
}
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;
}
// v3.1: Update lighting with sun orbiting the planet
updateDayNightLighting(engine) {
if (!engine) return;
// v3.1: Get sun longitude and orbit the light around the planet
const sunLong = engine.sunLongitude || 0;
const orbitRadius = this.planetRadius ? this.planetRadius * 3 : 200;
// Calculate sun position (orbits in XZ plane)
const sunX = Math.cos(sunLong) * orbitRadius;
const sunZ = Math.sin(sunLong) * orbitRadius;
const sunY = 50; // Slight elevation
// Update directional light position
if (this.dirLight) {
this.dirLight.position.set(sunX, sunY, sunZ);
this.dirLight.target.position.set(0, 0, 0);
// Update sun mesh position
if (this.sunMesh) {
this.sunMesh.position.set(sunX, sunY, sunZ);
}
// Warm sunrise/sunset colors
const globalDaylight = engine.getDaylightFactor();
const warmth = Math.sin(globalDaylight * Math.PI); // Peak at midday
const sunColor = new THREE.Color().setHSL(0.1 - warmth * 0.05, 0.8, 0.5 + globalDaylight * 0.3);
this.dirLight.color = sunColor;
this.dirLight.intensity = 2.0 + globalDaylight * 3.0;
}
// Update night light (opposite side)
if (this.nightLight) {
this.nightLight.position.set(-sunX, sunY * 0.5, -sunZ);
}
// Update sun glow position
if (this.sunGlow) {
this.sunGlow.position.set(sunX, sunY, sunZ);
}
// Update atmosphere glow color based on time of day
if (this.atmosphereGlow && this.atmosphereGlow.material.uniforms) {
const daylightFactor = engine.getDaylightFactor();
const atmosColor = new THREE.Color().lerpColors(
new THREE.Color(0x2244aa), // Night - deep blue
new THREE.Color(0x88ccff), // Day - bright blue
daylightFactor
);
this.atmosphereGlow.material.uniforms.glowColor.value = atmosColor;
this.atmosphereGlow.material.uniforms.viewVector.value.copy(this.camera.position);
}
// v3.1: Background based on camera's view of sun
// If camera is looking at lit side, brighter background
const camDir = new THREE.Vector3();
this.camera.getWorldDirection(camDir);
const sunDir = new THREE.Vector3(sunX, sunY, sunZ).normalize();
const viewingSun = camDir.dot(sunDir);
const bgFactor = Math.max(0.1, Math.min(0.9, (viewingSun + 1) / 2));
const daylightFactor = engine.getDaylightFactor();
this.currentBgColor.copy(this.nightColor).lerp(this.dayColor, daylightFactor);
this.scene.background = this.currentBgColor;
this.scene.fog.color = this.currentBgColor;
// Adjust bloom intensity based on time of day
if (this.bloomPass) {
this.bloomPass.strength = 0.5 + daylightFactor * 0.5;
}
// Adjust ambient light intensity
if (this.ambientLight) {
this.ambientLight.intensity = 0.8 + daylightFactor * 1.2;
}
}
render(engine) {
this.updateNxErs(engine.nxers, engine.world);
this.updateFood(engine.foods, engine.world);
this.updateEffects(engine);
// v3.0: Update day/night lighting
this.updateDayNightLighting(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();
}
// Ensure particle mesh is created
this.particles.getMesh();
// v4.5: CAMERA PROXIMITY → audio gate + group-singing trigger
// The closer the camera is to the planet surface, the more audible
// the NxEr conversations become. Far away = silence.
if (this.planetRadius) {
const camDist = this.camera.position.length();
const surfaceDist = camDist - this.planetRadius;
const closeThreshold = this.planetRadius * 0.6; // very close
const farThreshold = this.planetRadius * 2.0; // out of earshot
let proximity = 1.0 - (surfaceDist - closeThreshold) / (farThreshold - closeThreshold);
proximity = Math.max(0, Math.min(1, proximity));
audioEngine.setProximity(proximity);
engine.cameraIsClose = proximity > 0.45;
this.lastProximity = proximity;
// Update on-screen badge
if (!this._badgeRefs) {
this._badgeRefs = {
icon: document.getElementById('audioBadgeIcon'),
text: document.getElementById('audioBadgeText')
};
}
if (this._badgeRefs.icon && this._badgeRefs.text) {
if (proximity > 0.7) {
this._badgeRefs.icon.textContent = '🔊';
this._badgeRefs.text.textContent = 'Listening to NxEr songs';
} else if (proximity > 0.45) {
this._badgeRefs.icon.textContent = '🔉';
this._badgeRefs.text.textContent = 'Faint singing nearby';
} else if (proximity > 0.05) {
this._badgeRefs.icon.textContent = '🔈';
this._badgeRefs.text.textContent = 'Whispers in the distance';
} else {
this._badgeRefs.icon.textContent = '🔇';
this._badgeRefs.text.textContent = 'Zoom in to hear NxErs sing';
}
}
}
// Floating notes: gentle horizontal drift while rising
this.effectMeshes.forEach(ef => {
if (ef.mesh.userData && ef.mesh.userData.drift) {
const tangent = new THREE.Vector3(-ef.mesh.position.z, 0, ef.mesh.position.x).normalize();
ef.mesh.position.add(tangent.multiplyScalar(ef.mesh.userData.drift));
}
});
this.controls.update();
this.composer.render(); // Use composer instead of direct render
}
}
// =====================================================================
// 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();
// v3.1: Add sun position indicator
const sunPos = ((this.engine.sunLongitude || 0) / (Math.PI * 2) * 360).toFixed(0);
const phaseIcon = this.engine.dayNightPhase === 0 ? '☀️' :
this.engine.dayNightPhase === 2 ? '🌙' : '🌓';
let html = `<div class="hud-section"><div class="hud-section-title">${phaseIcon} ${this.engine.getStats().dayNightPhase} | Sun: ${sunPos}° | Env: ${this.engine.getStats().envTemp}°C</div></div>`;
for (const [title, data] of Object.entries(rankings)) {
html += `<div class="hud-section"><div class="hud-section-title">${title} (${title === 'Best g' ? (data.best >= 0 ? '+' : '') + data.best.toFixed(3) : 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} | Born: ${stats.born} | Dead: ${stats.dead}</div><div class="hud-stats">Avg Energy: ${stats.avgEnergy} | Branching: ${stats.avgBranching}</div><div class="hud-stats">Avg Body: ${stats.avgBodyTemp}°C | 🧠 ${this.engine.config.brainArch === 'chc6' ? 'CHC g-capable (6)' : 'Classic ×' + (this.engine.config.brainSpheres || 3)}${this.engine.lastG ? ' | g PC1: ' + this.engine.lastG.pc1.toFixed(2) + ' manifold: ' + (this.engine.lastG.posManifold*100).toFixed(0) + '%' : ''}</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);
const byG = all.slice().sort((a, b) => (b.stats.g || 0) - (a.stats.g || 0)); // v5.0 Lite
// Helper to format entries (digits: g needs sign + 3 decimals)
const format = (arr, key, digits = 1) => arr.slice(0, 3).map(n => ({
name: n.alive ? n.name : `${n.name}†`,
value: typeof n.stats[key] === 'number'
? (digits === 3 ? (n.stats[key] >= 0 ? '+' : '') + n.stats[key].toFixed(3)
: n.stats[key].toFixed(digits))
: '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)
},
'Best g': {
entries: format(byG, 'g', 3),
best: getBestScore('g', byG)
}
};
}
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";
const headings = ["NW", "N", "NE", "E", "SE", "S", "SW", "W"];
const headingStr = headings[nxer.heading] || "?";
// v4.0: Get multi-sphere data
const spheresSummary = nxer.net.getSpheresSummary();
const projsSummary = nxer.net.getProjectionsSummary();
const receptors = nxer.net.getReceptorActivations();
// v4.5: Voice / tonal info
const NOTE_NAMES = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
const freqToNote = (f) => {
// A4 = 440 Hz reference
const semis = 12 * Math.log2(f / 440);
const midi = Math.round(69 + semis);
const note = NOTE_NAMES[((midi % 12) + 12) % 12];
const octave = Math.floor(midi / 12) - 1;
return `${note}${octave}`;
};
const voiceArr = nxer.voice || [];
const muScore = nxer.musicality || 0;
const muPct = Math.round(muScore * 100);
const muColor = muScore > 0.7 ? '#7be37b' : (muScore > 0.45 ? '#ffd166' : '#ff8888');
const voiceTonesHTML = voiceArr.map(f => {
const note = freqToNote(f);
return `<span style="display:inline-block;background:#1c1c28;border:1px solid #3a3a4a;padding:2px 5px;border-radius:4px;margin:1px;font-family:monospace;font-size:10px;color:#9fdfff">${note} <span style="color:#888;font-size:9px">${f.toFixed(0)}Hz</span></span>`;
}).join('');
const songFresh = nxer.lastSungTick > 0 && (nxer.engine ? false : true);
const songsSung = nxer.songsSung || 0;
const tonesLearned = nxer.tonesLearned || 0;
const lastHeardTicksAgo = nxer.lastHeardSongTick > 0
? `${songsSung > 0 ? 'recent' : '—'}` : '—';
const voiceHTML = `
<div class="detail-section" style="margin-top: 8px;">
<div style="color: #c084ff; font-weight: bold; margin-bottom: 4px;">🎼 Voice & Hearing</div>
<div class="detail-info">Tones: ${voiceTonesHTML}</div>
<div class="detail-info" style="margin-top:4px">
🎵 Musicality:
<span style="display:inline-block;width:80px;height:6px;background:#1a1a22;border-radius:3px;vertical-align:middle;overflow:hidden">
<span style="display:block;height:100%;width:${muPct}%;background:${muColor}"></span>
</span>
<span style="color:${muColor};font-family:monospace">${muPct}%</span>
</div>
<div class="detail-info">🎤 Songs: ${songsSung} 📚 Tones learned: ${tonesLearned}</div>
</div>
`;
// Build sphere cards HTML
let sphereHTML = '';
for (const s of spheresSummary) {
const oscAmps = s.oscAmps || {};
const oscBars = ['infraslow','slow','theta','alpha','gamma'].map(b => {
const h = Math.round((oscAmps[b] || 0) * 20) + 2;
const colors = {infraslow:'#666',slow:'#4488ff',theta:'#44ddaa',alpha:'#ffcc44',gamma:'#ff5588'};
return `<div class="osc-bar" style="height:${h}px;background:${colors[b]}"></div>`;
}).join('');
sphereHTML += `<div class="sphere-card">
<div class="sphere-header">
<div class="sphere-dot" style="background:${s.color}"></div>
<span class="sphere-name">${s.label}</span>
<span class="sphere-type">${s.id}</span>
</div>
<div class="sphere-stats">E:${s.stats.exc} I:${s.stats.inh} N:${s.stats.neutral} nrg:${s.stats.energy.toFixed(1)}</div>
<div class="osc-bars">${oscBars}</div>
</div>`;
}
// Build projection links HTML
let linkHTML = projsSummary.map(p => {
const typeClass = `link-type-${p.type}`;
return `<div class="link-card"><span class="link-type ${typeClass}">${p.type}</span> ${p.src} → ${p.tgt}</div>`;
}).join('');
// Build receptor grid HTML
const rNames = ['D1','D2','5HT1A','5HT2A','5HT4','M1','M2','β1','α2'];
const rKeys = ['D1','D2','5HT1A','5HT2A','5HT4','M1','M2','beta1','alpha2'];
let receptorHTML = rNames.map((n, i) => {
const val = (receptors[rKeys[i]] || 0).toFixed(2);
const pct = Math.round((receptors[rKeys[i]] || 0) * 100);
return `<div class="receptor-cell"><div class="rname">${n}</div><div class="rval">${val}</div></div>`;
}).join('');
// Build neuromod bars HTML
const nmData = [
{label:'🎯', key:'dopamine', color:'#ff5090'},
{label:'😊', key:'serotonin', color:'#44ddaa'},
{label:'💡', key:'acetylcholine', color:'#ffc040'},
{label:'⚡', key:'norepinephrine', color:'#4488ff'}
];
let nmHTML = nmData.map(nm => {
const val = (nxer.net.neuromodulators[nm.key] || 0);
const pct = Math.min(100, Math.round(val * 50));
return `<div class="nm-bar-row">
<span class="nm-bar-label">${nm.label}</span>
<div class="nm-bar-track"><div class="nm-bar-fill" style="width:${pct}%;background:${nm.color}"></div></div>
<span class="nm-bar-val">${val.toFixed(2)}</span>
</div>`;
}).join('');
this.detailContent.innerHTML = `
<div class="detail-tabs">
<div class="detail-tab active" onclick="this.parentNode.querySelectorAll('.detail-tab').forEach(t=>t.classList.remove('active'));this.classList.add('active');this.parentNode.parentNode.querySelectorAll('.detail-tab-content').forEach(c=>c.classList.remove('active'));this.parentNode.parentNode.querySelector('#dtab-bio').classList.add('active');">Bio</div>
<div class="detail-tab" onclick="this.parentNode.querySelectorAll('.detail-tab').forEach(t=>t.classList.remove('active'));this.classList.add('active');this.parentNode.parentNode.querySelectorAll('.detail-tab-content').forEach(c=>c.classList.remove('active'));this.parentNode.parentNode.querySelector('#dtab-brain').classList.add('active');">Brain</div>
<div class="detail-tab" onclick="this.parentNode.querySelectorAll('.detail-tab').forEach(t=>t.classList.remove('active'));this.classList.add('active');this.parentNode.parentNode.querySelectorAll('.detail-tab-content').forEach(c=>c.classList.remove('active'));this.parentNode.parentNode.querySelector('#dtab-chem').classList.add('active');">Chem</div>
</div>
<div class="detail-tab-content active" id="dtab-bio">
<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} <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: ${nxer.visionRange} 🧭 Facing: ${headingStr} 👃 Smell: ${nxer.smellRadius}</div>
</div>
<div class="detail-section">
<div class="detail-info">Lived: ${nxer.stats.timeLived.toFixed(1)}s Mates: ${nxer.stats.matesPerformed}</div>
<div class="detail-info">Found: ${nxer.stats.foodFound.toFixed(1)} Stolen: ${nxer.stats.foodTaken.toFixed(1)} Explored: ${nxer.stats.explored}</div>
<div class="detail-info">Energy: ${energyStatus.averageEnergy.toFixed(1)} Fitness: ${nxer.stats.fitness.toFixed(3)} ⭐ g: ${((nxer.stats.g||0)>=0?'+':'')+(nxer.stats.g||0).toFixed(3)}</div>
</div>
<div class="detail-section">
<div style="color: #ff9944; font-weight: bold; margin-bottom: 2px;">v4.0 Bioinspired:</div>
<div class="detail-info">🌡️ Body: ${nxer.bodyTemperature.toFixed(1)}°C 🧱 Rocks: ${nxer.consecutiveRockHits}</div>
<div class="detail-info">🧠 ${params.brainArch === 'chc6' ? 'CHC g-capable' : 'Classic chain'} · Spheres: ${params.numSpheres} ⚡ Neurons/Sph: ${params.neuronsPerSphere}</div>
</div>
${voiceHTML}
</div>
<div class="detail-tab-content" id="dtab-brain">
<div style="color: #40d0ff; font-weight: bold; font-size: 11px; margin-bottom: 4px;">🧠 Multi-Sphere Architecture</div>
${sphereHTML}
<div style="color: #ffc040; font-weight: bold; font-size: 11px; margin: 6px 0 4px;">🔗 Inter-Sphere Projections</div>
${linkHTML || '<div style="color:#666;font-size:10px;">No projections</div>'}
<div class="detail-section" style="margin-top: 8px;">
<div style="font-size: 10px; color: #888; font-family: monospace;">
steps=${params.simulationSteps} conn=${params.connectionProbability.toFixed(2)}<br>
τ_f=${params.tauFast.toFixed(1)} τ_s=${params.tauSlow.toFixed(1)} τ_m=${params.tauMeta.toFixed(0)}<br>
θ₊=${params.firingThresholdExcitatory.toFixed(2)} θ₋=${params.firingThresholdInhibitory.toFixed(2)}<br>
lr=${params.learningRate.toFixed(3)} chrono_ρ=${params.ctsn_rho.toFixed(2)}
</div>
</div>
</div>
<div class="detail-tab-content" id="dtab-chem">
<div style="color: #ff5090; font-weight: bold; font-size: 11px; margin-bottom: 4px;">💊 Neuromodulators</div>
${nmHTML}
<div style="color: #aa77dd; font-weight: bold; font-size: 11px; margin: 8px 0 4px;">🧬 Receptor Activations (9 subtypes)</div>
<div class="receptor-grid">${receptorHTML}</div>
<div class="detail-section" style="margin-top: 8px;">
<div style="font-size: 10px; color: #888;">
Branching: ${energyStatus.branchingRatio.toFixed(2)} |
AGMP: active | CTSN: ρ=${params.ctsn_rho.toFixed(2)} |
Chrono: α_f=${params.chronoAlphaFast}
</div>
</div>
</div>
<div style="margin-top: 8px;"><button class="hud-button" id="saveSingleBtn" style="width:100%">💾 Save NxEr</button></div>
`;
// Re-bind save button
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 v2.0 Multi-Sphere v4'
},
nxer: {
name: nxer.name,
color: nxer.color,
canLand: nxer.canLand,
canSea: nxer.canSea,
isMale: nxer.isMale,
visionRange: nxer.visionRange,
smellRadius: nxer.smellRadius,
heading: nxer.heading,
clanId: nxer.clanId,
stats: nxer.stats,
bodyTemperature: nxer.bodyTemperature,
consecutiveRockHits: nxer.consecutiveRockHits,
networkParams: nxer.net.params,
// v4.0: Save multi-sphere architecture info
brainArchitecture: {
numSpheres: nxer.net.params.numSpheres,
neuronsPerSphere: nxer.net.params.neuronsPerSphere,
sphereTypes: nxer.net.spheres.map(s => s.type),
projections: nxer.net.getProjectionsSummary(),
receptorActivations: nxer.net.getReceptorActivations()
},
// v4.5: Save voice/tonal info
voice: {
tones: nxer.voice,
musicality: nxer.musicality,
songsSung: nxer.songsSung,
tonesLearned: nxer.tonesLearned
}
}
};
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}_multisphere_v4.json`;
a.click();
URL.revokeObjectURL(url);
}
saveBestChampions() {
this.engine.updateAllTimeBest();
const data = {
meta: { created: new Date().toISOString(), type: 'Neuraxon v2.0 Multi-Sphere v4', version: '4.0' },
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,
brainArch: {
spheres: n.net.params.numSpheres,
neuronsPerSphere: n.net.params.neuronsPerSphere,
types: n.net.spheres ? n.net.spheres.map(s => s.type) : []
}
}));
}
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_multisphere_v4_bests.json`; a.click();
}
}
// =====================================================================
// MAIN APPLICATION
// =====================================================================
class Application {
constructor() {
this.setupSplashScreen();
}
setupSplashScreen() {
const splash = document.getElementById('splashScreen');
splash.addEventListener('click', () => {
// v4.5: Initialize AudioEngine on first user gesture (browser requirement)
audioEngine.init();
audioEngine.resume();
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','dayNightCycle','baseTemperature','brainSpheres','neuronsPerSphere','oscCoupling'];
sliders.forEach(id => {
const el = document.getElementById(id);
const span = document.getElementById(id+'Value');
el.addEventListener('input', () => span.textContent = el.value + (id.includes('Pct')?'%':''));
});
// v5.0 Lite — brain-architecture selector. CHC g-capable
// is a fixed 6-sphere layout, so the sphere-count slider
// is disabled (and pinned at 6) while it is selected.
const archSel = document.getElementById('brainArch');
const sphRow = document.getElementById('brainSpheresRow');
const sphEl = document.getElementById('brainSpheres');
const sphVal = document.getElementById('brainSpheresValue');
const applyArch = () => {
if (archSel.value === 'chc6') {
sphEl.disabled = true;
sphRow.style.opacity = '0.45';
sphVal.textContent = '6 (CHC fixed)';
} else {
sphEl.disabled = false;
sphRow.style.opacity = '1';
sphVal.textContent = sphEl.value;
}
};
archSel.addEventListener('change', applyArch);
applyArch();
document.getElementById('startButton').addEventListener('click', () => {
const config = {};
sliders.forEach(id => config[id] = parseFloat(document.getElementById(id).value));
// v5.0 Lite — brain architecture. CHC pins 6 spheres.
config.brainArch = archSel.value;
if (config.brainArch === 'chc6') config.brainSpheres = 6;
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('audioBadge').style.display = 'block';
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>