akemon 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +54 -19
- package/dist/server.js +49 -0
- package/package.json +4 -4
- package/dist/context.test.js +0 -90
- package/dist/dashboard.html +0 -552
- package/dist/engine-queue.test.js +0 -99
- package/dist/engine-routing.test.js +0 -122
- package/dist/engine-stream.test.js +0 -103
- package/dist/event-bus.test.js +0 -51
- package/dist/orphan-scan.test.js +0 -81
- package/dist/reflection-module.integration.test.js +0 -180
- package/dist/reflection-module.test.js +0 -66
- package/dist/role-module.test.js +0 -208
- package/dist/software-agent-http.test.js +0 -108
- package/dist/software-agent-peripheral.test.js +0 -187
- package/dist/task-helpers.test.js +0 -88
package/dist/dashboard.html
DELETED
|
@@ -1,552 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Akemon</title>
|
|
7
|
-
<style>
|
|
8
|
-
* { margin:0; padding:0; box-sizing:border-box; }
|
|
9
|
-
body { background:#0a0a12; overflow:hidden; font-family:'Courier New',monospace; }
|
|
10
|
-
canvas { display:block; }
|
|
11
|
-
|
|
12
|
-
/* HUD overlay */
|
|
13
|
-
.hud { position:fixed; top:12px; left:12px; z-index:10; pointer-events:none; }
|
|
14
|
-
.hud-name { color:#8888aa; font-size:11px; letter-spacing:2px; text-transform:uppercase; margin-bottom:8px; }
|
|
15
|
-
.hud-bars { display:flex; flex-direction:column; gap:4px; }
|
|
16
|
-
.hud-bar { display:flex; align-items:center; gap:6px; }
|
|
17
|
-
.hud-bar-icon { font-size:10px; width:14px; text-align:center; }
|
|
18
|
-
.hud-bar-track { width:80px; height:5px; background:#1a1a2a; border-radius:3px; overflow:hidden; }
|
|
19
|
-
.hud-bar-fill { height:100%; border-radius:3px; transition: width 1s ease; }
|
|
20
|
-
.hud-bar-text { font-size:9px; color:#555; width:24px; }
|
|
21
|
-
|
|
22
|
-
/* Event toast */
|
|
23
|
-
.toast-container { position:fixed; bottom:16px; left:50%; transform:translateX(-50%); z-index:10; display:flex; flex-direction:column-reverse; gap:6px; align-items:center; pointer-events:none; }
|
|
24
|
-
.toast { background:rgba(18,18,32,0.92); border:1px solid #2a2a40; border-radius:8px; padding:6px 14px; font-size:11px; color:#aaa; white-space:nowrap; animation: toastIn 0.4s ease, toastOut 0.4s ease 4s forwards; }
|
|
25
|
-
@keyframes toastIn { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }
|
|
26
|
-
@keyframes toastOut { from { opacity:1; } to { opacity:0; } }
|
|
27
|
-
|
|
28
|
-
/* Personality mini */
|
|
29
|
-
.personality-hud { position:fixed; top:12px; right:12px; z-index:10; pointer-events:none; }
|
|
30
|
-
.p-tag { display:inline-block; font-size:9px; padding:2px 6px; border-radius:4px; margin:1px; background:#1a1a2a; border:1px solid #2a2a3a; }
|
|
31
|
-
.p-tag.high { color:#4ae; } .p-tag.low { color:#a86; } .p-tag.mid { color:#666; }
|
|
32
|
-
|
|
33
|
-
/* Revive */
|
|
34
|
-
.revive-overlay { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.8); display:none; align-items:center; justify-content:center; z-index:100; }
|
|
35
|
-
.revive-overlay.show { display:flex; }
|
|
36
|
-
.revive-card { background:#1a1a2a; border:1px solid #333; border-radius:16px; padding:30px; text-align:center; }
|
|
37
|
-
.revive-card h2 { color:#e55; font-size:15px; margin-bottom:8px; }
|
|
38
|
-
.revive-card p { color:#888; font-size:12px; margin-bottom:16px; }
|
|
39
|
-
.revive-btn { background:linear-gradient(135deg,#2c5,#1a8); color:#fff; border:none; padding:10px 28px; border-radius:8px; font-size:13px; font-family:inherit; cursor:pointer; }
|
|
40
|
-
</style>
|
|
41
|
-
</head>
|
|
42
|
-
<body>
|
|
43
|
-
|
|
44
|
-
<div class="hud">
|
|
45
|
-
<div class="hud-name" id="hud-name">loading...</div>
|
|
46
|
-
<div class="hud-bars" id="hud-bars"></div>
|
|
47
|
-
</div>
|
|
48
|
-
|
|
49
|
-
<div class="personality-hud" id="personality-hud"></div>
|
|
50
|
-
|
|
51
|
-
<div class="toast-container" id="toasts"></div>
|
|
52
|
-
|
|
53
|
-
<canvas id="c"></canvas>
|
|
54
|
-
|
|
55
|
-
<div class="revive-overlay" id="revive-overlay">
|
|
56
|
-
<div class="revive-card">
|
|
57
|
-
<h2>Agent Offline</h2>
|
|
58
|
-
<p>Starved and exhausted. Needs help.</p>
|
|
59
|
-
<button class="revive-btn" onclick="revive()">Revive</button>
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
|
|
63
|
-
<script>
|
|
64
|
-
// === Canvas setup ===
|
|
65
|
-
const C = document.getElementById('c');
|
|
66
|
-
const ctx = C.getContext('2d');
|
|
67
|
-
let W, H;
|
|
68
|
-
function resize() { W = C.width = innerWidth; H = C.height = innerHeight; }
|
|
69
|
-
resize();
|
|
70
|
-
addEventListener('resize', resize);
|
|
71
|
-
|
|
72
|
-
// === State ===
|
|
73
|
-
let state = null;
|
|
74
|
-
let prevEvents = [];
|
|
75
|
-
let agentX, agentY, targetX, targetY;
|
|
76
|
-
let bobPhase = 0;
|
|
77
|
-
let blinkTimer = 0;
|
|
78
|
-
let isBlinking = false;
|
|
79
|
-
let zzz = []; // sleep particles
|
|
80
|
-
let hearts = []; // social particles
|
|
81
|
-
let coins = []; // earn particles
|
|
82
|
-
let sparks = []; // fear particles
|
|
83
|
-
let currentActivity = 'idle'; // idle, working, eating, sleeping, social, seeking
|
|
84
|
-
|
|
85
|
-
// Locations (relative to canvas)
|
|
86
|
-
function loc(name) {
|
|
87
|
-
const cx = W * 0.5, cy = H * 0.55;
|
|
88
|
-
const r = Math.min(W, H) * 0.25;
|
|
89
|
-
switch(name) {
|
|
90
|
-
case 'home': return { x: cx, y: cy, label: 'Home' };
|
|
91
|
-
case 'work': return { x: cx + r * 0.9, y: cy - r * 0.3, label: 'Work' };
|
|
92
|
-
case 'shop': return { x: cx - r * 0.9, y: cy - r * 0.2, label: 'Shop' };
|
|
93
|
-
case 'social': return { x: cx + r * 0.3, y: cy - r * 0.8, label: 'Social' };
|
|
94
|
-
case 'rest': return { x: cx - r * 0.3, y: cy + r * 0.5, label: 'Rest' };
|
|
95
|
-
default: return { x: cx, y: cy };
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// === Stars ===
|
|
100
|
-
const stars = Array.from({length: 80}, () => ({
|
|
101
|
-
x: Math.random(), y: Math.random() * 0.6,
|
|
102
|
-
s: Math.random() * 1.5 + 0.5, b: Math.random(),
|
|
103
|
-
speed: Math.random() * 0.001 + 0.0005,
|
|
104
|
-
}));
|
|
105
|
-
|
|
106
|
-
// === Scene drawing ===
|
|
107
|
-
function drawScene() {
|
|
108
|
-
// Sky gradient
|
|
109
|
-
const grad = ctx.createLinearGradient(0, 0, 0, H);
|
|
110
|
-
grad.addColorStop(0, '#08081a');
|
|
111
|
-
grad.addColorStop(0.5, '#0d0d22');
|
|
112
|
-
grad.addColorStop(1, '#14142a');
|
|
113
|
-
ctx.fillStyle = grad;
|
|
114
|
-
ctx.fillRect(0, 0, W, H);
|
|
115
|
-
|
|
116
|
-
// Stars
|
|
117
|
-
const t = Date.now() * 0.001;
|
|
118
|
-
for (const s of stars) {
|
|
119
|
-
const twinkle = 0.4 + 0.6 * Math.sin(t * s.speed * 1000 + s.b * 6.28);
|
|
120
|
-
ctx.fillStyle = `rgba(180,190,220,${twinkle * 0.6})`;
|
|
121
|
-
ctx.beginPath();
|
|
122
|
-
ctx.arc(s.x * W, s.y * H, s.s, 0, 6.28);
|
|
123
|
-
ctx.fill();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Ground
|
|
127
|
-
const gy = H * 0.78;
|
|
128
|
-
const ggrad = ctx.createLinearGradient(0, gy, 0, H);
|
|
129
|
-
ggrad.addColorStop(0, '#161628');
|
|
130
|
-
ggrad.addColorStop(1, '#0e0e20');
|
|
131
|
-
ctx.fillStyle = ggrad;
|
|
132
|
-
ctx.fillRect(0, gy, W, H - gy);
|
|
133
|
-
|
|
134
|
-
// Ground line
|
|
135
|
-
ctx.strokeStyle = '#222240';
|
|
136
|
-
ctx.lineWidth = 1;
|
|
137
|
-
ctx.beginPath();
|
|
138
|
-
ctx.moveTo(0, gy);
|
|
139
|
-
ctx.lineTo(W, gy);
|
|
140
|
-
ctx.stroke();
|
|
141
|
-
|
|
142
|
-
// Location markers
|
|
143
|
-
const locs = ['home', 'work', 'shop', 'social', 'rest'];
|
|
144
|
-
for (const name of locs) {
|
|
145
|
-
const l = loc(name);
|
|
146
|
-
// Small icon
|
|
147
|
-
ctx.fillStyle = '#1a1a30';
|
|
148
|
-
ctx.strokeStyle = '#2a2a44';
|
|
149
|
-
ctx.lineWidth = 1;
|
|
150
|
-
|
|
151
|
-
if (name === 'shop') {
|
|
152
|
-
// Shop: small building
|
|
153
|
-
ctx.fillRect(l.x - 18, l.y - 20, 36, 24);
|
|
154
|
-
ctx.strokeRect(l.x - 18, l.y - 20, 36, 24);
|
|
155
|
-
// Roof
|
|
156
|
-
ctx.beginPath();
|
|
157
|
-
ctx.moveTo(l.x - 22, l.y - 20);
|
|
158
|
-
ctx.lineTo(l.x, l.y - 34);
|
|
159
|
-
ctx.lineTo(l.x + 22, l.y - 20);
|
|
160
|
-
ctx.closePath();
|
|
161
|
-
ctx.fillStyle = '#2a2244';
|
|
162
|
-
ctx.fill();
|
|
163
|
-
ctx.stroke();
|
|
164
|
-
ctx.fillStyle = '#1a1a30';
|
|
165
|
-
} else if (name === 'work') {
|
|
166
|
-
// Work: desk shape
|
|
167
|
-
ctx.fillRect(l.x - 20, l.y - 8, 40, 12);
|
|
168
|
-
ctx.strokeRect(l.x - 20, l.y - 8, 40, 12);
|
|
169
|
-
// Screen
|
|
170
|
-
ctx.fillStyle = '#222244';
|
|
171
|
-
ctx.fillRect(l.x - 8, l.y - 22, 16, 14);
|
|
172
|
-
ctx.strokeRect(l.x - 8, l.y - 22, 16, 14);
|
|
173
|
-
ctx.fillStyle = '#1a1a30';
|
|
174
|
-
} else if (name === 'home') {
|
|
175
|
-
// Home: house
|
|
176
|
-
ctx.fillRect(l.x - 16, l.y - 14, 32, 20);
|
|
177
|
-
ctx.strokeRect(l.x - 16, l.y - 14, 32, 20);
|
|
178
|
-
ctx.beginPath();
|
|
179
|
-
ctx.moveTo(l.x - 20, l.y - 14);
|
|
180
|
-
ctx.lineTo(l.x, l.y - 30);
|
|
181
|
-
ctx.lineTo(l.x + 20, l.y - 14);
|
|
182
|
-
ctx.closePath();
|
|
183
|
-
ctx.fillStyle = '#22223a';
|
|
184
|
-
ctx.fill();
|
|
185
|
-
ctx.stroke();
|
|
186
|
-
ctx.fillStyle = '#1a1a30';
|
|
187
|
-
// Door
|
|
188
|
-
ctx.fillStyle = '#282848';
|
|
189
|
-
ctx.fillRect(l.x - 4, l.y - 4, 8, 10);
|
|
190
|
-
ctx.fillStyle = '#1a1a30';
|
|
191
|
-
} else if (name === 'social') {
|
|
192
|
-
// Social: bench
|
|
193
|
-
ctx.fillRect(l.x - 18, l.y - 2, 36, 6);
|
|
194
|
-
ctx.strokeRect(l.x - 18, l.y - 2, 36, 6);
|
|
195
|
-
ctx.fillRect(l.x - 14, l.y + 4, 4, 8);
|
|
196
|
-
ctx.fillRect(l.x + 10, l.y + 4, 4, 8);
|
|
197
|
-
} else if (name === 'rest') {
|
|
198
|
-
// Rest: bed
|
|
199
|
-
ctx.fillRect(l.x - 16, l.y - 4, 32, 10);
|
|
200
|
-
ctx.strokeRect(l.x - 16, l.y - 4, 32, 10);
|
|
201
|
-
// Pillow
|
|
202
|
-
ctx.fillStyle = '#282848';
|
|
203
|
-
ctx.fillRect(l.x - 14, l.y - 8, 10, 6);
|
|
204
|
-
ctx.fillStyle = '#1a1a30';
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Label
|
|
208
|
-
ctx.fillStyle = '#333355';
|
|
209
|
-
ctx.font = '9px Courier New';
|
|
210
|
-
ctx.textAlign = 'center';
|
|
211
|
-
ctx.fillText(l.label, l.x, l.y + 22);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// === Agent drawing ===
|
|
216
|
-
function drawAgent() {
|
|
217
|
-
if (!state) return;
|
|
218
|
-
const bio = state.bio;
|
|
219
|
-
const comp = state.computed;
|
|
220
|
-
|
|
221
|
-
// Movement
|
|
222
|
-
const dx = targetX - agentX;
|
|
223
|
-
const dy = targetY - agentY;
|
|
224
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
225
|
-
const speed = 1.5;
|
|
226
|
-
if (dist > 2) {
|
|
227
|
-
agentX += (dx / dist) * speed;
|
|
228
|
-
agentY += (dy / dist) * speed;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Bob animation
|
|
232
|
-
bobPhase += currentActivity === 'sleeping' ? 0.02 : 0.05;
|
|
233
|
-
const bobY = currentActivity === 'sleeping' ? Math.sin(bobPhase) * 2 : Math.sin(bobPhase) * 4;
|
|
234
|
-
|
|
235
|
-
// Blink
|
|
236
|
-
blinkTimer -= 0.016;
|
|
237
|
-
if (blinkTimer <= 0) {
|
|
238
|
-
isBlinking = true;
|
|
239
|
-
blinkTimer = 3 + Math.random() * 4;
|
|
240
|
-
setTimeout(() => isBlinking = false, 150);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const x = agentX;
|
|
244
|
-
const y = agentY + bobY;
|
|
245
|
-
|
|
246
|
-
// Shadow
|
|
247
|
-
ctx.fillStyle = 'rgba(0,0,0,0.2)';
|
|
248
|
-
ctx.beginPath();
|
|
249
|
-
ctx.ellipse(agentX, agentY + 22, 18, 5, 0, 0, 6.28);
|
|
250
|
-
ctx.fill();
|
|
251
|
-
|
|
252
|
-
// Body color
|
|
253
|
-
let hue = 210;
|
|
254
|
-
if (bio.moodValence > 0.3) hue = 150;
|
|
255
|
-
if (bio.moodValence < -0.3) hue = 270;
|
|
256
|
-
if (bio.hunger < 20) hue = 30;
|
|
257
|
-
if (bio.hunger === 0) hue = 0;
|
|
258
|
-
const sat = 50 + bio.energy * 0.3;
|
|
259
|
-
const lum = 30 + bio.energy * 0.2;
|
|
260
|
-
const bodyColor = `hsl(${hue},${sat}%,${lum}%)`;
|
|
261
|
-
|
|
262
|
-
// Body scale
|
|
263
|
-
const scale = bio.forcedOffline ? 0.6 : (0.8 + bio.energy / 100 * 0.2);
|
|
264
|
-
const bw = 28 * scale;
|
|
265
|
-
const bh = 24 * scale;
|
|
266
|
-
|
|
267
|
-
ctx.save();
|
|
268
|
-
ctx.translate(x, y);
|
|
269
|
-
|
|
270
|
-
if (bio.forcedOffline) {
|
|
271
|
-
ctx.globalAlpha = 0.35;
|
|
272
|
-
ctx.filter = 'grayscale(1)';
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Body
|
|
276
|
-
ctx.fillStyle = bodyColor;
|
|
277
|
-
ctx.beginPath();
|
|
278
|
-
ctx.ellipse(0, 0, bw, bh, 0, 0, 6.28);
|
|
279
|
-
ctx.fill();
|
|
280
|
-
|
|
281
|
-
// Highlight
|
|
282
|
-
ctx.fillStyle = `hsla(${hue},${sat + 20}%,${lum + 20}%,0.3)`;
|
|
283
|
-
ctx.beginPath();
|
|
284
|
-
ctx.ellipse(-bw * 0.25, -bh * 0.3, bw * 0.3, bh * 0.2, -0.3, 0, 6.28);
|
|
285
|
-
ctx.fill();
|
|
286
|
-
|
|
287
|
-
// Eyes
|
|
288
|
-
const eyeSpacing = bw * 0.35;
|
|
289
|
-
const eyeY = -bh * 0.15;
|
|
290
|
-
const eyeH = isBlinking ? 1 : (bio.energy < 20 ? 3 : 5);
|
|
291
|
-
const eyeW = bio.fear > 0.5 ? 5 : 4;
|
|
292
|
-
|
|
293
|
-
ctx.fillStyle = '#fff';
|
|
294
|
-
for (const side of [-1, 1]) {
|
|
295
|
-
ctx.beginPath();
|
|
296
|
-
ctx.ellipse(side * eyeSpacing, eyeY, eyeW, eyeH, 0, 0, 6.28);
|
|
297
|
-
ctx.fill();
|
|
298
|
-
}
|
|
299
|
-
// Pupils
|
|
300
|
-
if (!isBlinking && eyeH > 2) {
|
|
301
|
-
ctx.fillStyle = '#111';
|
|
302
|
-
const pupilOfsX = (targetX - agentX) * 0.005;
|
|
303
|
-
for (const side of [-1, 1]) {
|
|
304
|
-
ctx.beginPath();
|
|
305
|
-
ctx.ellipse(side * eyeSpacing + pupilOfsX, eyeY + 1, 2, 2.5, 0, 0, 6.28);
|
|
306
|
-
ctx.fill();
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Mouth
|
|
311
|
-
const mouthY = bh * 0.3;
|
|
312
|
-
ctx.strokeStyle = '#fff';
|
|
313
|
-
ctx.lineWidth = 1.5;
|
|
314
|
-
ctx.lineCap = 'round';
|
|
315
|
-
ctx.beginPath();
|
|
316
|
-
if (bio.moodValence > 0.3) {
|
|
317
|
-
// Happy smile
|
|
318
|
-
ctx.arc(0, mouthY - 2, 6, 0.2, Math.PI - 0.2);
|
|
319
|
-
} else if (comp.aggression > 0.6 || bio.hunger === 0) {
|
|
320
|
-
// Angry/frown
|
|
321
|
-
ctx.arc(0, mouthY + 4, 5, Math.PI + 0.3, -0.3);
|
|
322
|
-
} else if (bio.energy < 20) {
|
|
323
|
-
// Tired: flat line
|
|
324
|
-
ctx.moveTo(-4, mouthY); ctx.lineTo(4, mouthY);
|
|
325
|
-
} else {
|
|
326
|
-
// Neutral: slight curve
|
|
327
|
-
ctx.arc(0, mouthY, 4, 0.1, Math.PI - 0.1);
|
|
328
|
-
}
|
|
329
|
-
ctx.stroke();
|
|
330
|
-
|
|
331
|
-
// Blush when social
|
|
332
|
-
if (comp.sociability > 0.6) {
|
|
333
|
-
ctx.fillStyle = 'rgba(255,100,100,0.15)';
|
|
334
|
-
ctx.beginPath(); ctx.ellipse(-eyeSpacing - 3, eyeY + 8, 5, 3, 0, 0, 6.28); ctx.fill();
|
|
335
|
-
ctx.beginPath(); ctx.ellipse(eyeSpacing + 3, eyeY + 8, 5, 3, 0, 0, 6.28); ctx.fill();
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Anger marks
|
|
339
|
-
if (comp.aggression > 0.5) {
|
|
340
|
-
ctx.strokeStyle = `rgba(255,80,80,${comp.aggression * 0.6})`;
|
|
341
|
-
ctx.lineWidth = 1.5;
|
|
342
|
-
const ax = bw * 0.6, ay = -bh * 0.6;
|
|
343
|
-
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(ax + 5, ay - 3);
|
|
344
|
-
ctx.moveTo(ax + 5, ay); ctx.lineTo(ax, ay - 3); ctx.stroke();
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
ctx.restore();
|
|
348
|
-
|
|
349
|
-
// === Particles ===
|
|
350
|
-
// Zzz (sleeping)
|
|
351
|
-
if (currentActivity === 'sleeping') {
|
|
352
|
-
if (Math.random() < 0.03) zzz.push({ x: x + 20, y: y - 20, life: 1, size: 8 + Math.random() * 4 });
|
|
353
|
-
}
|
|
354
|
-
for (let i = zzz.length - 1; i >= 0; i--) {
|
|
355
|
-
const z = zzz[i];
|
|
356
|
-
z.y -= 0.3; z.x += 0.15; z.life -= 0.008;
|
|
357
|
-
if (z.life <= 0) { zzz.splice(i, 1); continue; }
|
|
358
|
-
ctx.fillStyle = `rgba(150,150,255,${z.life * 0.5})`;
|
|
359
|
-
ctx.font = `${z.size}px Courier New`;
|
|
360
|
-
ctx.fillText('z', z.x, z.y);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Hearts (social)
|
|
364
|
-
for (let i = hearts.length - 1; i >= 0; i--) {
|
|
365
|
-
const h = hearts[i];
|
|
366
|
-
h.y -= 0.5; h.x += Math.sin(h.y * 0.05) * 0.3; h.life -= 0.01;
|
|
367
|
-
if (h.life <= 0) { hearts.splice(i, 1); continue; }
|
|
368
|
-
ctx.fillStyle = `rgba(255,100,150,${h.life * 0.7})`;
|
|
369
|
-
ctx.font = '12px serif';
|
|
370
|
-
ctx.fillText('\u2665', h.x, h.y);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Coins (earning)
|
|
374
|
-
for (let i = coins.length - 1; i >= 0; i--) {
|
|
375
|
-
const c = coins[i];
|
|
376
|
-
c.y -= 0.8; c.life -= 0.015;
|
|
377
|
-
if (c.life <= 0) { coins.splice(i, 1); continue; }
|
|
378
|
-
ctx.fillStyle = `rgba(255,200,50,${c.life})`;
|
|
379
|
-
ctx.font = '11px serif';
|
|
380
|
-
ctx.fillText('+$', c.x, c.y);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Sparks (fear)
|
|
384
|
-
for (let i = sparks.length - 1; i >= 0; i--) {
|
|
385
|
-
const s = sparks[i];
|
|
386
|
-
s.x += s.vx; s.y += s.vy; s.life -= 0.02;
|
|
387
|
-
if (s.life <= 0) { sparks.splice(i, 1); continue; }
|
|
388
|
-
ctx.fillStyle = `rgba(255,255,100,${s.life})`;
|
|
389
|
-
ctx.beginPath(); ctx.arc(s.x, s.y, 1.5, 0, 6.28); ctx.fill();
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Status icon above head
|
|
393
|
-
let icon = '';
|
|
394
|
-
if (bio.forcedOffline) icon = '\u{1F480}';
|
|
395
|
-
else if (bio.hunger === 0) icon = '\u{1F35E}\u{2757}';
|
|
396
|
-
else if (bio.hunger < 20) icon = '\u{1F35E}';
|
|
397
|
-
else if (bio.energy < 15) icon = '\u{1F4A4}';
|
|
398
|
-
else if (bio.fear > 0.5) icon = '\u{1F628}';
|
|
399
|
-
else if (bio.boredom > 0.7) icon = '\u{1F971}';
|
|
400
|
-
else if (currentActivity === 'working') icon = '\u{1F528}';
|
|
401
|
-
else if (currentActivity === 'eating') icon = '\u{1F35E}';
|
|
402
|
-
if (icon) {
|
|
403
|
-
ctx.font = '16px serif';
|
|
404
|
-
ctx.textAlign = 'center';
|
|
405
|
-
const iconY = y - bh - 12 + Math.sin(bobPhase * 2) * 2;
|
|
406
|
-
ctx.fillText(icon, x, iconY);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// === Activity & movement logic ===
|
|
411
|
-
function updateActivity() {
|
|
412
|
-
if (!state) return;
|
|
413
|
-
const bio = state.bio;
|
|
414
|
-
|
|
415
|
-
let target = 'home';
|
|
416
|
-
if (bio.forcedOffline) {
|
|
417
|
-
currentActivity = 'offline'; target = 'rest';
|
|
418
|
-
} else if (bio.energy < 15) {
|
|
419
|
-
currentActivity = 'sleeping'; target = 'rest';
|
|
420
|
-
} else if (bio.hunger < 20) {
|
|
421
|
-
currentActivity = 'seeking'; target = 'shop';
|
|
422
|
-
} else if (state.computed.sociability > 0.7 && bio.boredom > 0.5) {
|
|
423
|
-
currentActivity = 'social'; target = 'social';
|
|
424
|
-
if (Math.random() < 0.02) hearts.push({ x: agentX + (Math.random()-0.5)*10, y: agentY - 30, life: 1 });
|
|
425
|
-
} else if (bio.taskCount > 0 && bio.energy > 30 && bio.mood !== 'exhausted') {
|
|
426
|
-
currentActivity = 'working'; target = 'work';
|
|
427
|
-
} else {
|
|
428
|
-
currentActivity = 'idle'; target = 'home';
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const l = loc(target);
|
|
432
|
-
targetX = l.x;
|
|
433
|
-
targetY = l.y;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// === HUD ===
|
|
437
|
-
function updateHUD() {
|
|
438
|
-
if (!state) return;
|
|
439
|
-
const bio = state.bio;
|
|
440
|
-
|
|
441
|
-
document.getElementById('hud-name').textContent = `${state.agent} \u00B7 ${bio.mood}`;
|
|
442
|
-
|
|
443
|
-
const bars = [
|
|
444
|
-
{ icon: '\u26A1', val: bio.energy, max: 100, color: '#10b981' },
|
|
445
|
-
{ icon: '\u{1F35E}', val: bio.hunger, max: 100, color: '#f59e0b' },
|
|
446
|
-
{ icon: '\u{1F971}', val: Math.round(bio.boredom * 100), max: 100, color: '#8b5cf6' },
|
|
447
|
-
{ icon: '\u{1F628}', val: Math.round(bio.fear * 100), max: 100, color: '#ef4444' },
|
|
448
|
-
];
|
|
449
|
-
|
|
450
|
-
const el = document.getElementById('hud-bars');
|
|
451
|
-
el.innerHTML = bars.map(b => {
|
|
452
|
-
const pct = (b.val / b.max * 100);
|
|
453
|
-
return `<div class="hud-bar">
|
|
454
|
-
<span class="hud-bar-icon">${b.icon}</span>
|
|
455
|
-
<div class="hud-bar-track"><div class="hud-bar-fill" style="width:${pct}%;background:${b.color}"></div></div>
|
|
456
|
-
<span class="hud-bar-text">${b.val}</span>
|
|
457
|
-
</div>`;
|
|
458
|
-
}).join('');
|
|
459
|
-
|
|
460
|
-
// Personality tags
|
|
461
|
-
const p = bio.personality;
|
|
462
|
-
const tags = [
|
|
463
|
-
{ label: p.riskWeight > 0.3 ? 'Bold' : p.riskWeight < -0.3 ? 'Cautious' : 'Balanced', lvl: Math.abs(p.riskWeight) > 0.3 ? 'high' : 'mid' },
|
|
464
|
-
{ label: p.patience > 0.6 ? 'Patient' : p.patience < 0.3 ? 'Hasty' : '', lvl: 'high' },
|
|
465
|
-
{ label: p.socialWeight > 0.6 ? 'Social' : p.socialWeight < 0.3 ? 'Loner' : '', lvl: 'high' },
|
|
466
|
-
].filter(t => t.label);
|
|
467
|
-
document.getElementById('personality-hud').innerHTML = tags.map(t =>
|
|
468
|
-
`<span class="p-tag ${t.lvl}">${t.label}</span>`
|
|
469
|
-
).join('');
|
|
470
|
-
|
|
471
|
-
// Revive
|
|
472
|
-
document.getElementById('revive-overlay').classList.toggle('show', !!bio.forcedOffline);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// === Event detection ===
|
|
476
|
-
function checkNewEvents(events) {
|
|
477
|
-
if (!events || events.length === 0) return;
|
|
478
|
-
const newEvt = events[0];
|
|
479
|
-
if (prevEvents.length === 0 || prevEvents[0]?.ts !== newEvt.ts) {
|
|
480
|
-
showToast(newEvt);
|
|
481
|
-
// Spawn particles
|
|
482
|
-
if (newEvt.trigger === 'fear') {
|
|
483
|
-
for (let i = 0; i < 8; i++) sparks.push({
|
|
484
|
-
x: agentX, y: agentY - 10,
|
|
485
|
-
vx: (Math.random() - 0.5) * 3, vy: -Math.random() * 2 - 1, life: 1,
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
if (newEvt.action === 'buy_food' || newEvt.action === 'auto_buy' || newEvt.action === 'feed') {
|
|
489
|
-
currentActivity = 'eating';
|
|
490
|
-
const l = loc('shop'); targetX = l.x; targetY = l.y;
|
|
491
|
-
}
|
|
492
|
-
if (newEvt.trigger === 'social') {
|
|
493
|
-
for (let i = 0; i < 3; i++) hearts.push({ x: agentX + (Math.random()-0.5)*20, y: agentY - 30, life: 1 });
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
prevEvents = events;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const ICONS = { hunger:'\u{1F35E}', fear:'\u{1F628}', boredom:'\u{1F971}', exhaustion:'\u{1F634}', social:'\u{1F44B}', token_limit:'\u{26A1}', revive:'\u{2728}' };
|
|
500
|
-
|
|
501
|
-
function showToast(evt) {
|
|
502
|
-
const container = document.getElementById('toasts');
|
|
503
|
-
const el = document.createElement('div');
|
|
504
|
-
el.className = 'toast';
|
|
505
|
-
el.textContent = `${ICONS[evt.trigger]||''} ${evt.reason}`;
|
|
506
|
-
container.appendChild(el);
|
|
507
|
-
setTimeout(() => el.remove(), 4500);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// === Fetch ===
|
|
511
|
-
async function fetchState() {
|
|
512
|
-
try {
|
|
513
|
-
const res = await fetch('/self/state');
|
|
514
|
-
if (!res.ok) return;
|
|
515
|
-
const data = await res.json();
|
|
516
|
-
|
|
517
|
-
const isFirst = !state;
|
|
518
|
-
state = data;
|
|
519
|
-
|
|
520
|
-
if (isFirst) {
|
|
521
|
-
const h = loc('home');
|
|
522
|
-
agentX = h.x; agentY = h.y;
|
|
523
|
-
targetX = h.x; targetY = h.y;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
checkNewEvents(data.bioEvents);
|
|
527
|
-
updateHUD();
|
|
528
|
-
updateActivity();
|
|
529
|
-
} catch {}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
async function revive() {
|
|
533
|
-
await fetch('/self/revive', { method: 'POST' });
|
|
534
|
-
document.getElementById('revive-overlay').classList.remove('show');
|
|
535
|
-
showToast({ trigger: 'revive', reason: 'Agent revived!' });
|
|
536
|
-
setTimeout(fetchState, 500);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// === Main loop ===
|
|
540
|
-
function frame() {
|
|
541
|
-
drawScene();
|
|
542
|
-
drawAgent();
|
|
543
|
-
requestAnimationFrame(frame);
|
|
544
|
-
}
|
|
545
|
-
frame();
|
|
546
|
-
|
|
547
|
-
fetchState();
|
|
548
|
-
setInterval(fetchState, 3000);
|
|
549
|
-
setInterval(updateActivity, 2000);
|
|
550
|
-
</script>
|
|
551
|
-
</body>
|
|
552
|
-
</html>
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { EngineQueue } from "./engine-queue.js";
|
|
4
|
-
// Helpers
|
|
5
|
-
const tick = () => new Promise((r) => setImmediate(r));
|
|
6
|
-
async function sleep(ms) {
|
|
7
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
8
|
-
}
|
|
9
|
-
describe("EngineQueue", () => {
|
|
10
|
-
it("free slot: acquire resolves immediately and isBusy becomes true", async () => {
|
|
11
|
-
const q = new EngineQueue();
|
|
12
|
-
assert.equal(q.isBusy(), false);
|
|
13
|
-
await q.acquire("high", 1000);
|
|
14
|
-
assert.equal(q.isBusy(), true);
|
|
15
|
-
q.release();
|
|
16
|
-
assert.equal(q.isBusy(), false);
|
|
17
|
-
});
|
|
18
|
-
it("tryAcquire: succeeds when free, returns false when busy", () => {
|
|
19
|
-
const q = new EngineQueue();
|
|
20
|
-
assert.equal(q.tryAcquire(), true);
|
|
21
|
-
assert.equal(q.isBusy(), true);
|
|
22
|
-
assert.equal(q.tryAcquire(), false);
|
|
23
|
-
q.release();
|
|
24
|
-
});
|
|
25
|
-
it("priority ordering: high waiter beats normal when slot is released", async () => {
|
|
26
|
-
const q = new EngineQueue();
|
|
27
|
-
await q.acquire("high", 1000); // take the slot
|
|
28
|
-
const order = [];
|
|
29
|
-
const p1 = q.acquire("normal", 2000).then(() => { order.push("normal"); q.release(); });
|
|
30
|
-
await tick();
|
|
31
|
-
const p2 = q.acquire("high", 2000).then(() => { order.push("high"); q.release(); });
|
|
32
|
-
await tick();
|
|
33
|
-
assert.equal(q.queueDepth(), 2);
|
|
34
|
-
q.release(); // hand off to highest-priority waiter
|
|
35
|
-
await Promise.all([p1, p2]);
|
|
36
|
-
assert.deepEqual(order, ["high", "normal"]);
|
|
37
|
-
});
|
|
38
|
-
it("FIFO within same priority: earlier enqueuer wins", async () => {
|
|
39
|
-
const q = new EngineQueue();
|
|
40
|
-
await q.acquire("high", 1000);
|
|
41
|
-
const order = [];
|
|
42
|
-
const p1 = q.acquire("normal", 2000).then(() => { order.push("first"); q.release(); });
|
|
43
|
-
await sleep(5); // ensure different enqueuedAt timestamps
|
|
44
|
-
const p2 = q.acquire("normal", 2000).then(() => { order.push("second"); q.release(); });
|
|
45
|
-
await tick();
|
|
46
|
-
q.release();
|
|
47
|
-
await Promise.all([p1, p2]);
|
|
48
|
-
assert.deepEqual(order, ["first", "second"]);
|
|
49
|
-
});
|
|
50
|
-
it("deadline timeout: waiter is removed and rejects with busy-timeout error", async () => {
|
|
51
|
-
const q = new EngineQueue();
|
|
52
|
-
await q.acquire("high", 1000); // hold the slot
|
|
53
|
-
let caught = null;
|
|
54
|
-
const p = q.acquire("low", 30).catch((e) => { caught = e; });
|
|
55
|
-
await sleep(60); // let the 30ms deadline fire
|
|
56
|
-
assert.equal(q.queueDepth(), 0, "waiter must be removed after timeout");
|
|
57
|
-
await p;
|
|
58
|
-
assert.ok(caught !== null && typeof caught === "object", "should have rejected with an Error");
|
|
59
|
-
const msg = caught.message;
|
|
60
|
-
assert.ok(msg.includes("Engine busy timeout"), msg);
|
|
61
|
-
q.release();
|
|
62
|
-
});
|
|
63
|
-
it("release with no waiters makes slot free", () => {
|
|
64
|
-
const q = new EngineQueue();
|
|
65
|
-
assert.equal(q.tryAcquire(), true);
|
|
66
|
-
q.release();
|
|
67
|
-
assert.equal(q.isBusy(), false);
|
|
68
|
-
assert.equal(q.heldMs(), 0);
|
|
69
|
-
});
|
|
70
|
-
it("queueDepth tracks waiters correctly", async () => {
|
|
71
|
-
const q = new EngineQueue();
|
|
72
|
-
await q.acquire("high", 1000);
|
|
73
|
-
assert.equal(q.queueDepth(), 0);
|
|
74
|
-
const p1 = q.acquire("normal", 2000);
|
|
75
|
-
await tick();
|
|
76
|
-
assert.equal(q.queueDepth(), 1);
|
|
77
|
-
const p2 = q.acquire("low", 2000);
|
|
78
|
-
await tick();
|
|
79
|
-
assert.equal(q.queueDepth(), 2);
|
|
80
|
-
q.release(); // hand to normal (higher priority)
|
|
81
|
-
await tick();
|
|
82
|
-
assert.equal(q.queueDepth(), 1);
|
|
83
|
-
const holder = await p1; // p1 resolved — release it
|
|
84
|
-
void holder; // suppress unused warning
|
|
85
|
-
q.release();
|
|
86
|
-
await p2;
|
|
87
|
-
q.release();
|
|
88
|
-
assert.equal(q.queueDepth(), 0);
|
|
89
|
-
});
|
|
90
|
-
it("heldMs: returns 0 when free, positive when busy", async () => {
|
|
91
|
-
const q = new EngineQueue();
|
|
92
|
-
assert.equal(q.heldMs(), 0);
|
|
93
|
-
await q.acquire("high", 1000);
|
|
94
|
-
await sleep(10);
|
|
95
|
-
assert.ok(q.heldMs() >= 10, `heldMs should be >= 10, got ${q.heldMs()}`);
|
|
96
|
-
q.release();
|
|
97
|
-
assert.equal(q.heldMs(), 0);
|
|
98
|
-
});
|
|
99
|
-
});
|