Felladrin commited on
Commit
17fd0c2
·
verified ·
1 Parent(s): 2af648f

Add the simulator files

Browse files
Files changed (14) hide show
  1. css/animations.css +256 -0
  2. css/main.css +337 -0
  3. css/neon-effects.css +278 -0
  4. index.html +118 -17
  5. js/ecosystem.js +350 -0
  6. js/entities.js +476 -0
  7. js/entityManager.js +82 -0
  8. js/genetics.js +324 -0
  9. js/neuralNetwork.js +286 -0
  10. js/particles.js +269 -0
  11. js/stats.js +270 -0
  12. js/ui.js +293 -0
  13. js/utils.js +257 -0
  14. js/world.js +456 -0
css/animations.css ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @keyframes fadeSlideIn {
2
+ 0% {
3
+ opacity: 0;
4
+ transform: translateY(10px);
5
+ }
6
+ 100% {
7
+ opacity: 1;
8
+ transform: translateY(0);
9
+ }
10
+ }
11
+
12
+ @keyframes fadeSlideInRight {
13
+ 0% {
14
+ opacity: 0;
15
+ transform: translateX(10px);
16
+ }
17
+ 100% {
18
+ opacity: 1;
19
+ transform: translateX(0);
20
+ }
21
+ }
22
+
23
+ @keyframes fadeSlideInCenter {
24
+ 0% {
25
+ opacity: 0;
26
+ transform: translateX(-50%) translateY(-10px);
27
+ }
28
+ 100% {
29
+ opacity: 1;
30
+ transform: translateX(-50%) translateY(0);
31
+ }
32
+ }
33
+
34
+ @keyframes pulse {
35
+ 0%,
36
+ 100% {
37
+ opacity: 1;
38
+ }
39
+ 50% {
40
+ opacity: 0.5;
41
+ }
42
+ }
43
+
44
+ @keyframes glowPulse {
45
+ 0%,
46
+ 100% {
47
+ text-shadow:
48
+ 0 0 4px currentColor,
49
+ 0 0 8px currentColor;
50
+ }
51
+ 50% {
52
+ text-shadow:
53
+ 0 0 6px currentColor,
54
+ 0 0 14px currentColor,
55
+ 0 0 20px currentColor;
56
+ }
57
+ }
58
+
59
+ @keyframes borderGlow {
60
+ 0%,
61
+ 100% {
62
+ border-color: rgba(100, 200, 255, 0.15);
63
+ box-shadow: 0 0 8px rgba(0, 240, 255, 0.05);
64
+ }
65
+ 50% {
66
+ border-color: rgba(100, 200, 255, 0.3);
67
+ box-shadow: 0 0 16px rgba(0, 240, 255, 0.1);
68
+ }
69
+ }
70
+
71
+ @keyframes dotBlink {
72
+ 0%,
73
+ 100% {
74
+ opacity: 1;
75
+ }
76
+ 50% {
77
+ opacity: 0.3;
78
+ }
79
+ }
80
+
81
+ @keyframes shimmer {
82
+ 0% {
83
+ background-position: -100% 0;
84
+ }
85
+ 100% {
86
+ background-position: 200% 0;
87
+ }
88
+ }
89
+
90
+ @keyframes eventSlideIn {
91
+ 0% {
92
+ opacity: 0;
93
+ transform: translateX(-20px);
94
+ max-height: 0;
95
+ }
96
+ 100% {
97
+ opacity: 1;
98
+ transform: translateX(0);
99
+ max-height: 30px;
100
+ }
101
+ }
102
+
103
+ @keyframes generationFlash {
104
+ 0% {
105
+ color: var(--neon-cyan);
106
+ text-shadow:
107
+ 0 0 10px var(--neon-cyan),
108
+ 0 0 20px var(--neon-cyan),
109
+ 0 0 40px var(--neon-cyan);
110
+ }
111
+ 100% {
112
+ color: var(--neon-cyan);
113
+ text-shadow:
114
+ 0 0 4px var(--neon-cyan),
115
+ 0 0 8px var(--neon-cyan);
116
+ }
117
+ }
118
+
119
+ @keyframes announcementPulse {
120
+ 0% {
121
+ background: rgba(0, 240, 255, 0.1);
122
+ border-color: rgba(0, 240, 255, 0.3);
123
+ }
124
+ 50% {
125
+ background: rgba(0, 240, 255, 0.15);
126
+ border-color: rgba(0, 240, 255, 0.5);
127
+ }
128
+ 100% {
129
+ background: rgba(0, 240, 255, 0.1);
130
+ border-color: rgba(0, 240, 255, 0.3);
131
+ }
132
+ }
133
+
134
+ @keyframes breathe {
135
+ 0%,
136
+ 100% {
137
+ opacity: 0.85;
138
+ }
139
+ 50% {
140
+ opacity: 0.95;
141
+ }
142
+ }
143
+
144
+ @keyframes scanLine {
145
+ 0% {
146
+ transform: translateY(-100%);
147
+ }
148
+ 100% {
149
+ transform: translateY(200%);
150
+ }
151
+ }
152
+
153
+ @keyframes dataPop {
154
+ 0% {
155
+ transform: scale(0);
156
+ opacity: 0;
157
+ }
158
+ 60% {
159
+ transform: scale(1.3);
160
+ opacity: 1;
161
+ }
162
+ 100% {
163
+ transform: scale(1);
164
+ opacity: 1;
165
+ }
166
+ }
167
+
168
+ #panel-stats {
169
+ animation: fadeSlideIn 0.5s ease-out both;
170
+ animation-delay: 0.1s;
171
+ }
172
+
173
+ #panel-performance {
174
+ animation: fadeSlideInRight 0.5s ease-out both;
175
+ animation-delay: 0.2s;
176
+ }
177
+
178
+ #panel-generation {
179
+ animation: fadeSlideInCenter 0.6s ease-out both;
180
+ animation-delay: 0.05s;
181
+ }
182
+
183
+ #panel-events {
184
+ animation: fadeSlideIn 0.5s ease-out both;
185
+ animation-delay: 0.3s;
186
+ }
187
+
188
+ #panel-charts {
189
+ animation: fadeSlideInRight 0.5s ease-out both;
190
+ animation-delay: 0.4s;
191
+ }
192
+
193
+ .hud-panel {
194
+ animation: borderGlow 4s ease-in-out infinite;
195
+ }
196
+
197
+ .stat-value {
198
+ animation: glowPulse 3s ease-in-out infinite;
199
+ }
200
+
201
+ .event-entry {
202
+ animation: eventSlideIn 0.3s ease-out both;
203
+ }
204
+
205
+ .live-indicator {
206
+ display: inline-block;
207
+ width: 6px;
208
+ height: 6px;
209
+ border-radius: 50%;
210
+ background: var(--neon-green);
211
+ margin-right: 6px;
212
+ animation: dotBlink 1.5s ease-in-out infinite;
213
+ }
214
+
215
+ .generation-flash {
216
+ animation: generationFlash 1s ease-out;
217
+ }
218
+
219
+ .announcement {
220
+ position: fixed;
221
+ top: 60px;
222
+ left: 50%;
223
+ transform: translateX(-50%);
224
+ z-index: var(--z-announcements);
225
+ padding: 8px 24px;
226
+ border-radius: 8px;
227
+ border: 1px solid var(--neon-cyan);
228
+ background: rgba(6, 6, 15, 0.9);
229
+ backdrop-filter: blur(12px);
230
+ font-family: var(--font-mono);
231
+ font-size: 13px;
232
+ color: var(--neon-cyan);
233
+ white-space: nowrap;
234
+ pointer-events: none;
235
+ animation: announcementPulse 2s ease-in-out;
236
+ opacity: 0;
237
+ transition: opacity 0.3s ease;
238
+ }
239
+
240
+ .announcement.visible {
241
+ opacity: 1;
242
+ }
243
+
244
+ @media (prefers-reduced-motion: reduce) {
245
+ .hud-panel,
246
+ .stat-value,
247
+ .event-entry,
248
+ .live-indicator,
249
+ .announcement {
250
+ animation: none !important;
251
+ }
252
+
253
+ .announcement {
254
+ transition: none !important;
255
+ }
256
+ }
css/main.css ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg-primary: #06060f;
3
+ --bg-secondary: #0c0c1e;
4
+ --bg-panel: rgba(12, 12, 30, 0.75);
5
+ --border-glow: rgba(100, 200, 255, 0.2);
6
+ --text-primary: #e0e8ff;
7
+ --text-secondary: #8090b0;
8
+ --text-muted: #506080;
9
+ --neon-cyan: #00f0ff;
10
+ --neon-green: #00ff88;
11
+ --neon-red: #ff3366;
12
+ --neon-blue: #3399ff;
13
+ --neon-amber: #ffaa00;
14
+ --neon-purple: #cc66ff;
15
+ --neon-pink: #ff66cc;
16
+ --neon-white: #eef4ff;
17
+ --gatherer-color: #00ff88;
18
+ --predator-color: #ff3366;
19
+ --builder-color: #3399ff;
20
+ --explorer-color: #ffaa00;
21
+ --hybrid-color: #cc66ff;
22
+ --food-color: #00ffaa;
23
+ --energy-color: #ffee44;
24
+ --panel-radius: 12px;
25
+ --panel-blur: 16px;
26
+ --font-mono: "Courier New", "Consolas", monospace;
27
+ --font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
28
+ --z-canvas: 1;
29
+ --z-particles: 2;
30
+ --z-hud: 10;
31
+ --z-announcements: 20;
32
+ --z-modal: 30;
33
+ }
34
+
35
+ *,
36
+ *::before,
37
+ *::after {
38
+ box-sizing: border-box;
39
+ margin: 0;
40
+ padding: 0;
41
+ }
42
+
43
+ html,
44
+ body {
45
+ width: 100%;
46
+ height: 100%;
47
+ overflow: hidden;
48
+ background: var(--bg-primary);
49
+ color: var(--text-primary);
50
+ font-family: var(--font-sans);
51
+ font-size: 14px;
52
+ line-height: 1.4;
53
+ -webkit-font-smoothing: antialiased;
54
+ -moz-osx-font-smoothing: grayscale;
55
+ }
56
+
57
+ #app {
58
+ position: relative;
59
+ width: 100%;
60
+ height: 100%;
61
+ overflow: hidden;
62
+ }
63
+
64
+ #simulation-canvas {
65
+ position: absolute;
66
+ top: 0;
67
+ left: 0;
68
+ width: 100%;
69
+ height: 100%;
70
+ z-index: var(--z-canvas);
71
+ display: block;
72
+ }
73
+
74
+ #particle-canvas {
75
+ position: absolute;
76
+ top: 0;
77
+ left: 0;
78
+ width: 100%;
79
+ height: 100%;
80
+ z-index: var(--z-particles);
81
+ pointer-events: none;
82
+ display: block;
83
+ }
84
+
85
+ .hud-panel {
86
+ position: absolute;
87
+ z-index: var(--z-hud);
88
+ background: var(--bg-panel);
89
+ backdrop-filter: blur(var(--panel-blur));
90
+ -webkit-backdrop-filter: blur(var(--panel-blur));
91
+ border: 1px solid var(--border-glow);
92
+ border-radius: var(--panel-radius);
93
+ padding: 12px 16px;
94
+ pointer-events: auto;
95
+ transition:
96
+ opacity 0.3s ease,
97
+ transform 0.3s ease;
98
+ }
99
+
100
+ .hud-panel:focus-within {
101
+ border-color: var(--neon-cyan);
102
+ box-shadow: 0 0 12px rgba(0, 240, 255, 0.15);
103
+ }
104
+
105
+ #panel-stats {
106
+ top: 16px;
107
+ left: 16px;
108
+ min-width: 220px;
109
+ }
110
+
111
+ #panel-performance {
112
+ top: 16px;
113
+ right: 16px;
114
+ min-width: 160px;
115
+ text-align: right;
116
+ }
117
+
118
+ #panel-events {
119
+ bottom: 16px;
120
+ left: 16px;
121
+ right: 16px;
122
+ max-width: 600px;
123
+ max-height: 120px;
124
+ overflow-y: auto;
125
+ scrollbar-width: thin;
126
+ scrollbar-color: var(--text-muted) transparent;
127
+ }
128
+
129
+ #panel-charts {
130
+ bottom: 16px;
131
+ right: 16px;
132
+ width: 280px;
133
+ height: 200px;
134
+ }
135
+
136
+ #panel-generation {
137
+ top: 16px;
138
+ left: 50%;
139
+ transform: translateX(-50%);
140
+ text-align: center;
141
+ min-width: 180px;
142
+ }
143
+
144
+ .panel-title {
145
+ font-family: var(--font-mono);
146
+ font-size: 10px;
147
+ font-weight: 700;
148
+ text-transform: uppercase;
149
+ letter-spacing: 2px;
150
+ color: var(--text-muted);
151
+ margin-bottom: 8px;
152
+ }
153
+
154
+ .stat-value {
155
+ font-family: var(--font-mono);
156
+ font-size: 22px;
157
+ font-weight: 700;
158
+ color: var(--neon-cyan);
159
+ line-height: 1.1;
160
+ }
161
+
162
+ .stat-label {
163
+ font-family: var(--font-mono);
164
+ font-size: 11px;
165
+ color: var(--text-secondary);
166
+ }
167
+
168
+ .stat-row {
169
+ display: flex;
170
+ justify-content: space-between;
171
+ align-items: baseline;
172
+ padding: 3px 0;
173
+ }
174
+
175
+ .stat-row .dot {
176
+ display: inline-block;
177
+ width: 8px;
178
+ height: 8px;
179
+ border-radius: 50%;
180
+ margin-right: 6px;
181
+ vertical-align: middle;
182
+ }
183
+
184
+ .stat-row .count {
185
+ font-family: var(--font-mono);
186
+ font-size: 13px;
187
+ font-weight: 600;
188
+ color: var(--text-primary);
189
+ min-width: 30px;
190
+ text-align: right;
191
+ }
192
+
193
+ .sparkline-container {
194
+ width: 60px;
195
+ height: 20px;
196
+ display: inline-block;
197
+ vertical-align: middle;
198
+ margin-left: 8px;
199
+ }
200
+
201
+ .event-entry {
202
+ font-family: var(--font-mono);
203
+ font-size: 11px;
204
+ color: var(--text-secondary);
205
+ padding: 2px 0;
206
+ border-bottom: 1px solid rgba(100, 200, 255, 0.05);
207
+ white-space: nowrap;
208
+ overflow: hidden;
209
+ text-overflow: ellipsis;
210
+ }
211
+
212
+ .event-entry .timestamp {
213
+ color: var(--text-muted);
214
+ margin-right: 8px;
215
+ }
216
+
217
+ .event-entry.event-birth {
218
+ color: var(--neon-green);
219
+ }
220
+ .event-entry.event-death {
221
+ color: var(--neon-red);
222
+ }
223
+ .event-entry.event-evolution {
224
+ color: var(--neon-purple);
225
+ }
226
+ .event-entry.event-world {
227
+ color: var(--neon-amber);
228
+ }
229
+ .event-entry.event-milestone {
230
+ color: var(--neon-cyan);
231
+ }
232
+
233
+ .chart-canvas {
234
+ width: 100%;
235
+ height: 100%;
236
+ display: block;
237
+ }
238
+
239
+ .hud-panel::-webkit-scrollbar {
240
+ width: 4px;
241
+ }
242
+
243
+ .hud-panel::-webkit-scrollbar-track {
244
+ background: transparent;
245
+ }
246
+
247
+ .hud-panel::-webkit-scrollbar-thumb {
248
+ background: var(--text-muted);
249
+ border-radius: 2px;
250
+ }
251
+
252
+ @media (max-width: 768px) {
253
+ .hud-panel {
254
+ padding: 8px 12px;
255
+ border-radius: 8px;
256
+ }
257
+
258
+ #panel-stats {
259
+ top: 8px;
260
+ left: 8px;
261
+ min-width: 180px;
262
+ }
263
+
264
+ #panel-performance {
265
+ top: 8px;
266
+ right: 8px;
267
+ min-width: 120px;
268
+ }
269
+
270
+ #panel-events {
271
+ bottom: 8px;
272
+ left: 8px;
273
+ right: 8px;
274
+ max-height: 80px;
275
+ font-size: 10px;
276
+ }
277
+
278
+ #panel-charts {
279
+ display: none;
280
+ }
281
+
282
+ #panel-generation {
283
+ top: 8px;
284
+ }
285
+
286
+ .stat-value {
287
+ font-size: 18px;
288
+ }
289
+ }
290
+
291
+ @media (max-width: 480px) {
292
+ #panel-charts {
293
+ display: none;
294
+ }
295
+
296
+ #panel-stats {
297
+ min-width: 150px;
298
+ }
299
+
300
+ #panel-performance {
301
+ min-width: 100px;
302
+ }
303
+
304
+ .stat-value {
305
+ font-size: 16px;
306
+ }
307
+ .stat-label {
308
+ font-size: 10px;
309
+ }
310
+ }
311
+
312
+ .sr-only {
313
+ position: absolute;
314
+ width: 1px;
315
+ height: 1px;
316
+ margin: -1px;
317
+ padding: 0;
318
+ overflow: hidden;
319
+ clip: rect(0, 0, 0, 0);
320
+ border: 0;
321
+ white-space: nowrap;
322
+ }
323
+
324
+ *:focus-visible {
325
+ outline: 2px solid var(--neon-cyan);
326
+ outline-offset: 2px;
327
+ }
328
+
329
+ @media (prefers-reduced-motion: reduce) {
330
+ *,
331
+ *::before,
332
+ *::after {
333
+ animation-duration: 0.01ms !important;
334
+ animation-iteration-count: 1 !important;
335
+ transition-duration: 0.01ms !important;
336
+ }
337
+ }
css/neon-effects.css ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .glass-panel {
2
+ background: linear-gradient(
3
+ 135deg,
4
+ rgba(15, 15, 35, 0.8) 0%,
5
+ rgba(10, 10, 25, 0.6) 100%
6
+ );
7
+ backdrop-filter: blur(16px) saturate(1.2);
8
+ -webkit-backdrop-filter: blur(16px) saturate(1.2);
9
+ border: 1px solid rgba(100, 200, 255, 0.12);
10
+ box-shadow:
11
+ 0 4px 24px rgba(0, 0, 0, 0.4),
12
+ inset 0 1px 0 rgba(255, 255, 255, 0.04);
13
+ }
14
+
15
+ .glass-panel::before {
16
+ content: "";
17
+ position: absolute;
18
+ top: 0;
19
+ left: 0;
20
+ right: 0;
21
+ height: 1px;
22
+ background: linear-gradient(
23
+ 90deg,
24
+ transparent 0%,
25
+ rgba(100, 200, 255, 0.2) 50%,
26
+ transparent 100%
27
+ );
28
+ }
29
+
30
+ .neon-text-cyan {
31
+ color: var(--neon-cyan);
32
+ text-shadow:
33
+ 0 0 4px var(--neon-cyan),
34
+ 0 0 8px rgba(0, 240, 255, 0.4);
35
+ }
36
+
37
+ .neon-text-green {
38
+ color: var(--neon-green);
39
+ text-shadow:
40
+ 0 0 4px var(--neon-green),
41
+ 0 0 8px rgba(0, 255, 136, 0.4);
42
+ }
43
+
44
+ .neon-text-red {
45
+ color: var(--neon-red);
46
+ text-shadow:
47
+ 0 0 4px var(--neon-red),
48
+ 0 0 8px rgba(255, 51, 102, 0.4);
49
+ }
50
+
51
+ .neon-text-blue {
52
+ color: var(--neon-blue);
53
+ text-shadow:
54
+ 0 0 4px var(--neon-blue),
55
+ 0 0 8px rgba(51, 153, 255, 0.4);
56
+ }
57
+
58
+ .neon-text-amber {
59
+ color: var(--neon-amber);
60
+ text-shadow:
61
+ 0 0 4px var(--neon-amber),
62
+ 0 0 8px rgba(255, 170, 0, 0.4);
63
+ }
64
+
65
+ .neon-text-purple {
66
+ color: var(--neon-purple);
67
+ text-shadow:
68
+ 0 0 4px var(--neon-purple),
69
+ 0 0 8px rgba(204, 102, 255, 0.4);
70
+ }
71
+
72
+ .neon-border-cyan {
73
+ border-color: var(--neon-cyan);
74
+ box-shadow:
75
+ 0 0 6px rgba(0, 240, 255, 0.15),
76
+ inset 0 0 6px rgba(0, 240, 255, 0.05);
77
+ }
78
+
79
+ .neon-border-green {
80
+ border-color: var(--neon-green);
81
+ box-shadow:
82
+ 0 0 6px rgba(0, 255, 136, 0.15),
83
+ inset 0 0 6px rgba(0, 255, 136, 0.05);
84
+ }
85
+
86
+ .depth-1 {
87
+ box-shadow:
88
+ 0 2px 8px rgba(0, 0, 0, 0.3),
89
+ 0 0 1px rgba(100, 200, 255, 0.1);
90
+ }
91
+
92
+ .depth-2 {
93
+ box-shadow:
94
+ 0 4px 16px rgba(0, 0, 0, 0.4),
95
+ 0 0 2px rgba(100, 200, 255, 0.12);
96
+ }
97
+
98
+ .depth-3 {
99
+ box-shadow:
100
+ 0 8px 32px rgba(0, 0, 0, 0.5),
101
+ 0 0 4px rgba(100, 200, 255, 0.15);
102
+ }
103
+
104
+ .neon-divider {
105
+ width: 100%;
106
+ height: 1px;
107
+ border: none;
108
+ margin: 8px 0;
109
+ background: linear-gradient(
110
+ 90deg,
111
+ transparent 0%,
112
+ rgba(100, 200, 255, 0.2) 20%,
113
+ rgba(100, 200, 255, 0.2) 80%,
114
+ transparent 100%
115
+ );
116
+ }
117
+
118
+ .gradient-accent {
119
+ background: linear-gradient(
120
+ 135deg,
121
+ var(--neon-cyan) 0%,
122
+ var(--neon-purple) 100%
123
+ );
124
+ -webkit-background-clip: text;
125
+ background-clip: text;
126
+ -webkit-text-fill-color: transparent;
127
+ }
128
+
129
+ #panel-stats.hud-panel {
130
+ background: linear-gradient(
131
+ 160deg,
132
+ rgba(0, 255, 136, 0.03) 0%,
133
+ rgba(12, 12, 30, 0.75) 30%
134
+ );
135
+ }
136
+
137
+ #panel-performance.hud-panel {
138
+ background: linear-gradient(
139
+ 200deg,
140
+ rgba(0, 240, 255, 0.03) 0%,
141
+ rgba(12, 12, 30, 0.75) 30%
142
+ );
143
+ }
144
+
145
+ #panel-generation.hud-panel {
146
+ background: linear-gradient(
147
+ 180deg,
148
+ rgba(204, 102, 255, 0.05) 0%,
149
+ rgba(12, 12, 30, 0.8) 50%
150
+ );
151
+ }
152
+
153
+ #panel-events.hud-panel {
154
+ background: linear-gradient(
155
+ 0deg,
156
+ rgba(12, 12, 30, 0.85) 0%,
157
+ rgba(12, 12, 30, 0.7) 100%
158
+ );
159
+ }
160
+
161
+ #panel-charts.hud-panel {
162
+ background: linear-gradient(
163
+ 225deg,
164
+ rgba(51, 153, 255, 0.03) 0%,
165
+ rgba(12, 12, 30, 0.8) 30%
166
+ );
167
+ }
168
+
169
+ .scan-overlay {
170
+ position: absolute;
171
+ top: 0;
172
+ left: 0;
173
+ right: 0;
174
+ bottom: 0;
175
+ pointer-events: none;
176
+ z-index: 3;
177
+ background: repeating-linear-gradient(
178
+ 0deg,
179
+ transparent 0px,
180
+ transparent 2px,
181
+ rgba(100, 200, 255, 0.01) 2px,
182
+ rgba(100, 200, 255, 0.01) 4px
183
+ );
184
+ }
185
+
186
+ .type-gatherer {
187
+ color: var(--gatherer-color);
188
+ }
189
+ .type-predator {
190
+ color: var(--predator-color);
191
+ }
192
+ .type-builder {
193
+ color: var(--builder-color);
194
+ }
195
+ .type-explorer {
196
+ color: var(--explorer-color);
197
+ }
198
+ .type-hybrid {
199
+ color: var(--hybrid-color);
200
+ }
201
+
202
+ .dot-gatherer {
203
+ background: var(--gatherer-color);
204
+ }
205
+ .dot-predator {
206
+ background: var(--predator-color);
207
+ }
208
+ .dot-builder {
209
+ background: var(--builder-color);
210
+ }
211
+ .dot-explorer {
212
+ background: var(--explorer-color);
213
+ }
214
+ .dot-hybrid {
215
+ background: var(--hybrid-color);
216
+ }
217
+
218
+ .entity-tooltip {
219
+ position: absolute;
220
+ z-index: var(--z-modal);
221
+ padding: 8px 12px;
222
+ border-radius: 8px;
223
+ background: rgba(6, 6, 15, 0.95);
224
+ backdrop-filter: blur(12px);
225
+ border: 1px solid rgba(100, 200, 255, 0.2);
226
+ font-family: var(--font-mono);
227
+ font-size: 11px;
228
+ color: var(--text-primary);
229
+ pointer-events: none;
230
+ white-space: nowrap;
231
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
232
+ }
233
+
234
+ .entity-tooltip .tooltip-row {
235
+ padding: 1px 0;
236
+ }
237
+
238
+ .entity-tooltip .tooltip-label {
239
+ color: var(--text-muted);
240
+ margin-right: 8px;
241
+ }
242
+
243
+ .neon-bar {
244
+ height: 3px;
245
+ border-radius: 2px;
246
+ background: var(--bg-secondary);
247
+ overflow: hidden;
248
+ margin: 4px 0;
249
+ }
250
+
251
+ .neon-bar-fill {
252
+ height: 100%;
253
+ border-radius: 2px;
254
+ transition: width 0.3s ease;
255
+ }
256
+
257
+ .neon-bar-fill.cyan {
258
+ background: var(--neon-cyan);
259
+ box-shadow: 0 0 6px var(--neon-cyan);
260
+ }
261
+ .neon-bar-fill.green {
262
+ background: var(--neon-green);
263
+ box-shadow: 0 0 6px var(--neon-green);
264
+ }
265
+ .neon-bar-fill.red {
266
+ background: var(--neon-red);
267
+ box-shadow: 0 0 6px var(--neon-red);
268
+ }
269
+ .neon-bar-fill.amber {
270
+ background: var(--neon-amber);
271
+ box-shadow: 0 0 6px var(--neon-amber);
272
+ }
273
+
274
+ @media (prefers-reduced-motion: reduce) {
275
+ .neon-bar-fill {
276
+ transition: none;
277
+ }
278
+ }
index.html CHANGED
@@ -1,19 +1,120 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, user-scalable=no"
8
+ />
9
+ <meta
10
+ name="description"
11
+ content="Digital Life Evolution Simulator - An autonomous AI ecosystem demonstration"
12
+ />
13
+ <title>Digital Life Evolution Simulator</title>
14
+
15
+ <link rel="stylesheet" href="css/main.css" />
16
+ <link rel="stylesheet" href="css/animations.css" />
17
+ <link rel="stylesheet" href="css/neon-effects.css" />
18
+
19
+ <style>
20
+ html,
21
+ body {
22
+ background: #06060f;
23
+ }
24
+ #loading {
25
+ position: fixed;
26
+ top: 0;
27
+ left: 0;
28
+ right: 0;
29
+ bottom: 0;
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ background: #06060f;
34
+ z-index: 9999;
35
+ font-family: "Courier New", monospace;
36
+ color: #00f0ff;
37
+ font-size: 14px;
38
+ letter-spacing: 2px;
39
+ transition: opacity 0.5s ease;
40
+ }
41
+ #loading.fade-out {
42
+ opacity: 0;
43
+ pointer-events: none;
44
+ }
45
+ #loading .pulse {
46
+ animation: loadPulse 1.5s ease-in-out infinite;
47
+ }
48
+ @keyframes loadPulse {
49
+ 0%,
50
+ 100% {
51
+ opacity: 0.4;
52
+ }
53
+ 50% {
54
+ opacity: 1;
55
+ text-shadow:
56
+ 0 0 10px #00f0ff,
57
+ 0 0 20px #00f0ff;
58
+ }
59
+ }
60
+ </style>
61
+ </head>
62
+ <body>
63
+ <div id="loading" role="alert" aria-live="polite">
64
+ <span class="pulse">INITIALIZING ECOSYSTEM...</span>
65
+ </div>
66
+
67
+ <div
68
+ id="app"
69
+ role="application"
70
+ aria-label="Digital Life Evolution Simulator"
71
+ >
72
+ <a
73
+ href="#panel-stats"
74
+ class="sr-only"
75
+ style="position: absolute; z-index: 100"
76
+ >
77
+ Skip to simulation stats
78
+ </a>
79
+
80
+ <canvas
81
+ id="simulation-canvas"
82
+ aria-label="Simulation world with AI entities, resources, and environmental events"
83
+ role="img"
84
+ ></canvas>
85
+
86
+ <canvas id="particle-canvas" aria-hidden="true"></canvas>
87
+
88
+ <div class="scan-overlay" aria-hidden="true"></div>
89
+ </div>
90
+
91
+ <script type="module">
92
+ import { bootstrap } from "./js/ecosystem.js";
93
+
94
+ if (document.readyState === "loading") {
95
+ document.addEventListener("DOMContentLoaded", start);
96
+ } else {
97
+ start();
98
+ }
99
+
100
+ function start() {
101
+ requestAnimationFrame(() => {
102
+ try {
103
+ bootstrap();
104
+ } catch (err) {
105
+ console.error("Bootstrap failed:", err);
106
+ document.getElementById("loading").innerHTML =
107
+ '<span style="color:#ff3366">ERROR: ' + err.message + "</span>";
108
+ return;
109
+ }
110
+
111
+ const loading = document.getElementById("loading");
112
+ if (loading) {
113
+ loading.classList.add("fade-out");
114
+ setTimeout(() => loading.remove(), 600);
115
+ }
116
+ });
117
+ }
118
+ </script>
119
+ </body>
120
  </html>
js/ecosystem.js ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { World, EVENT_TYPES } from "./world.js";
2
+ import { EntityManager } from "./entityManager.js";
3
+ import { Entity } from "./entities.js";
4
+ import { ParticleSystem } from "./particles.js";
5
+ import { StatsTracker } from "./stats.js";
6
+ import { UI } from "./ui.js";
7
+ import { createGenome, ENTITY_TYPES } from "./genetics.js";
8
+ import { randomRange } from "./utils.js";
9
+
10
+ const SAVE_KEY = "digital-life-simulator-state";
11
+ const TARGET_FPS = 60;
12
+ const MIN_POPULATION = 15;
13
+ const MAX_POPULATION = 80;
14
+ const INITIAL_POPULATION = 35;
15
+ const GENERATION_INTERVAL = 45;
16
+
17
+ export class Ecosystem {
18
+ constructor() {
19
+ this.world = null;
20
+ this.entityManager = null;
21
+ this.particles = null;
22
+ this.stats = null;
23
+ this.ui = null;
24
+
25
+ this.simCanvas = null;
26
+ this.simCtx = null;
27
+ this.particleCanvas = null;
28
+ this.particleCtx = null;
29
+
30
+ this.running = false;
31
+ this._rafId = null;
32
+ this._lastTime = 0;
33
+ this._fpsAccum = 0;
34
+ this._fpsFrames = 0;
35
+ this._currentFps = 60;
36
+
37
+ this._genTimer = 0;
38
+ this._saveTimer = 0;
39
+ this._eventCheckTimer = 0;
40
+ this._lastEventCount = 0;
41
+ }
42
+
43
+ init() {
44
+ this.simCanvas = document.getElementById("simulation-canvas");
45
+ this.particleCanvas = document.getElementById("particle-canvas");
46
+
47
+ if (!this.simCanvas || !this.particleCanvas) {
48
+ console.error("Canvas elements not found");
49
+ return;
50
+ }
51
+
52
+ this.simCtx = this.simCanvas.getContext("2d");
53
+ this.particleCtx = this.particleCanvas.getContext("2d");
54
+
55
+ this._resizeCanvases();
56
+ window.addEventListener("resize", () => this._resizeCanvases());
57
+
58
+ const w = this.simCanvas.width;
59
+ const h = this.simCanvas.height;
60
+
61
+ const saved = this._loadState();
62
+
63
+ if (saved) {
64
+ this.world = World.fromJSON(saved.world);
65
+ this.stats = StatsTracker.fromJSON(saved.stats);
66
+ this.entityManager = new EntityManager(w, h);
67
+
68
+ if (saved.entities && saved.entities.length > 0) {
69
+ for (const ed of saved.entities) {
70
+ try {
71
+ const { Genome } = await_import_workaround();
72
+ } catch (_) {}
73
+ }
74
+ }
75
+ this.entityManager.spawnRandom(INITIAL_POPULATION, this.world);
76
+ } else {
77
+ this.world = new World(w, h);
78
+ this.stats = new StatsTracker();
79
+ this.entityManager = new EntityManager(w, h);
80
+ this.entityManager.spawnRandom(INITIAL_POPULATION, this.world);
81
+ }
82
+
83
+ this.particles = new ParticleSystem(2000);
84
+
85
+ const container = document.getElementById("app");
86
+ this.ui = new UI(container);
87
+ this.ui.init();
88
+ this.ui.addEvent("Ecosystem initialized", "milestone");
89
+ this.ui.announce("Digital Life Simulator Active");
90
+
91
+ const types = Object.keys(ENTITY_TYPES);
92
+ for (const type of types) {
93
+ const count = this.entityManager.entities.filter(
94
+ (e) => e.type === type,
95
+ ).length;
96
+ if (count > 0) {
97
+ this.ui.addEvent(
98
+ `${ENTITY_TYPES[type].name}: ${count} spawned`,
99
+ "birth",
100
+ );
101
+ }
102
+ }
103
+ }
104
+
105
+ start() {
106
+ if (this.running) return;
107
+ this.running = true;
108
+ this._lastTime = performance.now();
109
+ this._loop(this._lastTime);
110
+ }
111
+
112
+ stop() {
113
+ this.running = false;
114
+ if (this._rafId) {
115
+ cancelAnimationFrame(this._rafId);
116
+ this._rafId = null;
117
+ }
118
+ }
119
+
120
+ _loop(timestamp) {
121
+ if (!this.running) return;
122
+ this._rafId = requestAnimationFrame((t) => this._loop(t));
123
+
124
+ const rawDt = (timestamp - this._lastTime) / 1000;
125
+ const dt = Math.min(rawDt, 0.05);
126
+ this._lastTime = timestamp;
127
+
128
+ this._fpsAccum += rawDt;
129
+ this._fpsFrames++;
130
+ if (this._fpsAccum >= 0.5) {
131
+ this._currentFps = this._fpsFrames / this._fpsAccum;
132
+ this._fpsFrames = 0;
133
+ this._fpsAccum = 0;
134
+ }
135
+
136
+ this._update(dt);
137
+
138
+ this._render();
139
+ }
140
+
141
+ _update(dt) {
142
+ this.world.update(dt);
143
+
144
+ this._eventCheckTimer += dt;
145
+ if (this._eventCheckTimer >= 1) {
146
+ this._eventCheckTimer = 0;
147
+ if (this.world.events.length > this._lastEventCount) {
148
+ const newEvent = this.world.events[this.world.events.length - 1];
149
+ this.ui.announceWorldEvent(newEvent);
150
+ this._lastEventCount = this.world.events.length;
151
+ }
152
+ if (this.world.events.length < this._lastEventCount) {
153
+ this._lastEventCount = this.world.events.length;
154
+ }
155
+ }
156
+
157
+ this.entityManager.update(dt, this.world, this.particles, this.stats);
158
+
159
+ this.particles.update(dt);
160
+
161
+ for (const event of this.world.events) {
162
+ if (event.type === "storm" || event.type === "bloom") {
163
+ this.particles.emitWeather(event, dt);
164
+ }
165
+ }
166
+
167
+ this._managePopulation(dt);
168
+
169
+ this._genTimer += dt;
170
+ if (this._genTimer >= GENERATION_INTERVAL) {
171
+ this._genTimer = 0;
172
+ this.stats.onGeneration();
173
+ this.ui.addEvent(
174
+ `Generation ${this.stats.generationCount} reached`,
175
+ "evolution",
176
+ );
177
+ this.ui.announce(`Generation ${this.stats.generationCount}`);
178
+
179
+ const genEl = document.getElementById("gen-count");
180
+ if (genEl) {
181
+ genEl.classList.remove("generation-flash");
182
+ void genEl.offsetWidth;
183
+ genEl.classList.add("generation-flash");
184
+ }
185
+ }
186
+
187
+ this.stats.record(
188
+ this.entityManager.entities,
189
+ this.world,
190
+ dt,
191
+ this._currentFps,
192
+ this.particles.count,
193
+ );
194
+
195
+ this._saveTimer += dt;
196
+ if (this._saveTimer >= 30) {
197
+ this._saveTimer = 0;
198
+ this._saveState();
199
+ }
200
+
201
+ this.ui.update(this.stats, this.world, this.entityManager, dt);
202
+ }
203
+
204
+ _render() {
205
+ const simCtx = this.simCtx;
206
+ const partCtx = this.particleCtx;
207
+ const w = this.world.width;
208
+ const h = this.world.height;
209
+
210
+ simCtx.fillStyle = "#06060f";
211
+ simCtx.fillRect(0, 0, w, h);
212
+ partCtx.clearRect(0, 0, w, h);
213
+
214
+ this.world.render(simCtx);
215
+
216
+ this._renderFrame = (this._renderFrame || 0) + 1;
217
+ if (this._renderFrame % 15 === 0) {
218
+ this.stats.renderHeatmap(simCtx, w, h);
219
+ }
220
+
221
+ this.entityManager.render(simCtx);
222
+
223
+ this.particles.render(partCtx);
224
+ }
225
+
226
+ _managePopulation(dt) {
227
+ const alive = this.entityManager.entities.length;
228
+
229
+ if (alive < MIN_POPULATION) {
230
+ const deficit = MIN_POPULATION - alive;
231
+ const toSpawn = Math.min(deficit, 5);
232
+
233
+ if (alive >= 2) {
234
+ this.entityManager.spawnFromPopulation(
235
+ this.entityManager.entities,
236
+ toSpawn,
237
+ this.world,
238
+ );
239
+ this.ui.addEvent(
240
+ `${toSpawn} entities evolved from survivors`,
241
+ "evolution",
242
+ );
243
+ } else {
244
+ this.entityManager.spawnRandom(toSpawn, this.world);
245
+ this.ui.addEvent(`${toSpawn} new entities seeded`, "birth");
246
+ }
247
+ }
248
+
249
+ this._diversityTimer = (this._diversityTimer || 0) + dt;
250
+ if (this._diversityTimer >= 8) {
251
+ this._diversityTimer = 0;
252
+ const types = Object.keys(ENTITY_TYPES);
253
+ const typeCounts = {};
254
+ for (const t of types) typeCounts[t] = 0;
255
+ for (const e of this.entityManager.entities) {
256
+ if (e.alive) typeCounts[e.type]++;
257
+ }
258
+
259
+ for (const type of types) {
260
+ if (typeCounts[type] < 2 && alive < MAX_POPULATION - 3) {
261
+ const count = Math.min(3, MAX_POPULATION - alive);
262
+ for (let i = 0; i < count; i++) {
263
+ const genome = createGenome(type);
264
+ const x = randomRange(40, this.world.width - 40);
265
+ const y = randomRange(40, this.world.height - 40);
266
+ const entity = new Entity(x, y, genome, this.stats.generationCount);
267
+ this.entityManager.add(entity);
268
+ }
269
+ this.ui.addEvent(
270
+ `${ENTITY_TYPES[type].name} migration wave (${count})`,
271
+ "birth",
272
+ );
273
+ }
274
+ }
275
+ }
276
+
277
+ if (alive > MAX_POPULATION) {
278
+ const excess = alive - MAX_POPULATION;
279
+ const sorted = [...this.entityManager.entities].sort(
280
+ (a, b) => a.fitness - b.fitness,
281
+ );
282
+ for (let i = 0; i < excess && i < sorted.length; i++) {
283
+ sorted[i].alive = false;
284
+ sorted[i].energy = 0;
285
+ }
286
+ }
287
+ }
288
+
289
+ _resizeCanvases() {
290
+ const w = window.innerWidth;
291
+ const h = window.innerHeight;
292
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
293
+
294
+ for (const canvas of [this.simCanvas, this.particleCanvas]) {
295
+ if (!canvas) continue;
296
+ canvas.width = w * dpr;
297
+ canvas.height = h * dpr;
298
+ canvas.style.width = w + "px";
299
+ canvas.style.height = h + "px";
300
+ const ctx = canvas.getContext("2d");
301
+ ctx.scale(dpr, dpr);
302
+ }
303
+
304
+ if (this.world) {
305
+ this.world.width = w;
306
+ this.world.height = h;
307
+ }
308
+
309
+ if (this.entityManager) {
310
+ this.entityManager.spatialGrid = new (Object.getPrototypeOf(
311
+ this.entityManager.spatialGrid,
312
+ ).constructor)(w, h, 60);
313
+ }
314
+ }
315
+
316
+ _saveState() {
317
+ try {
318
+ const state = {
319
+ world: this.world.toJSON(),
320
+ stats: this.stats.toJSON(),
321
+ version: 1,
322
+ };
323
+ localStorage.setItem(SAVE_KEY, JSON.stringify(state));
324
+ } catch (e) {}
325
+ }
326
+
327
+ _loadState() {
328
+ try {
329
+ const raw = localStorage.getItem(SAVE_KEY);
330
+ if (!raw) return null;
331
+ return JSON.parse(raw);
332
+ } catch (e) {
333
+ return null;
334
+ }
335
+ }
336
+ }
337
+
338
+ function await_import_workaround() {
339
+ return null;
340
+ }
341
+
342
+ export function bootstrap() {
343
+ const eco = new Ecosystem();
344
+ eco.init();
345
+ eco.start();
346
+
347
+ window.__ecosystem = eco;
348
+
349
+ return eco;
350
+ }
js/entities.js ADDED
@@ -0,0 +1,476 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { distance, clamp, TAU, COLORS, randomRange } from "./utils.js";
2
+ import { calculateFitness, NET_TOPOLOGY } from "./genetics.js";
3
+ import { QuantumDecisionLayer } from "./neuralNetwork.js";
4
+
5
+ let nextEntityId = 1;
6
+
7
+ const _glowCache = {};
8
+ function getGlowSprite(type, size) {
9
+ const key = `${type}_${Math.round(size)}`;
10
+ if (_glowCache[key]) return _glowCache[key];
11
+
12
+ const c = COLORS[type] || COLORS.cyan;
13
+ const spriteSize = Math.ceil(size * 5);
14
+ const canvas = document.createElement("canvas");
15
+ canvas.width = spriteSize;
16
+ canvas.height = spriteSize;
17
+ const ctx = canvas.getContext("2d");
18
+ const cx = spriteSize / 2;
19
+
20
+ const grad = ctx.createRadialGradient(cx, cx, size * 0.3, cx, cx, cx);
21
+ grad.addColorStop(0, `rgba(${c.r},${c.g},${c.b},0.3)`);
22
+ grad.addColorStop(0.4, `rgba(${c.r},${c.g},${c.b},0.1)`);
23
+ grad.addColorStop(1, `rgba(${c.r},${c.g},${c.b},0)`);
24
+ ctx.fillStyle = grad;
25
+ ctx.fillRect(0, 0, spriteSize, spriteSize);
26
+
27
+ _glowCache[key] = canvas;
28
+ return canvas;
29
+ }
30
+
31
+ export class Entity {
32
+ constructor(x, y, genome, generation = 0) {
33
+ this.id = nextEntityId++;
34
+ this.x = x;
35
+ this.y = y;
36
+ this.genome = genome;
37
+ this.type = genome.type;
38
+ this.generation = generation;
39
+ this.brain = genome.buildBrain();
40
+ this.quantumLayer = generation > 5 ? new QuantumDecisionLayer(0.8) : null;
41
+ const t = genome.traits;
42
+ this.energy = 60;
43
+ this.maxEnergy = 100;
44
+ this.health = 100;
45
+ this.maxHealth = 100;
46
+ this.speed = t.speed;
47
+ this.size = t.size;
48
+ this.visionRange = t.vision;
49
+ this.attackPower = t.attack;
50
+ this.defensePower = t.defense;
51
+ this.metabolism = t.metabolism;
52
+
53
+ this.vx = 0;
54
+ this.vy = 0;
55
+ this.angle = Math.random() * TAU;
56
+ this.age = 0;
57
+ this.alive = true;
58
+ this.signal = 0;
59
+ this.signalStrength = 0;
60
+ this.stats = { survivalTime: 0, energyGathered: 0, offspring: 0, kills: 0 };
61
+ this.fitness = 0;
62
+ this._thinkTimer = Math.random() * 0.15;
63
+ this._thinkInterval = 0.12;
64
+ this._reproTimer = 0;
65
+ this._reproInterval = 5;
66
+ this.lastOutputs = new Array(NET_TOPOLOGY[NET_TOPOLOGY.length - 1]).fill(0);
67
+ this._trail = [];
68
+ this._trailTimer = 0;
69
+ this._nearbyEntities = [];
70
+ this._nearbyResources = [];
71
+ }
72
+
73
+ update(dt, world, _allEntities, spatialGrid, particles) {
74
+ if (!this.alive) return;
75
+
76
+ this.age += dt;
77
+ this.stats.survivalTime = this.age;
78
+
79
+ this.energy -= this.metabolism * dt * 0.5;
80
+
81
+ const envDmg = world.getCatastropheDamage(this.x, this.y);
82
+ if (envDmg > 0) {
83
+ this.health -= envDmg * dt * 30;
84
+ }
85
+
86
+ if (this.energy <= 0 || this.health <= 0) {
87
+ this.alive = false;
88
+ particles.emit(this.x, this.y, "death", this._colorArray());
89
+ return;
90
+ }
91
+
92
+ this._thinkTimer += dt;
93
+ if (this._thinkTimer >= this._thinkInterval) {
94
+ this._thinkTimer = 0;
95
+ this._sense(world, spatialGrid);
96
+ this._think();
97
+ }
98
+
99
+ this._act(dt, world, spatialGrid, particles);
100
+
101
+ this._trailTimer += dt;
102
+ if (this._trailTimer > 0.08) {
103
+ this._trailTimer = 0;
104
+ this._trail.push({ x: this.x, y: this.y });
105
+ if (this._trail.length > 8) this._trail.shift();
106
+ }
107
+
108
+ this._reproTimer += dt;
109
+
110
+ this.fitness = calculateFitness(this.type, this.stats);
111
+ }
112
+
113
+ _sense(world, spatialGrid) {
114
+ this._nearbyEntities = spatialGrid.query(this.x, this.y, this.visionRange);
115
+
116
+ this._nearbyResources = world.getResourcesNear(
117
+ this.x,
118
+ this.y,
119
+ this.visionRange,
120
+ );
121
+ }
122
+
123
+ _think() {
124
+ const inputs = this._buildInputs();
125
+ const outputs = this.brain.forward(inputs);
126
+ this.lastOutputs = outputs;
127
+ }
128
+
129
+ _buildInputs() {
130
+ const inputs = new Array(NET_TOPOLOGY[0]).fill(0);
131
+ const vr = this.visionRange;
132
+
133
+ let nearestFood = null,
134
+ nearestFoodDist = Infinity;
135
+ for (const r of this._nearbyResources) {
136
+ const d = distance(this.x, this.y, r.x, r.y);
137
+ if (d < nearestFoodDist) {
138
+ nearestFoodDist = d;
139
+ nearestFood = r;
140
+ }
141
+ }
142
+
143
+ if (nearestFood) {
144
+ inputs[0] = (nearestFood.x - this.x) / vr;
145
+ inputs[1] = (nearestFood.y - this.y) / vr;
146
+ inputs[2] = 1 - nearestFoodDist / vr;
147
+ }
148
+
149
+ let nearestThreat = null,
150
+ nearestThreatDist = Infinity;
151
+ let nearestAlly = null,
152
+ nearestAllyDist = Infinity;
153
+ let allySignalSum = 0,
154
+ allyCount = 0;
155
+ let nearbyCount = 0;
156
+
157
+ for (const e of this._nearbyEntities) {
158
+ if (e.id === this.id || !e.alive) continue;
159
+ const d = distance(this.x, this.y, e.x, e.y);
160
+ nearbyCount++;
161
+
162
+ const isThreat =
163
+ (this.type !== "predator" && e.type === "predator") ||
164
+ e.attackPower > this.defensePower * 1.5;
165
+
166
+ if (isThreat && d < nearestThreatDist) {
167
+ nearestThreatDist = d;
168
+ nearestThreat = e;
169
+ }
170
+
171
+ const isAlly = e.type === this.type;
172
+ if (isAlly && d < nearestAllyDist) {
173
+ nearestAllyDist = d;
174
+ nearestAlly = e;
175
+ allySignalSum += e.signal;
176
+ allyCount++;
177
+ }
178
+ }
179
+
180
+ if (nearestThreat) {
181
+ inputs[3] = (nearestThreat.x - this.x) / vr;
182
+ inputs[4] = (nearestThreat.y - this.y) / vr;
183
+ inputs[5] = 1 - nearestThreatDist / vr;
184
+ }
185
+
186
+ if (nearestAlly) {
187
+ inputs[6] = (nearestAlly.x - this.x) / vr;
188
+ inputs[7] = (nearestAlly.y - this.y) / vr;
189
+ }
190
+
191
+ inputs[8] = this.energy / this.maxEnergy;
192
+ inputs[9] = this.health / this.maxHealth;
193
+ inputs[10] = clamp(nearbyCount / 10, 0, 1);
194
+ inputs[11] = allyCount > 0 ? allySignalSum / allyCount / 7 : 0;
195
+ inputs[12] = Math.sin(this.age * 0.5);
196
+ inputs[13] = 1;
197
+
198
+ return inputs;
199
+ }
200
+
201
+ _act(dt, world, spatialGrid, particles) {
202
+ const o = this.lastOutputs;
203
+
204
+ const moveX = o[0];
205
+ const moveY = o[1];
206
+ const moveLen = Math.sqrt(moveX * moveX + moveY * moveY);
207
+ if (moveLen > 0.01) {
208
+ const nx = moveX / moveLen;
209
+ const ny = moveY / moveLen;
210
+ this.vx = nx * this.speed * 30;
211
+ this.vy = ny * this.speed * 30;
212
+ this.angle = Math.atan2(ny, nx);
213
+ } else {
214
+ this.vx *= 0.9;
215
+ this.vy *= 0.9;
216
+ }
217
+
218
+ this.x += this.vx * dt;
219
+ this.y += this.vy * dt;
220
+
221
+ if (this.x < 0) this.x += world.width;
222
+ if (this.x >= world.width) this.x -= world.width;
223
+ if (this.y < 0) this.y += world.height;
224
+ if (this.y >= world.height) this.y -= world.height;
225
+
226
+ this._tryEat(world, particles, o[2]);
227
+
228
+ if (o[3] > 0.3 && this.attackPower > 0.3) {
229
+ this._tryAttack(spatialGrid, particles);
230
+ }
231
+
232
+ this.signal = Math.floor((o[5] + 1) * 4) % 8;
233
+ this.signalStrength = Math.abs(o[5]);
234
+ if (this.signalStrength > 0.5) {
235
+ particles.emit(this.x, this.y, "signal", [
236
+ COLORS[this.type].r,
237
+ COLORS[this.type].g,
238
+ COLORS[this.type].b,
239
+ ]);
240
+ }
241
+ }
242
+
243
+ _tryEat(world, particles, eagerness = 0) {
244
+ const eatRange = this.size + 20;
245
+ const resources = world.getResourcesNear(this.x, this.y, eatRange);
246
+ for (const r of resources) {
247
+ const d = distance(this.x, this.y, r.x, r.y);
248
+ if (d < eatRange) {
249
+ const efficiency = 0.4 + Math.max(0, eagerness) * 0.6;
250
+ const amount = r.consume(6 * efficiency);
251
+ this.energy = Math.min(this.maxEnergy, this.energy + amount * 1.5);
252
+ this.stats.energyGathered += amount;
253
+ if (amount > 0.5) particles.emit(r.x, r.y, "eat");
254
+ break;
255
+ }
256
+ }
257
+ }
258
+
259
+ _tryAttack(spatialGrid, particles) {
260
+ const attackRange = this.size + 12;
261
+ const nearby = spatialGrid.query(this.x, this.y, attackRange);
262
+ for (const e of nearby) {
263
+ if (e.id === this.id || !e.alive) continue;
264
+ if (e.type === this.type) continue;
265
+ const d = distance(this.x, this.y, e.x, e.y);
266
+ if (d < attackRange) {
267
+ const dmg = this.attackPower * 15 - e.defensePower * 5;
268
+ if (dmg > 0) {
269
+ e.health -= dmg;
270
+ this.energy = Math.min(this.maxEnergy, this.energy + dmg * 0.3);
271
+ particles.emit(e.x, e.y, "attack");
272
+ if (e.health <= 0) {
273
+ e.alive = false;
274
+ this.stats.kills++;
275
+ this.energy = Math.min(this.maxEnergy, this.energy + 20);
276
+ particles.emit(e.x, e.y, "death", this._colorArray());
277
+ }
278
+ }
279
+ break;
280
+ }
281
+ }
282
+ }
283
+
284
+ canReproduce() {
285
+ return (
286
+ this.alive &&
287
+ this.energy > 40 &&
288
+ this._reproTimer >= this._reproInterval &&
289
+ this.age > 2
290
+ );
291
+ }
292
+
293
+ tryReproduce(spatialGrid, particles) {
294
+ if (!this.canReproduce() || this.lastOutputs[4] < -0.2) return null;
295
+
296
+ const mateRange = this.visionRange * 0.5;
297
+ const nearby = spatialGrid.query(this.x, this.y, mateRange);
298
+ for (const e of nearby) {
299
+ if (e.id === this.id || !e.alive || e.type !== this.type) continue;
300
+ if (!e.canReproduce()) continue;
301
+
302
+ this.energy -= 18;
303
+ e.energy -= 18;
304
+ this._reproTimer = 0;
305
+ e._reproTimer = 0;
306
+ this.stats.offspring++;
307
+ e.stats.offspring++;
308
+
309
+ const childGenome = this.genome.crossover(e.genome);
310
+ childGenome.mutate(0.2, 0.06, 0.25);
311
+
312
+ const cx = (this.x + e.x) / 2 + randomRange(-10, 10);
313
+ const cy = (this.y + e.y) / 2 + randomRange(-10, 10);
314
+ const child = new Entity(
315
+ cx,
316
+ cy,
317
+ childGenome,
318
+ Math.max(this.generation, e.generation) + 1,
319
+ );
320
+
321
+ particles.emit(cx, cy, "reproduce", [
322
+ COLORS[this.type].r,
323
+ COLORS[this.type].g,
324
+ COLORS[this.type].b,
325
+ ]);
326
+ return child;
327
+ }
328
+ return null;
329
+ }
330
+
331
+ _colorArray() {
332
+ const c = COLORS[this.type];
333
+ return c ? [c.r, c.g, c.b] : [255, 255, 255];
334
+ }
335
+
336
+ render(ctx) {
337
+ if (!this.alive) return;
338
+ const c = COLORS[this.type] || COLORS.cyan;
339
+ const pulse = 0.85 + 0.15 * Math.sin(this.age * 3 + this.id);
340
+ const energyRatio = this.energy / this.maxEnergy;
341
+
342
+ if (this._trail.length > 1) {
343
+ ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},0.15)`;
344
+ ctx.lineWidth = 2;
345
+ ctx.beginPath();
346
+ ctx.moveTo(this._trail[0].x, this._trail[0].y);
347
+ for (let i = 1; i < this._trail.length; i++) {
348
+ ctx.lineTo(this._trail[i].x, this._trail[i].y);
349
+ }
350
+ ctx.stroke();
351
+ }
352
+
353
+ const glow = getGlowSprite(this.type, this.size);
354
+ const glowSize = glow.width;
355
+ ctx.drawImage(glow, this.x - glowSize / 2, this.y - glowSize / 2);
356
+
357
+ ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},0.5)`;
358
+ ctx.lineWidth = 1.5;
359
+ ctx.beginPath();
360
+ ctx.arc(this.x, this.y, this.size * pulse, 0, TAU);
361
+ ctx.stroke();
362
+
363
+ ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},0.65)`;
364
+ ctx.beginPath();
365
+ ctx.arc(this.x, this.y, this.size * 0.8 * pulse, 0, TAU);
366
+ ctx.fill();
367
+ ctx.fillStyle = `rgba(${Math.min(255, c.r + 100)},${Math.min(255, c.g + 100)},${Math.min(255, c.b + 100)},0.9)`;
368
+ ctx.beginPath();
369
+ ctx.arc(this.x, this.y, this.size * 0.3, 0, TAU);
370
+ ctx.fill();
371
+
372
+ ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},0.45)`;
373
+ const s = this.size * 0.55;
374
+ switch (this.type) {
375
+ case "predator":
376
+ this._drawTriangle(ctx, this.x, this.y, s);
377
+ break;
378
+ case "builder":
379
+ ctx.fillRect(this.x - s * 0.6, this.y - s * 0.6, s * 1.2, s * 1.2);
380
+ break;
381
+ case "explorer":
382
+ this._drawDiamond(ctx, this.x, this.y, s);
383
+ break;
384
+ case "hybrid":
385
+ this._drawStar(ctx, this.x, this.y, s, 5);
386
+ break;
387
+ default:
388
+ ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},0.3)`;
389
+ ctx.lineWidth = 1;
390
+ ctx.beginPath();
391
+ ctx.arc(this.x, this.y, this.size * 0.5, 0, TAU);
392
+ ctx.stroke();
393
+ }
394
+
395
+ const dirLen = this.size + 5;
396
+ ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},0.6)`;
397
+ ctx.lineWidth = 1.5;
398
+ ctx.beginPath();
399
+ ctx.moveTo(this.x, this.y);
400
+ ctx.lineTo(
401
+ this.x + Math.cos(this.angle) * dirLen,
402
+ this.y + Math.sin(this.angle) * dirLen,
403
+ );
404
+ ctx.stroke();
405
+
406
+ if (energyRatio < 0.9) {
407
+ ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},0.3)`;
408
+ ctx.lineWidth = 2;
409
+ ctx.beginPath();
410
+ ctx.arc(
411
+ this.x,
412
+ this.y,
413
+ this.size + 3,
414
+ -Math.PI / 2,
415
+ -Math.PI / 2 + TAU * energyRatio,
416
+ );
417
+ ctx.stroke();
418
+ }
419
+
420
+ if (this.health < this.maxHealth * 0.9) {
421
+ const barW = this.size * 3;
422
+ const barH = 2.5;
423
+ const barX = this.x - barW / 2;
424
+ const barY = this.y - this.size - 8;
425
+ ctx.fillStyle = "rgba(60,20,20,0.6)";
426
+ ctx.fillRect(barX, barY, barW, barH);
427
+ const healthRatio = this.health / this.maxHealth;
428
+ ctx.fillStyle =
429
+ healthRatio > 0.5
430
+ ? `rgba(0,255,80,0.7)`
431
+ : `rgba(255,${Math.floor(healthRatio * 2 * 255)},80,0.7)`;
432
+ ctx.fillRect(barX, barY, barW * healthRatio, barH);
433
+ }
434
+
435
+ if (this.signalStrength > 0.4) {
436
+ ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${this.signalStrength * 0.2})`;
437
+ ctx.lineWidth = 1;
438
+ ctx.beginPath();
439
+ ctx.arc(this.x, this.y, this.size + 8 + this.signalStrength * 5, 0, TAU);
440
+ ctx.stroke();
441
+ }
442
+ }
443
+
444
+ _drawTriangle(ctx, cx, cy, s) {
445
+ ctx.beginPath();
446
+ ctx.moveTo(cx, cy - s);
447
+ ctx.lineTo(cx - s * 0.87, cy + s * 0.5);
448
+ ctx.lineTo(cx + s * 0.87, cy + s * 0.5);
449
+ ctx.closePath();
450
+ ctx.fill();
451
+ }
452
+
453
+ _drawDiamond(ctx, cx, cy, s) {
454
+ ctx.beginPath();
455
+ ctx.moveTo(cx, cy - s);
456
+ ctx.lineTo(cx + s, cy);
457
+ ctx.lineTo(cx, cy + s);
458
+ ctx.lineTo(cx - s, cy);
459
+ ctx.closePath();
460
+ ctx.fill();
461
+ }
462
+
463
+ _drawStar(ctx, cx, cy, r, points) {
464
+ ctx.beginPath();
465
+ for (let i = 0; i < points * 2; i++) {
466
+ const rad = i % 2 === 0 ? r : r * 0.4;
467
+ const angle = (i * Math.PI) / points - Math.PI / 2;
468
+ const px = cx + Math.cos(angle) * rad;
469
+ const py = cy + Math.sin(angle) * rad;
470
+ if (i === 0) ctx.moveTo(px, py);
471
+ else ctx.lineTo(px, py);
472
+ }
473
+ ctx.closePath();
474
+ ctx.fill();
475
+ }
476
+ }
js/entityManager.js ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SpatialGrid, randomRange } from "./utils.js";
2
+ import { ENTITY_TYPES, createGenome, reproduce } from "./genetics.js";
3
+ import { Entity } from "./entities.js";
4
+
5
+ export class EntityManager {
6
+ constructor(worldWidth, worldHeight) {
7
+ this.entities = [];
8
+ this.spatialGrid = new SpatialGrid(worldWidth, worldHeight, 60);
9
+ this.worldWidth = worldWidth;
10
+ this.worldHeight = worldHeight;
11
+ }
12
+
13
+ add(entity) {
14
+ this.entities.push(entity);
15
+ }
16
+
17
+ update(dt, world, particles, stats) {
18
+ this.spatialGrid.clear();
19
+ for (const e of this.entities) {
20
+ if (e.alive) this.spatialGrid.insert(e, e.x, e.y);
21
+ }
22
+
23
+ const newborns = [];
24
+ for (const e of this.entities) {
25
+ e.update(dt, world, this.entities, this.spatialGrid, particles);
26
+
27
+ const child = e.tryReproduce(this.spatialGrid, particles);
28
+ if (child) {
29
+ newborns.push(child);
30
+ stats.onBirth();
31
+ }
32
+ }
33
+
34
+ for (const child of newborns) {
35
+ this.entities.push(child);
36
+ }
37
+
38
+ for (let i = this.entities.length - 1; i >= 0; i--) {
39
+ if (!this.entities[i].alive) {
40
+ stats.onDeath();
41
+ this.entities.splice(i, 1);
42
+ }
43
+ }
44
+ }
45
+
46
+ render(ctx) {
47
+ this.entities.sort((a, b) => a.y - b.y);
48
+ for (const e of this.entities) {
49
+ e.render(ctx);
50
+ }
51
+ }
52
+
53
+ getAlive() {
54
+ return this.entities.filter((e) => e.alive);
55
+ }
56
+
57
+ spawnRandom(count, world) {
58
+ const types = Object.keys(ENTITY_TYPES);
59
+ for (let i = 0; i < count; i++) {
60
+ const type = types[Math.floor(Math.random() * types.length)];
61
+ const genome = createGenome(type);
62
+ const x = randomRange(20, world.width - 20);
63
+ const y = randomRange(20, world.height - 20);
64
+ this.entities.push(new Entity(x, y, genome, 0));
65
+ }
66
+ }
67
+
68
+ spawnFromPopulation(population, count, world) {
69
+ if (population.length < 2) return;
70
+ const candidates = population.map((e) => ({
71
+ genome: e.genome,
72
+ fitness: e.fitness,
73
+ }));
74
+ for (let i = 0; i < count; i++) {
75
+ const childGenome = reproduce(candidates);
76
+ const x = randomRange(20, world.width - 20);
77
+ const y = randomRange(20, world.height - 20);
78
+ const gen = Math.max(...population.map((e) => e.generation)) + 1;
79
+ this.entities.push(new Entity(x, y, childGenome, gen));
80
+ }
81
+ }
82
+ }
js/genetics.js ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NeuralNetwork } from "./neuralNetwork.js";
2
+ import { randomRange, randomGaussian, clamp } from "./utils.js";
3
+
4
+ /** Neural network topology shared by all entities */
5
+ export const NET_TOPOLOGY = [14, 12, 8];
6
+
7
+ /** Entity archetype definitions */
8
+ export const ENTITY_TYPES = {
9
+ gatherer: {
10
+ name: "Gatherer",
11
+ baseTraits: {
12
+ speed: 2.5,
13
+ size: 8,
14
+ vision: 120,
15
+ attack: 0.2,
16
+ defense: 0.5,
17
+ metabolism: 0.8,
18
+ },
19
+ color: "#00ff88",
20
+ fitnessWeights: { survival: 1.0, energy: 2.0, offspring: 1.5, kills: 0.0 },
21
+ description: "Herbivores that seek food resources",
22
+ },
23
+ predator: {
24
+ name: "Predator",
25
+ baseTraits: {
26
+ speed: 3.5,
27
+ size: 10,
28
+ vision: 140,
29
+ attack: 1.5,
30
+ defense: 0.3,
31
+ metabolism: 1.2,
32
+ },
33
+ color: "#ff3366",
34
+ fitnessWeights: { survival: 0.5, energy: 1.0, offspring: 1.0, kills: 2.5 },
35
+ description: "Carnivores that hunt other entities",
36
+ },
37
+ builder: {
38
+ name: "Builder",
39
+ baseTraits: {
40
+ speed: 1.8,
41
+ size: 9,
42
+ vision: 100,
43
+ attack: 0.1,
44
+ defense: 1.5,
45
+ metabolism: 0.6,
46
+ },
47
+ color: "#3399ff",
48
+ fitnessWeights: { survival: 2.0, energy: 1.5, offspring: 2.0, kills: 0.0 },
49
+ description: "Defensive entities that build and fortify",
50
+ },
51
+ explorer: {
52
+ name: "Explorer",
53
+ baseTraits: {
54
+ speed: 4.0,
55
+ size: 6,
56
+ vision: 180,
57
+ attack: 0.3,
58
+ defense: 0.3,
59
+ metabolism: 1.0,
60
+ },
61
+ color: "#ffaa00",
62
+ fitnessWeights: { survival: 1.5, energy: 1.0, offspring: 1.0, kills: 0.5 },
63
+ description: "Fast scouts with wide vision range",
64
+ },
65
+ hybrid: {
66
+ name: "Hybrid",
67
+ baseTraits: {
68
+ speed: 3.0,
69
+ size: 8,
70
+ vision: 130,
71
+ attack: 0.8,
72
+ defense: 0.8,
73
+ metabolism: 1.0,
74
+ },
75
+ color: "#cc66ff",
76
+ fitnessWeights: { survival: 1.0, energy: 1.0, offspring: 1.5, kills: 1.0 },
77
+ description: "Balanced generalists that adapt to conditions",
78
+ },
79
+ };
80
+
81
+ /** Trait constraints to prevent runaway evolution */
82
+ const TRAIT_BOUNDS = {
83
+ speed: { min: 0.5, max: 8.0 },
84
+ size: { min: 2.0, max: 14.0 },
85
+ vision: { min: 30, max: 250 },
86
+ attack: { min: 0.0, max: 3.0 },
87
+ defense: { min: 0.0, max: 3.0 },
88
+ metabolism: { min: 0.2, max: 2.5 },
89
+ };
90
+
91
+ /**
92
+ * Genome encapsulates an entity's heritable traits and neural weights.
93
+ */
94
+ export class Genome {
95
+ /**
96
+ * @param {string} type - Entity archetype key
97
+ * @param {object} [traits] - Physical traits (or auto-generated)
98
+ * @param {number[]} [neuralWeights] - Flat weight array (or auto-generated)
99
+ */
100
+ constructor(type, traits = null, neuralWeights = null) {
101
+ this.type = type;
102
+ const base = ENTITY_TYPES[type].baseTraits;
103
+
104
+ if (traits) {
105
+ this.traits = { ...traits };
106
+ } else {
107
+ this.traits = {};
108
+ for (const key of Object.keys(base)) {
109
+ const variance = base[key] * 0.15;
110
+ this.traits[key] = clamp(
111
+ base[key] + randomGaussian(0, variance),
112
+ TRAIT_BOUNDS[key].min,
113
+ TRAIT_BOUNDS[key].max,
114
+ );
115
+ }
116
+ }
117
+
118
+ if (neuralWeights) {
119
+ this.neuralWeights = neuralWeights.slice();
120
+ } else {
121
+ const tempNet = new NeuralNetwork(NET_TOPOLOGY);
122
+ this.neuralWeights = tempNet.serialize();
123
+ }
124
+
125
+ /** Communication symbol preference (0-7), evolvable */
126
+ this.signalPreference = traits
127
+ ? traits._signal || Math.floor(Math.random() * 8)
128
+ : Math.floor(Math.random() * 8);
129
+ }
130
+
131
+ /** Create a brain (NeuralNetwork) from this genome's stored weights */
132
+ buildBrain() {
133
+ const nn = new NeuralNetwork(NET_TOPOLOGY);
134
+ nn.deserialize(this.neuralWeights);
135
+ return nn;
136
+ }
137
+
138
+ /** Deep copy */
139
+ clone() {
140
+ const g = new Genome(
141
+ this.type,
142
+ { ...this.traits },
143
+ this.neuralWeights.slice(),
144
+ );
145
+ g.signalPreference = this.signalPreference;
146
+ return g;
147
+ }
148
+
149
+ /**
150
+ * Single-point crossover of two genomes.
151
+ * Child inherits type from the fitter parent.
152
+ * @param {Genome} other
153
+ * @param {string} [childType] - Override child type
154
+ * @returns {Genome}
155
+ */
156
+ crossover(other, childType = null) {
157
+ const type = childType || this.type;
158
+
159
+ const childTraits = {};
160
+ for (const key of Object.keys(this.traits)) {
161
+ if (key === "_signal") continue;
162
+ const t = Math.random();
163
+ const raw = this.traits[key] * t + other.traits[key] * (1 - t);
164
+ childTraits[key] = clamp(
165
+ raw,
166
+ TRAIT_BOUNDS[key].min,
167
+ TRAIT_BOUNDS[key].max,
168
+ );
169
+ }
170
+
171
+ const w1 = this.neuralWeights;
172
+ const w2 = other.neuralWeights;
173
+ const len = Math.min(w1.length, w2.length);
174
+ const childWeights = new Array(len);
175
+ const crossPoint = Math.floor(Math.random() * len);
176
+ for (let i = 0; i < len; i++) {
177
+ childWeights[i] = i < crossPoint ? w1[i] : w2[i];
178
+ }
179
+
180
+ const child = new Genome(type, childTraits, childWeights);
181
+
182
+ child.signalPreference =
183
+ Math.random() < 0.5 ? this.signalPreference : other.signalPreference;
184
+
185
+ return child;
186
+ }
187
+
188
+ /**
189
+ * Apply mutations to traits and weights.
190
+ * @param {number} traitRate - Probability per trait (e.g. 0.3)
191
+ * @param {number} weightRate - Probability per weight (e.g. 0.05)
192
+ * @param {number} magnitude - Gaussian std for weight mutation
193
+ */
194
+ mutate(traitRate = 0.3, weightRate = 0.05, magnitude = 0.3) {
195
+ const base = ENTITY_TYPES[this.type].baseTraits;
196
+
197
+ for (const key of Object.keys(this.traits)) {
198
+ if (key === "_signal") continue;
199
+ if (Math.random() < traitRate) {
200
+ const scale = base[key] * 0.1;
201
+ this.traits[key] = clamp(
202
+ this.traits[key] + randomGaussian(0, scale),
203
+ TRAIT_BOUNDS[key].min,
204
+ TRAIT_BOUNDS[key].max,
205
+ );
206
+ }
207
+ }
208
+
209
+ for (let i = 0; i < this.neuralWeights.length; i++) {
210
+ if (Math.random() < weightRate) {
211
+ this.neuralWeights[i] += randomGaussian(0, magnitude);
212
+ }
213
+ }
214
+
215
+ if (Math.random() < 0.05) {
216
+ this.signalPreference = Math.floor(Math.random() * 8);
217
+ }
218
+ }
219
+
220
+ /** Serialize to a plain object (for LocalStorage) */
221
+ toJSON() {
222
+ return {
223
+ type: this.type,
224
+ traits: { ...this.traits },
225
+ neuralWeights: this.neuralWeights,
226
+ signalPreference: this.signalPreference,
227
+ };
228
+ }
229
+
230
+ /** Deserialize from a plain object */
231
+ static fromJSON(data) {
232
+ const g = new Genome(data.type, data.traits, data.neuralWeights);
233
+ g.signalPreference = data.signalPreference || 0;
234
+ return g;
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Create a new random genome of a given type.
240
+ * @param {string} type - Archetype key
241
+ * @returns {Genome}
242
+ */
243
+ export function createGenome(type) {
244
+ return new Genome(type);
245
+ }
246
+
247
+ /**
248
+ * Tournament selection: pick the fittest from a random subset.
249
+ * @param {Array<{genome: Genome, fitness: number}>} population
250
+ * @param {number} tournamentSize
251
+ * @returns {Genome}
252
+ */
253
+ export function tournamentSelect(population, tournamentSize = 3) {
254
+ let best = null;
255
+ let bestFitness = -Infinity;
256
+ for (let i = 0; i < tournamentSize; i++) {
257
+ const idx = Math.floor(Math.random() * population.length);
258
+ const candidate = population[idx];
259
+ if (candidate.fitness > bestFitness) {
260
+ best = candidate;
261
+ bestFitness = candidate.fitness;
262
+ }
263
+ }
264
+ return best.genome;
265
+ }
266
+
267
+ /**
268
+ * Calculate fitness score for an entity's lifetime stats.
269
+ * @param {string} type - Entity type
270
+ * @param {object} stats - { survivalTime, energyGathered, offspring, kills }
271
+ * @returns {number}
272
+ */
273
+ export function calculateFitness(type, stats) {
274
+ const w = ENTITY_TYPES[type].fitnessWeights;
275
+ return (
276
+ w.survival * stats.survivalTime +
277
+ w.energy * stats.energyGathered +
278
+ w.offspring * stats.offspring * 10 +
279
+ w.kills * stats.kills * 5
280
+ );
281
+ }
282
+
283
+ /**
284
+ * Measure genetic diversity as average pairwise trait distance.
285
+ * @param {Genome[]} genomes
286
+ * @returns {number} 0-1 normalized diversity score
287
+ */
288
+ export function measureDiversity(genomes) {
289
+ if (genomes.length < 2) return 0;
290
+
291
+ const traitKeys = Object.keys(TRAIT_BOUNDS);
292
+ let totalDist = 0;
293
+ let pairs = 0;
294
+
295
+ const sampleSize = Math.min(genomes.length, 30);
296
+ for (let i = 0; i < sampleSize; i++) {
297
+ for (let j = i + 1; j < sampleSize; j++) {
298
+ let dist = 0;
299
+ for (const key of traitKeys) {
300
+ const range = TRAIT_BOUNDS[key].max - TRAIT_BOUNDS[key].min;
301
+ const diff =
302
+ Math.abs(genomes[i].traits[key] - genomes[j].traits[key]) / range;
303
+ dist += diff * diff;
304
+ }
305
+ totalDist += Math.sqrt(dist / traitKeys.length);
306
+ pairs++;
307
+ }
308
+ }
309
+
310
+ return pairs > 0 ? totalDist / pairs : 0;
311
+ }
312
+
313
+ /**
314
+ * Produce a child genome from the population using selection + crossover + mutation.
315
+ * @param {Array<{genome: Genome, fitness: number}>} population
316
+ * @returns {Genome}
317
+ */
318
+ export function reproduce(population) {
319
+ const parent1 = tournamentSelect(population, 3);
320
+ const parent2 = tournamentSelect(population, 3);
321
+ const child = parent1.crossover(parent2);
322
+ child.mutate(0.25, 0.05, 0.2);
323
+ return child;
324
+ }
js/neuralNetwork.js ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Lightweight feedforward neural network for entity decision-making.
3
+ * Supports arbitrary topology, weight serialization, mutation, and
4
+ * activation visualization for the HUD neural activity overlay.
5
+ */
6
+ export class NeuralNetwork {
7
+ /**
8
+ * @param {number[]} topology - Layer sizes, e.g. [12, 10, 6]
9
+ */
10
+ constructor(topology) {
11
+ this.topology = topology;
12
+ this.weights = [];
13
+ this.biases = [];
14
+ this.activations = [];
15
+ this._initWeights();
16
+ }
17
+
18
+ /** Xavier-style initialization for stable gradient flow */
19
+ _initWeights() {
20
+ for (let i = 1; i < this.topology.length; i++) {
21
+ const fanIn = this.topology[i - 1];
22
+ const fanOut = this.topology[i];
23
+ const scale = Math.sqrt(2 / (fanIn + fanOut));
24
+ const layerW = new Array(fanOut);
25
+ const layerB = new Array(fanOut);
26
+ for (let j = 0; j < fanOut; j++) {
27
+ layerW[j] = new Array(fanIn);
28
+ for (let k = 0; k < fanIn; k++) {
29
+ layerW[j][k] = (Math.random() * 2 - 1) * scale;
30
+ }
31
+ layerB[j] = (Math.random() * 2 - 1) * 0.1;
32
+ }
33
+ this.weights.push(layerW);
34
+ this.biases.push(layerB);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Run a forward pass through the network.
40
+ * @param {number[]} inputs - Input activations
41
+ * @returns {number[]} Output activations
42
+ */
43
+ forward(inputs) {
44
+ let current = inputs;
45
+ this.activations = [inputs.slice()];
46
+
47
+ for (let layer = 0; layer < this.weights.length; layer++) {
48
+ const w = this.weights[layer];
49
+ const b = this.biases[layer];
50
+ const numNeurons = w.length;
51
+ const next = new Array(numNeurons);
52
+ const isOutput = layer === this.weights.length - 1;
53
+
54
+ for (let j = 0; j < numNeurons; j++) {
55
+ let sum = b[j];
56
+ const wj = w[j];
57
+ for (let k = 0; k < current.length; k++) {
58
+ sum += wj[k] * current[k];
59
+ }
60
+ next[j] = Math.tanh(sum);
61
+ }
62
+
63
+ current = next;
64
+ this.activations.push(current.slice());
65
+ }
66
+
67
+ return current;
68
+ }
69
+
70
+ /** Count total trainable parameters */
71
+ get totalWeights() {
72
+ let count = 0;
73
+ for (let i = 0; i < this.weights.length; i++) {
74
+ count += this.weights[i].length * this.weights[i][0].length;
75
+ count += this.biases[i].length;
76
+ }
77
+ return count;
78
+ }
79
+
80
+ /** Flatten all weights + biases into a single array */
81
+ serialize() {
82
+ const flat = new Array(this.totalWeights);
83
+ let idx = 0;
84
+ for (let i = 0; i < this.weights.length; i++) {
85
+ const w = this.weights[i];
86
+ for (let j = 0; j < w.length; j++) {
87
+ for (let k = 0; k < w[j].length; k++) {
88
+ flat[idx++] = w[j][k];
89
+ }
90
+ }
91
+ const b = this.biases[i];
92
+ for (let j = 0; j < b.length; j++) {
93
+ flat[idx++] = b[j];
94
+ }
95
+ }
96
+ return flat;
97
+ }
98
+
99
+ /** Restore weights from a flat array */
100
+ deserialize(flat) {
101
+ let idx = 0;
102
+ for (let i = 0; i < this.weights.length; i++) {
103
+ const w = this.weights[i];
104
+ for (let j = 0; j < w.length; j++) {
105
+ for (let k = 0; k < w[j].length; k++) {
106
+ w[j][k] = flat[idx++];
107
+ }
108
+ }
109
+ const b = this.biases[i];
110
+ for (let j = 0; j < b.length; j++) {
111
+ b[j] = flat[idx++];
112
+ }
113
+ }
114
+ }
115
+
116
+ /** Create an exact copy of this network */
117
+ clone() {
118
+ const nn = new NeuralNetwork(this.topology.slice());
119
+ nn.deserialize(this.serialize());
120
+ return nn;
121
+ }
122
+
123
+ /**
124
+ * Apply Gaussian mutations to a fraction of weights.
125
+ * @param {number} rate - Probability of mutating each weight (0-1)
126
+ * @param {number} magnitude - Standard deviation of mutation noise
127
+ */
128
+ mutate(rate, magnitude) {
129
+ for (let i = 0; i < this.weights.length; i++) {
130
+ const w = this.weights[i];
131
+ for (let j = 0; j < w.length; j++) {
132
+ for (let k = 0; k < w[j].length; k++) {
133
+ if (Math.random() < rate) {
134
+ w[j][k] += gaussianNoise() * magnitude;
135
+ }
136
+ }
137
+ }
138
+ const b = this.biases[i];
139
+ for (let j = 0; j < b.length; j++) {
140
+ if (Math.random() < rate) {
141
+ b[j] += gaussianNoise() * magnitude;
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Uniform crossover between two networks of the same topology.
149
+ * @param {NeuralNetwork} other
150
+ * @returns {NeuralNetwork} child network
151
+ */
152
+ crossover(other) {
153
+ const child = new NeuralNetwork(this.topology.slice());
154
+ const w1 = this.serialize();
155
+ const w2 = other.serialize();
156
+ const childW = new Array(w1.length);
157
+
158
+ const crossPoint = Math.floor(Math.random() * w1.length);
159
+ for (let i = 0; i < w1.length; i++) {
160
+ childW[i] = i < crossPoint ? w1[i] : w2[i];
161
+ }
162
+ child.deserialize(childW);
163
+ return child;
164
+ }
165
+
166
+ /**
167
+ * Get the maximum absolute activation across all layers.
168
+ * Useful for visualization intensity scaling.
169
+ */
170
+ getMaxActivation() {
171
+ let max = 0;
172
+ for (const layer of this.activations) {
173
+ for (const v of layer) {
174
+ const abs = Math.abs(v);
175
+ if (abs > max) max = abs;
176
+ }
177
+ }
178
+ return max;
179
+ }
180
+
181
+ /**
182
+ * Render a mini neural network diagram to a canvas context.
183
+ * @param {CanvasRenderingContext2D} ctx
184
+ * @param {number} x - Top-left x
185
+ * @param {number} y - Top-left y
186
+ * @param {number} w - Width
187
+ * @param {number} h - Height
188
+ */
189
+ renderMini(ctx, x, y, w, h) {
190
+ const layers = this.topology.length;
191
+ const layerSpacing = w / (layers - 1);
192
+
193
+ ctx.lineWidth = 0.5;
194
+ for (let l = 0; l < this.weights.length; l++) {
195
+ const fromCount = this.topology[l];
196
+ const toCount = this.topology[l + 1];
197
+ const fromX = x + l * layerSpacing;
198
+ const toX = x + (l + 1) * layerSpacing;
199
+
200
+ for (let j = 0; j < toCount; j++) {
201
+ const toY = y + (j + 0.5) * (h / toCount);
202
+ for (let k = 0; k < fromCount; k++) {
203
+ const fromY = y + (k + 0.5) * (h / fromCount);
204
+ const weight = this.weights[l][j][k];
205
+ const alpha = Math.min(Math.abs(weight) * 0.5, 0.6);
206
+ ctx.strokeStyle =
207
+ weight > 0
208
+ ? `rgba(0, 240, 255, ${alpha})`
209
+ : `rgba(255, 51, 102, ${alpha})`;
210
+ ctx.beginPath();
211
+ ctx.moveTo(fromX, fromY);
212
+ ctx.lineTo(toX, toY);
213
+ ctx.stroke();
214
+ }
215
+ }
216
+ }
217
+
218
+ for (let l = 0; l < layers; l++) {
219
+ const count = this.topology[l];
220
+ const lx = x + l * layerSpacing;
221
+ const act = this.activations[l] || [];
222
+
223
+ for (let n = 0; n < count; n++) {
224
+ const ny = y + (n + 0.5) * (h / count);
225
+ const val = act[n] || 0;
226
+ const intensity = Math.abs(val);
227
+ const r = Math.max(2, 4 - layers * 0.3);
228
+
229
+ ctx.beginPath();
230
+ ctx.arc(lx, ny, r, 0, Math.PI * 2);
231
+ ctx.fillStyle =
232
+ val > 0
233
+ ? `rgba(0, 240, 255, ${0.3 + intensity * 0.7})`
234
+ : `rgba(255, 51, 102, ${0.3 + intensity * 0.7})`;
235
+ ctx.fill();
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ /** Gaussian noise using Box-Muller transform */
242
+ function gaussianNoise() {
243
+ let u = 0,
244
+ v = 0;
245
+ while (u === 0) u = Math.random();
246
+ while (v === 0) v = Math.random();
247
+ return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
248
+ }
249
+
250
+ /**
251
+ * QuantumDecisionLayer - probabilistic branching for elite entities.
252
+ * Samples from a probability distribution over actions rather than
253
+ * taking the deterministic argmax.
254
+ */
255
+ export class QuantumDecisionLayer {
256
+ /**
257
+ * @param {number} temperature - Controls exploration vs exploitation.
258
+ * Higher = more random, lower = more greedy.
259
+ */
260
+ constructor(temperature = 1.0) {
261
+ this.temperature = temperature;
262
+ }
263
+
264
+ /**
265
+ * Apply softmax with temperature to raw outputs and sample.
266
+ * @param {number[]} logits - Raw network outputs
267
+ * @returns {{ action: number, probabilities: number[] }}
268
+ */
269
+ sample(logits) {
270
+ const t = this.temperature;
271
+ const maxLogit = Math.max(...logits);
272
+ const exps = logits.map((l) => Math.exp((l - maxLogit) / t));
273
+ const sum = exps.reduce((a, b) => a + b, 0);
274
+ const probs = exps.map((e) => e / sum);
275
+
276
+ const r = Math.random();
277
+ let cumulative = 0;
278
+ for (let i = 0; i < probs.length; i++) {
279
+ cumulative += probs[i];
280
+ if (r <= cumulative) {
281
+ return { action: i, probabilities: probs };
282
+ }
283
+ }
284
+ return { action: probs.length - 1, probabilities: probs };
285
+ }
286
+ }
js/particles.js ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ObjectPool, TAU, randomRange, lerp } from "./utils.js";
2
+
3
+ class Particle {
4
+ constructor() {
5
+ this.reset();
6
+ }
7
+
8
+ reset() {
9
+ this.x = 0;
10
+ this.y = 0;
11
+ this.vx = 0;
12
+ this.vy = 0;
13
+ this.life = 0;
14
+ this.maxLife = 1;
15
+ this.size = 2;
16
+ this.r = 255;
17
+ this.g = 255;
18
+ this.b = 255;
19
+ this.alpha = 1;
20
+ this.decay = 1;
21
+ this.friction = 0.98;
22
+ this.gravity = 0;
23
+ this.type = "circle";
24
+ }
25
+ }
26
+
27
+ const PRESETS = {
28
+ eat: {
29
+ count: 6,
30
+ speed: [20, 50],
31
+ life: [0.3, 0.6],
32
+ size: [1.5, 3],
33
+ color: [0, 255, 170],
34
+ friction: 0.92,
35
+ type: "circle",
36
+ },
37
+ attack: {
38
+ count: 10,
39
+ speed: [40, 80],
40
+ life: [0.2, 0.4],
41
+ size: [1.5, 3.5],
42
+ color: [255, 51, 102],
43
+ friction: 0.9,
44
+ type: "spark",
45
+ },
46
+ reproduce: {
47
+ count: 14,
48
+ speed: [30, 70],
49
+ life: [0.5, 1.0],
50
+ size: [2, 5],
51
+ color: [204, 102, 255],
52
+ friction: 0.94,
53
+ type: "circle",
54
+ },
55
+ death: {
56
+ count: 20,
57
+ speed: [20, 60],
58
+ life: [0.6, 1.2],
59
+ size: [2, 5],
60
+ color: [255, 100, 100],
61
+ friction: 0.96,
62
+ gravity: 15,
63
+ type: "spark",
64
+ },
65
+ energy: {
66
+ count: 4,
67
+ speed: [10, 30],
68
+ life: [0.4, 0.8],
69
+ size: [2, 4],
70
+ color: [255, 238, 68],
71
+ friction: 0.95,
72
+ type: "circle",
73
+ },
74
+ signal: {
75
+ count: 3,
76
+ speed: [5, 15],
77
+ life: [0.5, 0.8],
78
+ size: [1.5, 3],
79
+ color: [100, 200, 255],
80
+ friction: 0.97,
81
+ type: "circle",
82
+ },
83
+ storm: {
84
+ count: 1,
85
+ speed: [30, 80],
86
+ life: [0.8, 1.5],
87
+ size: [1, 2.5],
88
+ color: [255, 238, 68],
89
+ friction: 0.99,
90
+ gravity: 40,
91
+ type: "spark",
92
+ },
93
+ bloom: {
94
+ count: 1,
95
+ speed: [10, 40],
96
+ life: [0.6, 1.2],
97
+ size: [2, 4],
98
+ color: [0, 255, 170],
99
+ friction: 0.96,
100
+ gravity: -5,
101
+ type: "circle",
102
+ },
103
+ };
104
+
105
+ export class ParticleSystem {
106
+ constructor(maxParticles = 2000) {
107
+ this.maxParticles = maxParticles;
108
+ this.active = [];
109
+ this._pool = new ObjectPool(
110
+ () => new Particle(),
111
+ (p) => p.reset(),
112
+ maxParticles,
113
+ );
114
+ }
115
+
116
+ emit(x, y, preset, colorOverride = null) {
117
+ const config = typeof preset === "string" ? PRESETS[preset] : preset;
118
+ if (!config) return;
119
+
120
+ const count = config.count || 5;
121
+ for (let i = 0; i < count; i++) {
122
+ if (this.active.length >= this.maxParticles) {
123
+ const old = this.active.shift();
124
+ this._pool.release(old);
125
+ }
126
+
127
+ const p = this._pool.acquire();
128
+ const angle = Math.random() * TAU;
129
+ const speed = randomRange(config.speed[0], config.speed[1]);
130
+
131
+ p.x = x;
132
+ p.y = y;
133
+ p.vx = Math.cos(angle) * speed;
134
+ p.vy = Math.sin(angle) * speed;
135
+ p.maxLife = randomRange(config.life[0], config.life[1]);
136
+ p.life = p.maxLife;
137
+ p.size = randomRange(config.size[0], config.size[1]);
138
+ p.friction = config.friction || 0.95;
139
+ p.gravity = config.gravity || 0;
140
+ p.type = config.type || "circle";
141
+
142
+ if (colorOverride) {
143
+ p.r = colorOverride[0];
144
+ p.g = colorOverride[1];
145
+ p.b = colorOverride[2];
146
+ } else {
147
+ p.r = config.color[0];
148
+ p.g = config.color[1];
149
+ p.b = config.color[2];
150
+ }
151
+
152
+ this.active.push(p);
153
+ }
154
+ }
155
+
156
+ emitTrail(x, y, r, g, b, size = 1.5) {
157
+ if (this.active.length >= this.maxParticles) {
158
+ const old = this.active.shift();
159
+ this._pool.release(old);
160
+ }
161
+
162
+ const p = this._pool.acquire();
163
+ p.x = x;
164
+ p.y = y;
165
+ p.vx = 0;
166
+ p.vy = 0;
167
+ p.maxLife = 0.4;
168
+ p.life = 0.4;
169
+ p.size = size;
170
+ p.r = r;
171
+ p.g = g;
172
+ p.b = b;
173
+ p.friction = 1.0;
174
+ p.type = "trail";
175
+ this.active.push(p);
176
+ }
177
+
178
+ update(dt) {
179
+ for (let i = this.active.length - 1; i >= 0; i--) {
180
+ const p = this.active[i];
181
+ p.life -= dt;
182
+
183
+ if (p.life <= 0) {
184
+ this.active.splice(i, 1);
185
+ this._pool.release(p);
186
+ continue;
187
+ }
188
+
189
+ p.vy += p.gravity * dt;
190
+ p.vx *= p.friction;
191
+ p.vy *= p.friction;
192
+ p.x += p.vx * dt;
193
+ p.y += p.vy * dt;
194
+ }
195
+ }
196
+
197
+ render(ctx) {
198
+ if (this.active.length === 0) return;
199
+
200
+ ctx.save();
201
+
202
+ for (const p of this.active) {
203
+ const t = p.life / p.maxLife;
204
+ const alpha = t * 0.8;
205
+ const size = p.size * (0.3 + 0.7 * t);
206
+
207
+ switch (p.type) {
208
+ case "circle":
209
+ ctx.fillStyle = `rgba(${p.r},${p.g},${p.b},${alpha * 0.3})`;
210
+ ctx.beginPath();
211
+ ctx.arc(p.x, p.y, size * 2, 0, TAU);
212
+ ctx.fill();
213
+ ctx.fillStyle = `rgba(${p.r},${p.g},${p.b},${alpha})`;
214
+ ctx.beginPath();
215
+ ctx.arc(p.x, p.y, size, 0, TAU);
216
+ ctx.fill();
217
+ break;
218
+
219
+ case "spark": {
220
+ const len = Math.sqrt(p.vx * p.vx + p.vy * p.vy) * 0.05 + 2;
221
+ const angle = Math.atan2(p.vy, p.vx);
222
+ ctx.strokeStyle = `rgba(${p.r},${p.g},${p.b},${alpha})`;
223
+ ctx.lineWidth = size * 0.6;
224
+ ctx.beginPath();
225
+ ctx.moveTo(p.x - Math.cos(angle) * len, p.y - Math.sin(angle) * len);
226
+ ctx.lineTo(
227
+ p.x + Math.cos(angle) * len * 0.3,
228
+ p.y + Math.sin(angle) * len * 0.3,
229
+ );
230
+ ctx.stroke();
231
+ break;
232
+ }
233
+
234
+ case "trail":
235
+ ctx.fillStyle = `rgba(${p.r},${p.g},${p.b},${alpha * 0.4})`;
236
+ ctx.beginPath();
237
+ ctx.arc(p.x, p.y, size, 0, TAU);
238
+ ctx.fill();
239
+ break;
240
+ }
241
+ }
242
+
243
+ ctx.shadowBlur = 0;
244
+ ctx.restore();
245
+ }
246
+
247
+ emitWeather(event, dt) {
248
+ const preset = event.type === "storm" ? "storm" : "bloom";
249
+ const chance = event.type === "storm" ? 0.6 : 0.3;
250
+ if (Math.random() < chance * dt * 10) {
251
+ const angle = Math.random() * TAU;
252
+ const dist = Math.random() * event.radius;
253
+ const x = event.x + Math.cos(angle) * dist;
254
+ const y = event.y + Math.sin(angle) * dist;
255
+ this.emit(x, y, preset);
256
+ }
257
+ }
258
+
259
+ get count() {
260
+ return this.active.length;
261
+ }
262
+
263
+ clear() {
264
+ for (const p of this.active) {
265
+ this._pool.release(p);
266
+ }
267
+ this.active.length = 0;
268
+ }
269
+ }
js/stats.js ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { RingBuffer, COLORS, EMA } from "./utils.js";
2
+ import { ENTITY_TYPES, measureDiversity } from "./genetics.js";
3
+
4
+ const HISTORY_LENGTH = 200;
5
+
6
+ export class StatsTracker {
7
+ constructor() {
8
+ this.generationCount = 0;
9
+ this.totalBirths = 0;
10
+ this.totalDeaths = 0;
11
+ this.totalTime = 0;
12
+ this.peakPopulation = 0;
13
+
14
+ this.popHistory = {};
15
+ this.totalPopHistory = new RingBuffer(HISTORY_LENGTH);
16
+ for (const type of Object.keys(ENTITY_TYPES)) {
17
+ this.popHistory[type] = new RingBuffer(HISTORY_LENGTH);
18
+ }
19
+
20
+ this.avgFitnessHistory = new RingBuffer(HISTORY_LENGTH);
21
+ this.maxFitnessHistory = new RingBuffer(HISTORY_LENGTH);
22
+
23
+ this.diversityHistory = new RingBuffer(HISTORY_LENGTH);
24
+
25
+ this.fpsEMA = new EMA(0.1);
26
+ this.entityCountEMA = new EMA(0.2);
27
+
28
+ this.current = {
29
+ population: {},
30
+ totalPop: 0,
31
+ avgFitness: 0,
32
+ maxFitness: 0,
33
+ diversity: 0,
34
+ fps: 60,
35
+ entityCount: 0,
36
+ particleCount: 0,
37
+ };
38
+
39
+ this.heatmapCols = 40;
40
+ this.heatmapRows = 30;
41
+ this.heatmap = new Float32Array(this.heatmapCols * this.heatmapRows);
42
+
43
+ this._recordTimer = 0;
44
+ this._recordInterval = 0.5;
45
+ }
46
+
47
+ record(entities, world, dt, fps, particleCount) {
48
+ this.totalTime += dt;
49
+ this._recordTimer += dt;
50
+
51
+ this.current.fps = this.fpsEMA.update(fps);
52
+ this.current.entityCount = entities.length;
53
+ this.current.particleCount = particleCount;
54
+
55
+ if (this._recordTimer < this._recordInterval) return;
56
+ this._recordTimer = 0;
57
+
58
+ const popCounts = {};
59
+ for (const type of Object.keys(ENTITY_TYPES)) {
60
+ popCounts[type] = 0;
61
+ }
62
+ let totalFitness = 0;
63
+ let maxFitness = 0;
64
+ const genomes = [];
65
+
66
+ this.heatmap.fill(0);
67
+ const cellW = world.width / this.heatmapCols;
68
+ const cellH = world.height / this.heatmapRows;
69
+
70
+ for (const e of entities) {
71
+ popCounts[e.type] = (popCounts[e.type] || 0) + 1;
72
+ totalFitness += e.fitness;
73
+ if (e.fitness > maxFitness) maxFitness = e.fitness;
74
+ genomes.push(e.genome);
75
+
76
+ const col = Math.floor(e.x / cellW);
77
+ const row = Math.floor(e.y / cellH);
78
+ if (
79
+ col >= 0 &&
80
+ col < this.heatmapCols &&
81
+ row >= 0 &&
82
+ row < this.heatmapRows
83
+ ) {
84
+ this.heatmap[row * this.heatmapCols + col] += 1;
85
+ }
86
+ }
87
+
88
+ const totalPop = entities.length;
89
+ this.current.population = popCounts;
90
+ this.current.totalPop = totalPop;
91
+ this.current.avgFitness = totalPop > 0 ? totalFitness / totalPop : 0;
92
+ this.current.maxFitness = maxFitness;
93
+ this.current.diversity = measureDiversity(genomes);
94
+
95
+ if (totalPop > this.peakPopulation) this.peakPopulation = totalPop;
96
+
97
+ this.totalPopHistory.push(totalPop);
98
+ for (const type of Object.keys(ENTITY_TYPES)) {
99
+ this.popHistory[type].push(popCounts[type] || 0);
100
+ }
101
+ this.avgFitnessHistory.push(this.current.avgFitness);
102
+ this.maxFitnessHistory.push(maxFitness);
103
+ this.diversityHistory.push(this.current.diversity);
104
+ }
105
+
106
+ onBirth() {
107
+ this.totalBirths++;
108
+ }
109
+ onDeath() {
110
+ this.totalDeaths++;
111
+ }
112
+ onGeneration() {
113
+ this.generationCount++;
114
+ }
115
+
116
+ renderSparkline(ctx, buffer, x, y, w, h, color) {
117
+ const data = buffer.toArray();
118
+ if (data.length < 2) return;
119
+
120
+ let max = -Infinity,
121
+ min = Infinity;
122
+ for (const v of data) {
123
+ if (v > max) max = v;
124
+ if (v < min) min = v;
125
+ }
126
+ const range = max - min || 1;
127
+
128
+ ctx.beginPath();
129
+ ctx.strokeStyle = color;
130
+ ctx.lineWidth = 1.5;
131
+ ctx.lineJoin = "round";
132
+
133
+ for (let i = 0; i < data.length; i++) {
134
+ const px = x + (i / (data.length - 1)) * w;
135
+ const py = y + h - ((data[i] - min) / range) * h;
136
+ if (i === 0) ctx.moveTo(px, py);
137
+ else ctx.lineTo(px, py);
138
+ }
139
+ ctx.stroke();
140
+
141
+ ctx.lineTo(x + w, y + h);
142
+ ctx.lineTo(x, y + h);
143
+ ctx.closePath();
144
+ const grad = ctx.createLinearGradient(x, y, x, y + h);
145
+ const rgbMatch = color.match(/(\d+),\s*(\d+),\s*(\d+)/);
146
+ if (rgbMatch) {
147
+ grad.addColorStop(
148
+ 0,
149
+ `rgba(${rgbMatch[1]},${rgbMatch[2]},${rgbMatch[3]},0.15)`,
150
+ );
151
+ grad.addColorStop(
152
+ 1,
153
+ `rgba(${rgbMatch[1]},${rgbMatch[2]},${rgbMatch[3]},0.0)`,
154
+ );
155
+ }
156
+ ctx.fillStyle = grad;
157
+ ctx.fill();
158
+ }
159
+
160
+ renderCharts(ctx, x, y, w, h) {
161
+ const chartH = h / 3 - 8;
162
+ const pad = 4;
163
+
164
+ ctx.fillStyle = "rgba(100, 200, 255, 0.4)";
165
+ ctx.font = "9px Courier New";
166
+ ctx.fillText("POPULATION", x + 2, y + 8);
167
+ this.renderSparkline(
168
+ ctx,
169
+ this.totalPopHistory,
170
+ x,
171
+ y + 12,
172
+ w,
173
+ chartH,
174
+ "rgb(0, 240, 255)",
175
+ );
176
+
177
+ const types = Object.keys(ENTITY_TYPES);
178
+ for (const type of types) {
179
+ const c = COLORS[type];
180
+ if (c) {
181
+ this.renderSparkline(
182
+ ctx,
183
+ this.popHistory[type],
184
+ x,
185
+ y + 12,
186
+ w,
187
+ chartH,
188
+ `rgb(${c.r},${c.g},${c.b})`,
189
+ );
190
+ }
191
+ }
192
+
193
+ const fy = y + chartH + 20;
194
+ ctx.fillStyle = "rgba(100, 200, 255, 0.4)";
195
+ ctx.fillText("FITNESS", x + 2, fy + 8);
196
+ this.renderSparkline(
197
+ ctx,
198
+ this.avgFitnessHistory,
199
+ x,
200
+ fy + 12,
201
+ w,
202
+ chartH,
203
+ "rgb(0, 255, 136)",
204
+ );
205
+ this.renderSparkline(
206
+ ctx,
207
+ this.maxFitnessHistory,
208
+ x,
209
+ fy + 12,
210
+ w,
211
+ chartH,
212
+ "rgb(255, 170, 0)",
213
+ );
214
+
215
+ const dy = fy + chartH + 20;
216
+ ctx.fillStyle = "rgba(100, 200, 255, 0.4)";
217
+ ctx.fillText("DIVERSITY", x + 2, dy + 8);
218
+ this.renderSparkline(
219
+ ctx,
220
+ this.diversityHistory,
221
+ x,
222
+ dy + 12,
223
+ w,
224
+ chartH,
225
+ "rgb(204, 102, 255)",
226
+ );
227
+ }
228
+
229
+ renderHeatmap(ctx, worldW, worldH) {
230
+ const cellW = worldW / this.heatmapCols;
231
+ const cellH = worldH / this.heatmapRows;
232
+
233
+ let maxDensity = 0;
234
+ for (let i = 0; i < this.heatmap.length; i++) {
235
+ if (this.heatmap[i] > maxDensity) maxDensity = this.heatmap[i];
236
+ }
237
+ if (maxDensity === 0) return;
238
+
239
+ for (let row = 0; row < this.heatmapRows; row++) {
240
+ for (let col = 0; col < this.heatmapCols; col++) {
241
+ const density = this.heatmap[row * this.heatmapCols + col];
242
+ if (density === 0) continue;
243
+ const intensity = density / maxDensity;
244
+ const alpha = intensity * 0.06;
245
+ ctx.fillStyle = `rgba(0, 240, 255, ${alpha})`;
246
+ ctx.fillRect(col * cellW, row * cellH, cellW + 1, cellH + 1);
247
+ }
248
+ }
249
+ }
250
+
251
+ toJSON() {
252
+ return {
253
+ generationCount: this.generationCount,
254
+ totalBirths: this.totalBirths,
255
+ totalDeaths: this.totalDeaths,
256
+ totalTime: this.totalTime,
257
+ peakPopulation: this.peakPopulation,
258
+ };
259
+ }
260
+
261
+ static fromJSON(data) {
262
+ const s = new StatsTracker();
263
+ s.generationCount = data.generationCount || 0;
264
+ s.totalBirths = data.totalBirths || 0;
265
+ s.totalDeaths = data.totalDeaths || 0;
266
+ s.totalTime = data.totalTime || 0;
267
+ s.peakPopulation = data.peakPopulation || 0;
268
+ return s;
269
+ }
270
+ }
js/ui.js ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ENTITY_TYPES } from "./genetics.js";
2
+ import { EVENT_TYPES } from "./world.js";
3
+ import { COLORS, formatNumber } from "./utils.js";
4
+
5
+ const MAX_EVENTS = 30;
6
+
7
+ export class UI {
8
+ constructor(container) {
9
+ this.container = container;
10
+ this.events = [];
11
+ this._announcement = null;
12
+ this._announcementTimeout = null;
13
+ this._elements = {};
14
+ this._chartCanvas = null;
15
+ this._chartCtx = null;
16
+ this._built = false;
17
+ }
18
+
19
+ init() {
20
+ if (this._built) return;
21
+ this._built = true;
22
+
23
+ this._buildPanel(
24
+ "panel-generation",
25
+ `
26
+ <div class="panel-title">GENERATION</div>
27
+ <div class="stat-value neon-text-cyan" id="gen-count" aria-live="polite">0</div>
28
+ <div class="stat-label" id="gen-time">0:00</div>
29
+ `,
30
+ );
31
+
32
+ this._buildPanel(
33
+ "panel-stats",
34
+ `
35
+ <div class="panel-title"><span class="live-indicator" aria-hidden="true"></span>POPULATION</div>
36
+ <div id="pop-total" class="stat-value" style="margin-bottom:6px">0</div>
37
+ <div id="pop-breakdown" role="list" aria-label="Population by type"></div>
38
+ <hr class="neon-divider">
39
+ <div class="stat-row">
40
+ <span class="stat-label">Births</span>
41
+ <span class="count" id="stat-births">0</span>
42
+ </div>
43
+ <div class="stat-row">
44
+ <span class="stat-label">Deaths</span>
45
+ <span class="count" id="stat-deaths">0</span>
46
+ </div>
47
+ <div class="stat-row">
48
+ <span class="stat-label">Peak</span>
49
+ <span class="count" id="stat-peak">0</span>
50
+ </div>
51
+ <div class="stat-row">
52
+ <span class="stat-label">Diversity</span>
53
+ <span class="count" id="stat-diversity">0%</span>
54
+ </div>
55
+ `,
56
+ );
57
+
58
+ this._buildPanel(
59
+ "panel-performance",
60
+ `
61
+ <div class="panel-title">SYSTEM</div>
62
+ <div class="stat-row">
63
+ <span class="stat-label">FPS</span>
64
+ <span class="count neon-text-green" id="perf-fps">60</span>
65
+ </div>
66
+ <div class="stat-row">
67
+ <span class="stat-label">Entities</span>
68
+ <span class="count" id="perf-entities">0</span>
69
+ </div>
70
+ <div class="stat-row">
71
+ <span class="stat-label">Particles</span>
72
+ <span class="count" id="perf-particles">0</span>
73
+ </div>
74
+ `,
75
+ );
76
+
77
+ this._buildPanel(
78
+ "panel-events",
79
+ `
80
+ <div class="panel-title">EVENT LOG</div>
81
+ <div id="event-list" role="log" aria-label="Simulation events" aria-live="polite"></div>
82
+ `,
83
+ );
84
+
85
+ this._buildPanel(
86
+ "panel-charts",
87
+ `
88
+ <canvas id="chart-canvas" class="chart-canvas" aria-label="Population and fitness charts" role="img"></canvas>
89
+ `,
90
+ );
91
+
92
+ this._buildPopRows();
93
+
94
+ const ann = document.createElement("div");
95
+ ann.className = "announcement";
96
+ ann.id = "announcement";
97
+ ann.setAttribute("role", "alert");
98
+ ann.setAttribute("aria-live", "assertive");
99
+ this.container.appendChild(ann);
100
+ this._announcement = ann;
101
+
102
+ this._chartCanvas = document.getElementById("chart-canvas");
103
+ if (this._chartCanvas) {
104
+ this._resizeChartCanvas();
105
+ this._chartCtx = this._chartCanvas.getContext("2d");
106
+ }
107
+
108
+ const srSummary = document.createElement("div");
109
+ srSummary.className = "sr-only";
110
+ srSummary.id = "sr-summary";
111
+ srSummary.setAttribute("aria-live", "polite");
112
+ srSummary.setAttribute("aria-atomic", "true");
113
+ this.container.appendChild(srSummary);
114
+ this._srSummary = srSummary;
115
+ this._srTimer = 0;
116
+
117
+ window.addEventListener("resize", () => this._resizeChartCanvas());
118
+ }
119
+
120
+ _buildPanel(id, html) {
121
+ const panel = document.createElement("div");
122
+ panel.id = id;
123
+ panel.className = "hud-panel glass-panel";
124
+ panel.innerHTML = html;
125
+ panel.setAttribute("tabindex", "0");
126
+ this.container.appendChild(panel);
127
+ return panel;
128
+ }
129
+
130
+ _buildPopRows() {
131
+ const container = document.getElementById("pop-breakdown");
132
+ if (!container) return;
133
+
134
+ for (const [type, info] of Object.entries(ENTITY_TYPES)) {
135
+ const row = document.createElement("div");
136
+ row.className = "stat-row";
137
+ row.setAttribute("role", "listitem");
138
+ row.innerHTML = `
139
+ <span>
140
+ <span class="dot dot-${type}" aria-hidden="true"></span>
141
+ <span class="stat-label">${info.name}</span>
142
+ </span>
143
+ <span class="count type-${type}" id="pop-${type}">0</span>
144
+ `;
145
+ container.appendChild(row);
146
+ }
147
+ }
148
+
149
+ _resizeChartCanvas() {
150
+ const canvas = this._chartCanvas;
151
+ if (!canvas) return;
152
+ const panel = canvas.parentElement;
153
+ const rect = panel.getBoundingClientRect();
154
+ canvas.width = rect.width - 32;
155
+ canvas.height = rect.height - 32;
156
+ this._chartCtx = canvas.getContext("2d");
157
+ }
158
+
159
+ update(stats, world, entityManager, dt) {
160
+ const c = stats.current;
161
+
162
+ this._setText("gen-count", stats.generationCount.toString());
163
+ this._setText("gen-time", this._formatTime(stats.totalTime));
164
+
165
+ this._setText("pop-total", c.totalPop.toString());
166
+ for (const type of Object.keys(ENTITY_TYPES)) {
167
+ this._setText(`pop-${type}`, (c.population[type] || 0).toString());
168
+ }
169
+
170
+ this._setText("stat-births", formatNumber(stats.totalBirths));
171
+ this._setText("stat-deaths", formatNumber(stats.totalDeaths));
172
+ this._setText("stat-peak", stats.peakPopulation.toString());
173
+ this._setText("stat-diversity", (c.diversity * 100).toFixed(0) + "%");
174
+
175
+ this._setText("perf-fps", Math.round(c.fps).toString());
176
+ this._setText("perf-entities", c.entityCount.toString());
177
+ this._setText("perf-particles", c.particleCount.toString());
178
+
179
+ const fpsEl = document.getElementById("perf-fps");
180
+ if (fpsEl) {
181
+ if (c.fps >= 50) fpsEl.className = "count neon-text-green";
182
+ else if (c.fps >= 30) fpsEl.className = "count neon-text-amber";
183
+ else fpsEl.className = "count neon-text-red";
184
+ }
185
+
186
+ if (this._chartCtx && this._chartCanvas) {
187
+ this._chartCtx.clearRect(
188
+ 0,
189
+ 0,
190
+ this._chartCanvas.width,
191
+ this._chartCanvas.height,
192
+ );
193
+ stats.renderCharts(
194
+ this._chartCtx,
195
+ 4,
196
+ 0,
197
+ this._chartCanvas.width - 8,
198
+ this._chartCanvas.height,
199
+ );
200
+ }
201
+
202
+ this._srTimer += dt;
203
+ if (this._srTimer >= 10) {
204
+ this._srTimer = 0;
205
+ this._updateScreenReaderSummary(stats);
206
+ }
207
+ }
208
+
209
+ _setText(id, text) {
210
+ const el = document.getElementById(id);
211
+ if (el && el.textContent !== text) {
212
+ el.textContent = text;
213
+ }
214
+ }
215
+
216
+ _formatTime(seconds) {
217
+ const m = Math.floor(seconds / 60);
218
+ const s = Math.floor(seconds % 60);
219
+ return `${m}:${s.toString().padStart(2, "0")}`;
220
+ }
221
+
222
+ addEvent(message, type = "world") {
223
+ const time = new Date();
224
+ const timeStr = time.toLocaleTimeString([], {
225
+ hour: "2-digit",
226
+ minute: "2-digit",
227
+ second: "2-digit",
228
+ });
229
+
230
+ this.events.push({ message, type, time: timeStr });
231
+ if (this.events.length > MAX_EVENTS) this.events.shift();
232
+
233
+ const list = document.getElementById("event-list");
234
+ if (!list) return;
235
+
236
+ const entry = document.createElement("div");
237
+ entry.className = `event-entry event-${type}`;
238
+ entry.innerHTML = `<span class="timestamp">${timeStr}</span>${this._escapeHtml(message)}`;
239
+ list.appendChild(entry);
240
+
241
+ while (list.children.length > MAX_EVENTS) {
242
+ list.removeChild(list.firstChild);
243
+ }
244
+
245
+ const panel = document.getElementById("panel-events");
246
+ if (panel) panel.scrollTop = panel.scrollHeight;
247
+ }
248
+
249
+ announce(message) {
250
+ if (!this._announcement) return;
251
+ this._announcement.textContent = message;
252
+ this._announcement.classList.add("visible");
253
+
254
+ if (this._announcementTimeout) clearTimeout(this._announcementTimeout);
255
+ this._announcementTimeout = setTimeout(() => {
256
+ this._announcement.classList.remove("visible");
257
+ }, 4000);
258
+ }
259
+
260
+ announceWorldEvent(event) {
261
+ const info = EVENT_TYPES[event.type];
262
+ if (info) {
263
+ this.announce(`${info.icon} ${info.name} detected!`);
264
+ this.addEvent(
265
+ `${info.name} at (${Math.round(event.x)}, ${Math.round(event.y)})`,
266
+ "world",
267
+ );
268
+ }
269
+ }
270
+
271
+ _updateScreenReaderSummary(stats) {
272
+ const c = stats.current;
273
+ const parts = [
274
+ `Generation ${stats.generationCount}.`,
275
+ `Total population: ${c.totalPop}.`,
276
+ ];
277
+ for (const [type, info] of Object.entries(ENTITY_TYPES)) {
278
+ parts.push(`${info.name}: ${c.population[type] || 0}.`);
279
+ }
280
+ parts.push(`Average fitness: ${c.avgFitness.toFixed(1)}.`);
281
+ parts.push(`Diversity: ${(c.diversity * 100).toFixed(0)} percent.`);
282
+ if (this._srSummary) {
283
+ this._srSummary.textContent = parts.join(" ");
284
+ }
285
+ }
286
+
287
+ _escapeHtml(str) {
288
+ return str
289
+ .replace(/&/g, "&amp;")
290
+ .replace(/</g, "&lt;")
291
+ .replace(/>/g, "&gt;");
292
+ }
293
+ }
js/utils.js ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const TAU = Math.PI * 2;
2
+
3
+ export function lerp(a, b, t) {
4
+ return a + (b - a) * t;
5
+ }
6
+
7
+ export function clamp(v, min, max) {
8
+ return v < min ? min : v > max ? max : v;
9
+ }
10
+
11
+ export function distance(x1, y1, x2, y2) {
12
+ const dx = x2 - x1;
13
+ const dy = y2 - y1;
14
+ return Math.sqrt(dx * dx + dy * dy);
15
+ }
16
+
17
+ export function distanceSq(x1, y1, x2, y2) {
18
+ const dx = x2 - x1;
19
+ const dy = y2 - y1;
20
+ return dx * dx + dy * dy;
21
+ }
22
+
23
+ export function randomRange(min, max) {
24
+ return min + Math.random() * (max - min);
25
+ }
26
+
27
+ export function randomInt(min, max) {
28
+ return Math.floor(randomRange(min, max + 1));
29
+ }
30
+
31
+ export function randomGaussian(mean = 0, std = 1) {
32
+ let u = 0,
33
+ v = 0;
34
+ while (u === 0) u = Math.random();
35
+ while (v === 0) v = Math.random();
36
+ return mean + std * Math.sqrt(-2.0 * Math.log(u)) * Math.cos(TAU * v);
37
+ }
38
+
39
+ export function randomChoice(arr) {
40
+ return arr[Math.floor(Math.random() * arr.length)];
41
+ }
42
+
43
+ export function normalize(x, y) {
44
+ const len = Math.sqrt(x * x + y * y);
45
+ if (len === 0) return { x: 0, y: 0 };
46
+ return { x: x / len, y: y / len };
47
+ }
48
+
49
+ export function angleBetween(x1, y1, x2, y2) {
50
+ return Math.atan2(y2 - y1, x2 - x1);
51
+ }
52
+
53
+ export function hslToHex(h, s, l) {
54
+ s /= 100;
55
+ l /= 100;
56
+ const a = s * Math.min(l, 1 - l);
57
+ const f = (n) => {
58
+ const k = (n + h / 30) % 12;
59
+ const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
60
+ return Math.round(255 * color)
61
+ .toString(16)
62
+ .padStart(2, "0");
63
+ };
64
+ return `#${f(0)}${f(8)}${f(4)}`;
65
+ }
66
+
67
+ export function hexToRgb(hex) {
68
+ const r = parseInt(hex.slice(1, 3), 16);
69
+ const g = parseInt(hex.slice(3, 5), 16);
70
+ const b = parseInt(hex.slice(5, 7), 16);
71
+ return { r, g, b };
72
+ }
73
+
74
+ export function rgbToString(r, g, b, a = 1) {
75
+ return a < 1 ? `rgba(${r},${g},${b},${a})` : `rgb(${r},${g},${b})`;
76
+ }
77
+
78
+ export function smoothstep(edge0, edge1, x) {
79
+ const t = clamp((x - edge0) / (edge1 - edge0), 0, 1);
80
+ return t * t * (3 - 2 * t);
81
+ }
82
+
83
+ /** Simple seeded pseudo-random for reproducible sequences */
84
+ export function seededRandom(seed) {
85
+ let s = seed;
86
+ return function () {
87
+ s = (s * 1664525 + 1013904223) & 0xffffffff;
88
+ return (s >>> 0) / 0xffffffff;
89
+ };
90
+ }
91
+
92
+ /** Object pool for reducing GC pressure */
93
+ export class ObjectPool {
94
+ constructor(factory, reset, initialSize = 100) {
95
+ this._factory = factory;
96
+ this._reset = reset;
97
+ this._pool = [];
98
+ for (let i = 0; i < initialSize; i++) {
99
+ this._pool.push(factory());
100
+ }
101
+ }
102
+
103
+ acquire() {
104
+ if (this._pool.length > 0) {
105
+ return this._pool.pop();
106
+ }
107
+ return this._factory();
108
+ }
109
+
110
+ release(obj) {
111
+ this._reset(obj);
112
+ this._pool.push(obj);
113
+ }
114
+
115
+ get available() {
116
+ return this._pool.length;
117
+ }
118
+ }
119
+
120
+ /** Spatial hash grid for efficient proximity queries */
121
+ export class SpatialGrid {
122
+ constructor(width, height, cellSize) {
123
+ this.cellSize = cellSize;
124
+ this.cols = Math.ceil(width / cellSize);
125
+ this.rows = Math.ceil(height / cellSize);
126
+ this.cells = new Array(this.cols * this.rows);
127
+ this.clear();
128
+ }
129
+
130
+ clear() {
131
+ for (let i = 0; i < this.cells.length; i++) {
132
+ if (this.cells[i]) {
133
+ this.cells[i].length = 0;
134
+ } else {
135
+ this.cells[i] = [];
136
+ }
137
+ }
138
+ }
139
+
140
+ _key(col, row) {
141
+ return row * this.cols + col;
142
+ }
143
+
144
+ insert(item, x, y) {
145
+ const col = Math.floor(x / this.cellSize);
146
+ const row = Math.floor(y / this.cellSize);
147
+ if (col >= 0 && col < this.cols && row >= 0 && row < this.rows) {
148
+ this.cells[this._key(col, row)].push(item);
149
+ }
150
+ }
151
+
152
+ query(x, y, radius) {
153
+ const results = [];
154
+ const minCol = Math.max(0, Math.floor((x - radius) / this.cellSize));
155
+ const maxCol = Math.min(
156
+ this.cols - 1,
157
+ Math.floor((x + radius) / this.cellSize),
158
+ );
159
+ const minRow = Math.max(0, Math.floor((y - radius) / this.cellSize));
160
+ const maxRow = Math.min(
161
+ this.rows - 1,
162
+ Math.floor((y + radius) / this.cellSize),
163
+ );
164
+ const rSq = radius * radius;
165
+
166
+ for (let row = minRow; row <= maxRow; row++) {
167
+ for (let col = minCol; col <= maxCol; col++) {
168
+ const cell = this.cells[this._key(col, row)];
169
+ for (let i = 0; i < cell.length; i++) {
170
+ const item = cell[i];
171
+ const dx = item.x - x;
172
+ const dy = item.y - y;
173
+ if (dx * dx + dy * dy <= rSq) {
174
+ results.push(item);
175
+ }
176
+ }
177
+ }
178
+ }
179
+ return results;
180
+ }
181
+ }
182
+
183
+ /** Exponential moving average for smooth telemetry */
184
+ export class EMA {
185
+ constructor(alpha = 0.1) {
186
+ this.alpha = alpha;
187
+ this.value = null;
188
+ }
189
+
190
+ update(v) {
191
+ if (this.value === null) {
192
+ this.value = v;
193
+ } else {
194
+ this.value = this.alpha * v + (1 - this.alpha) * this.value;
195
+ }
196
+ return this.value;
197
+ }
198
+
199
+ reset() {
200
+ this.value = null;
201
+ }
202
+ }
203
+
204
+ /** Ring buffer for fixed-size history */
205
+ export class RingBuffer {
206
+ constructor(capacity) {
207
+ this.capacity = capacity;
208
+ this.data = new Array(capacity);
209
+ this.head = 0;
210
+ this.size = 0;
211
+ }
212
+
213
+ push(value) {
214
+ this.data[this.head] = value;
215
+ this.head = (this.head + 1) % this.capacity;
216
+ if (this.size < this.capacity) this.size++;
217
+ }
218
+
219
+ toArray() {
220
+ const arr = new Array(this.size);
221
+ for (let i = 0; i < this.size; i++) {
222
+ arr[i] =
223
+ this.data[(this.head - this.size + i + this.capacity) % this.capacity];
224
+ }
225
+ return arr;
226
+ }
227
+
228
+ get last() {
229
+ if (this.size === 0) return undefined;
230
+ return this.data[(this.head - 1 + this.capacity) % this.capacity];
231
+ }
232
+ }
233
+
234
+ /** Color constants for entity types */
235
+ export const COLORS = {
236
+ gatherer: { hex: "#00ff88", r: 0, g: 255, b: 136 },
237
+ predator: { hex: "#ff3366", r: 255, g: 51, b: 102 },
238
+ builder: { hex: "#3399ff", r: 51, g: 153, b: 255 },
239
+ explorer: { hex: "#ffaa00", r: 255, g: 170, b: 0 },
240
+ hybrid: { hex: "#cc66ff", r: 204, g: 102, b: 255 },
241
+ food: { hex: "#00ffaa", r: 0, g: 255, b: 170 },
242
+ energy: { hex: "#ffee44", r: 255, g: 238, b: 68 },
243
+ cyan: { hex: "#00f0ff", r: 0, g: 240, b: 255 },
244
+ };
245
+
246
+ /** Format a number for display (e.g., 1234 -> 1.2k) */
247
+ export function formatNumber(n) {
248
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + "M";
249
+ if (n >= 1000) return (n / 1000).toFixed(1) + "k";
250
+ return Math.round(n).toString();
251
+ }
252
+
253
+ /** Wrap value within bounds */
254
+ export function wrap(value, min, max) {
255
+ const range = max - min;
256
+ return ((((value - min) % range) + range) % range) + min;
257
+ }
js/world.js ADDED
@@ -0,0 +1,456 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { randomRange, SpatialGrid, TAU, distance } from "./utils.js";
2
+
3
+ const RESOURCE_FOOD = 0;
4
+ const RESOURCE_ENERGY = 1;
5
+
6
+ let nextResourceId = 1;
7
+
8
+ class Resource {
9
+ constructor(x, y, type, amount) {
10
+ this.id = nextResourceId++;
11
+ this.x = x;
12
+ this.y = y;
13
+ this.type = type;
14
+ this.amount = amount;
15
+ this.maxAmount = amount;
16
+ this.regenRate = type === RESOURCE_FOOD ? 0.3 : 0.15;
17
+ this.pulsePhase = Math.random() * TAU;
18
+ this.alive = true;
19
+ }
20
+
21
+ update(dt, regenMultiplier) {
22
+ if (this.amount < this.maxAmount) {
23
+ this.amount = Math.min(
24
+ this.maxAmount,
25
+ this.amount + this.regenRate * regenMultiplier * dt,
26
+ );
27
+ }
28
+ this.pulsePhase += dt * 2;
29
+ }
30
+
31
+ consume(amount) {
32
+ const taken = Math.min(this.amount, amount);
33
+ this.amount -= taken;
34
+ if (this.amount <= 0.01) {
35
+ this.alive = false;
36
+ }
37
+ return taken;
38
+ }
39
+ }
40
+
41
+ class WorldEvent {
42
+ constructor(type, x, y, radius, duration, intensity) {
43
+ this.type = type;
44
+ this.x = x;
45
+ this.y = y;
46
+ this.radius = radius;
47
+ this.duration = duration;
48
+ this.elapsed = 0;
49
+ this.intensity = intensity;
50
+ this.active = true;
51
+ }
52
+
53
+ update(dt) {
54
+ this.elapsed += dt;
55
+ if (this.elapsed >= this.duration) {
56
+ this.active = false;
57
+ }
58
+ }
59
+
60
+ get progress() {
61
+ return this.elapsed / this.duration;
62
+ }
63
+
64
+ get currentIntensity() {
65
+ const t = this.progress;
66
+ if (t < 0.1) return this.intensity * (t / 0.1);
67
+ if (t > 0.8) return this.intensity * ((1 - t) / 0.2);
68
+ return this.intensity;
69
+ }
70
+ }
71
+
72
+ export const EVENT_TYPES = {
73
+ bloom: { name: "Resource Bloom", color: "#00ffaa", icon: "B" },
74
+ drought: { name: "Drought", color: "#ff8844", icon: "D" },
75
+ storm: { name: "Energy Storm", color: "#ffee44", icon: "S" },
76
+ catastrophe: { name: "Catastrophe", color: "#ff3366", icon: "!" },
77
+ abundance: { name: "Abundance", color: "#00ff88", icon: "A" },
78
+ };
79
+
80
+ export class World {
81
+ constructor(width, height) {
82
+ this.width = width;
83
+ this.height = height;
84
+ this.resources = [];
85
+ this.events = [];
86
+ this.resourceGrid = new SpatialGrid(width, height, 80);
87
+ this.eventTimer = 0;
88
+ this.eventInterval = randomRange(15, 25);
89
+ this.totalTime = 0;
90
+ this.regenMultiplier = 1.0;
91
+
92
+ this.memory = {
93
+ totalBlooms: 0,
94
+ totalDroughts: 0,
95
+ totalCatastrophes: 0,
96
+ recentEvents: [],
97
+ };
98
+
99
+ this._initResources();
100
+ }
101
+
102
+ _initResources() {
103
+ const foodCount = Math.floor((this.width * this.height) / 8000);
104
+ for (let i = 0; i < foodCount; i++) {
105
+ this.resources.push(
106
+ new Resource(
107
+ randomRange(20, this.width - 20),
108
+ randomRange(20, this.height - 20),
109
+ RESOURCE_FOOD,
110
+ randomRange(15, 40),
111
+ ),
112
+ );
113
+ }
114
+
115
+ const energyCount = Math.floor(foodCount * 0.3);
116
+ for (let i = 0; i < energyCount; i++) {
117
+ this.resources.push(
118
+ new Resource(
119
+ randomRange(20, this.width - 20),
120
+ randomRange(20, this.height - 20),
121
+ RESOURCE_ENERGY,
122
+ randomRange(25, 60),
123
+ ),
124
+ );
125
+ }
126
+ }
127
+
128
+ update(dt) {
129
+ this.totalTime += dt;
130
+
131
+ for (let i = this.resources.length - 1; i >= 0; i--) {
132
+ const r = this.resources[i];
133
+ r.update(dt, this.regenMultiplier);
134
+ if (!r.alive) {
135
+ this.resources.splice(i, 1);
136
+ }
137
+ }
138
+
139
+ const targetFood = Math.floor((this.width * this.height) / 8000);
140
+ if (this.resources.length < targetFood * 0.6) {
141
+ const count = Math.min(3, targetFood - this.resources.length);
142
+ for (let i = 0; i < count; i++) {
143
+ this.resources.push(
144
+ new Resource(
145
+ randomRange(20, this.width - 20),
146
+ randomRange(20, this.height - 20),
147
+ Math.random() < 0.75 ? RESOURCE_FOOD : RESOURCE_ENERGY,
148
+ randomRange(15, 40),
149
+ ),
150
+ );
151
+ }
152
+ }
153
+
154
+ this.resourceGrid.clear();
155
+ for (const r of this.resources) {
156
+ this.resourceGrid.insert(r, r.x, r.y);
157
+ }
158
+
159
+ for (let i = this.events.length - 1; i >= 0; i--) {
160
+ this.events[i].update(dt);
161
+ if (!this.events[i].active) {
162
+ this.events.splice(i, 1);
163
+ }
164
+ }
165
+
166
+ this.eventTimer += dt;
167
+ if (this.eventTimer >= this.eventInterval) {
168
+ this.eventTimer = 0;
169
+ this.eventInterval = randomRange(12, 22);
170
+ this._triggerRandomEvent();
171
+ }
172
+
173
+ this._updateMemoryEffects();
174
+ }
175
+
176
+ _triggerRandomEvent() {
177
+ const types = Object.keys(EVENT_TYPES);
178
+ const weights = [1, 1, 1, 0.3, 1];
179
+ if (this.memory.totalDroughts > this.memory.totalBlooms) {
180
+ weights[0] += 1;
181
+ }
182
+ if (this.memory.totalCatastrophes > 2) {
183
+ weights[3] *= 0.3;
184
+ weights[4] += 1;
185
+ }
186
+
187
+ const totalWeight = weights.reduce((a, b) => a + b, 0);
188
+ let r = Math.random() * totalWeight;
189
+ let typeIdx = 0;
190
+ for (let i = 0; i < weights.length; i++) {
191
+ r -= weights[i];
192
+ if (r <= 0) {
193
+ typeIdx = i;
194
+ break;
195
+ }
196
+ }
197
+
198
+ const type = types[typeIdx];
199
+ const x = randomRange(100, this.width - 100);
200
+ const y = randomRange(100, this.height - 100);
201
+ const radius = randomRange(120, 250);
202
+ const duration = randomRange(8, 18);
203
+
204
+ const event = new WorldEvent(type, x, y, radius, duration, 1.0);
205
+ this.events.push(event);
206
+ this._applyEventEffects(event);
207
+
208
+ this.memory.recentEvents.push({ type, time: this.totalTime });
209
+ if (this.memory.recentEvents.length > 20) this.memory.recentEvents.shift();
210
+
211
+ if (type === "bloom" || type === "abundance") this.memory.totalBlooms++;
212
+ if (type === "drought") this.memory.totalDroughts++;
213
+ if (type === "catastrophe") this.memory.totalCatastrophes++;
214
+
215
+ return event;
216
+ }
217
+
218
+ _applyEventEffects(event) {
219
+ switch (event.type) {
220
+ case "bloom": {
221
+ for (let i = 0; i < 8; i++) {
222
+ const angle = Math.random() * TAU;
223
+ const dist = Math.random() * event.radius;
224
+ const rx = event.x + Math.cos(angle) * dist;
225
+ const ry = event.y + Math.sin(angle) * dist;
226
+ if (
227
+ rx > 10 &&
228
+ rx < this.width - 10 &&
229
+ ry > 10 &&
230
+ ry < this.height - 10
231
+ ) {
232
+ this.resources.push(
233
+ new Resource(rx, ry, RESOURCE_FOOD, randomRange(20, 50)),
234
+ );
235
+ }
236
+ }
237
+ break;
238
+ }
239
+ case "drought": {
240
+ for (const r of this.resources) {
241
+ if (distance(r.x, r.y, event.x, event.y) < event.radius) {
242
+ r.amount *= 0.5;
243
+ }
244
+ }
245
+ break;
246
+ }
247
+ case "storm": {
248
+ for (let i = 0; i < 5; i++) {
249
+ const angle = Math.random() * TAU;
250
+ const dist = Math.random() * event.radius;
251
+ const rx = event.x + Math.cos(angle) * dist;
252
+ const ry = event.y + Math.sin(angle) * dist;
253
+ if (
254
+ rx > 10 &&
255
+ rx < this.width - 10 &&
256
+ ry > 10 &&
257
+ ry < this.height - 10
258
+ ) {
259
+ this.resources.push(
260
+ new Resource(rx, ry, RESOURCE_ENERGY, randomRange(30, 70)),
261
+ );
262
+ }
263
+ }
264
+ break;
265
+ }
266
+ case "abundance": {
267
+ this.regenMultiplier = 2.0;
268
+ setTimeout(() => {
269
+ this.regenMultiplier = 1.0;
270
+ }, event.duration * 1000);
271
+ break;
272
+ }
273
+ }
274
+ }
275
+
276
+ _updateMemoryEffects() {
277
+ if (this.memory.totalCatastrophes > 3) {
278
+ this.regenMultiplier = Math.max(this.regenMultiplier, 1.2);
279
+ }
280
+ }
281
+
282
+ getResourcesNear(x, y, radius) {
283
+ return this.resourceGrid.query(x, y, radius);
284
+ }
285
+
286
+ getEventsAt(x, y) {
287
+ const affecting = [];
288
+ for (const e of this.events) {
289
+ if (distance(x, y, e.x, e.y) < e.radius) {
290
+ affecting.push(e);
291
+ }
292
+ }
293
+ return affecting;
294
+ }
295
+
296
+ getCatastropheDamage(x, y) {
297
+ let dmg = 0;
298
+ for (const e of this.events) {
299
+ if (e.type === "catastrophe") {
300
+ const d = distance(x, y, e.x, e.y);
301
+ if (d < e.radius) {
302
+ dmg += e.currentIntensity * (1 - d / e.radius) * 0.5;
303
+ }
304
+ }
305
+ }
306
+ return dmg;
307
+ }
308
+
309
+ render(ctx) {
310
+ if (
311
+ !this._bgCache ||
312
+ this._bgCache.width !== this.width ||
313
+ this._bgCache.height !== this.height
314
+ ) {
315
+ this._bgCache = document.createElement("canvas");
316
+ this._bgCache.width = this.width;
317
+ this._bgCache.height = this.height;
318
+ const bgCtx = this._bgCache.getContext("2d");
319
+ const spacing = 35;
320
+ bgCtx.fillStyle = "rgba(80, 160, 220, 0.025)";
321
+ for (let x = spacing; x < this.width; x += spacing) {
322
+ for (let y = spacing; y < this.height; y += spacing) {
323
+ bgCtx.fillRect(x, y, 1.2, 1.2);
324
+ }
325
+ }
326
+ bgCtx.strokeStyle = "rgba(60, 120, 180, 0.015)";
327
+ bgCtx.lineWidth = 0.5;
328
+ for (let x = spacing; x < this.width; x += spacing * 4) {
329
+ bgCtx.beginPath();
330
+ bgCtx.moveTo(x, 0);
331
+ bgCtx.lineTo(x, this.height);
332
+ bgCtx.stroke();
333
+ }
334
+ for (let y = spacing; y < this.height; y += spacing * 4) {
335
+ bgCtx.beginPath();
336
+ bgCtx.moveTo(0, y);
337
+ bgCtx.lineTo(this.width, y);
338
+ bgCtx.stroke();
339
+ }
340
+ }
341
+ ctx.drawImage(this._bgCache, 0, 0);
342
+
343
+ for (const e of this.events) {
344
+ const info = EVENT_TYPES[e.type];
345
+ const intensity = e.currentIntensity;
346
+ const rgb = this._hexToRgb(info.color);
347
+
348
+ const grad = ctx.createRadialGradient(e.x, e.y, 0, e.x, e.y, e.radius);
349
+ grad.addColorStop(
350
+ 0,
351
+ `rgba(${rgb.r},${rgb.g},${rgb.b},${0.08 * intensity})`,
352
+ );
353
+ grad.addColorStop(
354
+ 0.7,
355
+ `rgba(${rgb.r},${rgb.g},${rgb.b},${0.03 * intensity})`,
356
+ );
357
+ grad.addColorStop(1, `rgba(${rgb.r},${rgb.g},${rgb.b},0)`);
358
+ ctx.fillStyle = grad;
359
+ ctx.beginPath();
360
+ ctx.arc(e.x, e.y, e.radius, 0, TAU);
361
+ ctx.fill();
362
+
363
+ const ringPulse = 0.5 + 0.5 * Math.sin(this.totalTime * 2);
364
+ ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},${0.15 * intensity * ringPulse})`;
365
+ ctx.lineWidth = 1.5;
366
+ ctx.setLineDash([8, 8]);
367
+ ctx.stroke();
368
+ ctx.setLineDash([]);
369
+
370
+ ctx.fillStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},${0.3 * intensity})`;
371
+ ctx.font = "10px Courier New";
372
+ ctx.textAlign = "center";
373
+ ctx.fillText(info.name.toUpperCase(), e.x, e.y - e.radius + 15);
374
+ ctx.textAlign = "start";
375
+ }
376
+
377
+ if (!this._foodGlow) {
378
+ this._foodGlow = this._makeGlowSprite(0, 255, 170, 20);
379
+ this._energyGlow = this._makeGlowSprite(255, 238, 68, 20);
380
+ }
381
+
382
+ for (const r of this.resources) {
383
+ const pulse = 0.6 + 0.4 * Math.sin(r.pulsePhase);
384
+ const sizeRatio = r.amount / r.maxAmount;
385
+ const baseRadius = 3 + sizeRatio * 4;
386
+ const radius = baseRadius * (0.85 + 0.15 * pulse);
387
+
388
+ const sprite =
389
+ r.type === RESOURCE_FOOD ? this._foodGlow : this._energyGlow;
390
+ const gs = sprite.width;
391
+ ctx.globalAlpha = 0.4 + 0.4 * sizeRatio;
392
+ ctx.drawImage(sprite, r.x - gs / 2, r.y - gs / 2);
393
+ ctx.globalAlpha = 1;
394
+
395
+ if (r.type === RESOURCE_FOOD) {
396
+ ctx.fillStyle = `rgba(0,255,170,${0.5 + 0.4 * sizeRatio})`;
397
+ } else {
398
+ ctx.fillStyle = `rgba(255,238,68,${0.5 + 0.4 * sizeRatio})`;
399
+ }
400
+ ctx.beginPath();
401
+ ctx.arc(r.x, r.y, radius, 0, TAU);
402
+ ctx.fill();
403
+ }
404
+ }
405
+
406
+ _makeGlowSprite(r, g, b, size) {
407
+ const canvas = document.createElement("canvas");
408
+ const s = size * 3;
409
+ canvas.width = s;
410
+ canvas.height = s;
411
+ const ctx = canvas.getContext("2d");
412
+ const grad = ctx.createRadialGradient(s / 2, s / 2, 0, s / 2, s / 2, s / 2);
413
+ grad.addColorStop(0, `rgba(${r},${g},${b},0.3)`);
414
+ grad.addColorStop(0.5, `rgba(${r},${g},${b},0.08)`);
415
+ grad.addColorStop(1, `rgba(${r},${g},${b},0)`);
416
+ ctx.fillStyle = grad;
417
+ ctx.fillRect(0, 0, s, s);
418
+ return canvas;
419
+ }
420
+
421
+ _hexToRgb(hex) {
422
+ const r = parseInt(hex.slice(1, 3), 16);
423
+ const g = parseInt(hex.slice(3, 5), 16);
424
+ const b = parseInt(hex.slice(5, 7), 16);
425
+ return { r, g, b };
426
+ }
427
+
428
+ toJSON() {
429
+ return {
430
+ width: this.width,
431
+ height: this.height,
432
+ totalTime: this.totalTime,
433
+ memory: { ...this.memory },
434
+ resources: this.resources.map((r) => ({
435
+ x: r.x,
436
+ y: r.y,
437
+ type: r.type,
438
+ amount: r.amount,
439
+ maxAmount: r.maxAmount,
440
+ })),
441
+ };
442
+ }
443
+
444
+ static fromJSON(data) {
445
+ const w = new World(data.width, data.height);
446
+ w.resources = [];
447
+ w.totalTime = data.totalTime || 0;
448
+ w.memory = data.memory || w.memory;
449
+ for (const rd of data.resources) {
450
+ const r = new Resource(rd.x, rd.y, rd.type, rd.maxAmount);
451
+ r.amount = rd.amount;
452
+ w.resources.push(r);
453
+ }
454
+ return w;
455
+ }
456
+ }