4 Agenten, 14 Landmarks, 15 Tools, 240x240 Grid, SQLite-Persistenz. Round-Robin Turn-Manager mit Reactive Triggern, Town-Hall-Voting (70%-Threshold) mit Live-Constitution-Amendment. - engine/: db, world, agents, needs, tools, reasoning, governance, turn - web/: Canvas-basierte Live-View mit WebSocket-Stream - server.py: FastAPI + WebSocket auf 127.0.0.1:8080 - tests/: 70 Unit + Integration Tests (pytest), alle gruen - smoke_test.py: 50+ End-to-End-Checks - README: Quickstart, Architektur, Security, Tests, Lizenz - .gitignore: DB, Cache, Logs Basiert auf https://github.com/EmergenceAI/Emergence-World (Lizenz: CC-BY-NC-4.0, Research-only)
241 lines
7.5 KiB
JavaScript
241 lines
7.5 KiB
JavaScript
// Emergence-Mini live view
|
|
const canvas = document.getElementById('world');
|
|
const ctx = canvas.getContext('2d');
|
|
const GRID = 240;
|
|
let snapshot = null;
|
|
let actions = [];
|
|
const AGENT_COLORS = {
|
|
anchor: '#ffd166',
|
|
flora: '#6cf0c2',
|
|
lovely: '#ff8fb1',
|
|
spark: '#82aaff',
|
|
};
|
|
const LANDMARK_COLORS = {
|
|
town_hall: '#ff6c6c',
|
|
library: '#82aaff',
|
|
plaza: '#ff8fb1',
|
|
park: '#6cf08a',
|
|
victory_arch: '#ffd166',
|
|
police: '#cccccc',
|
|
bookworm: '#a47cff',
|
|
cafe: '#ffb066',
|
|
billboard: '#dddddd',
|
|
techhub: '#6cf0c2',
|
|
home_anchor: '#3a3a3a',
|
|
home_flora: '#3a3a3a',
|
|
home_lovely: '#3a3a3a',
|
|
home_spark: '#3a3a3a',
|
|
};
|
|
|
|
function draw() {
|
|
if (!snapshot) return;
|
|
const W = canvas.width, H = canvas.height;
|
|
ctx.fillStyle = '#060a0e';
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
// grid
|
|
ctx.strokeStyle = '#0e1620';
|
|
ctx.lineWidth = 1;
|
|
for (let i = 0; i <= GRID; i += 30) {
|
|
const px = (i / GRID) * W;
|
|
ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, H); ctx.stroke();
|
|
ctx.beginPath(); ctx.moveTo(0, px); ctx.lineTo(W, px); ctx.stroke();
|
|
}
|
|
|
|
// landmarks
|
|
for (const lm of snapshot.landmarks) {
|
|
const x = (lm.x / GRID) * W;
|
|
const y = (lm.y / GRID) * H;
|
|
const isHome = lm.id.startsWith('home_');
|
|
ctx.fillStyle = LANDMARK_COLORS[lm.id] || '#888';
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, isHome ? 5 : 7, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
if (!isHome) {
|
|
ctx.fillStyle = '#0a1018';
|
|
ctx.font = '10px ui-monospace';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(lm.name, x, y - 9);
|
|
}
|
|
}
|
|
|
|
// agents
|
|
for (const a of snapshot.agents) {
|
|
const x = (a.x / GRID) * W;
|
|
const y = (a.y / GRID) * H;
|
|
ctx.fillStyle = AGENT_COLORS[a.id] || '#fff';
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, 8, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#0a1018';
|
|
ctx.font = 'bold 10px ui-monospace';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(a.name[0], x, y + 3);
|
|
// energy halo
|
|
ctx.strokeStyle = '#ffd166';
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, 8 + (100 - a.energy) * 0.06, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
function refreshAgentCards() {
|
|
const wrap = document.getElementById('agentList');
|
|
wrap.innerHTML = '';
|
|
for (const a of snapshot.agents) {
|
|
const div = document.createElement('div');
|
|
div.className = 'agent-card';
|
|
div.innerHTML = `
|
|
<h3>${a.name} <small>· ${a.role}</small></h3>
|
|
<div>at (${a.x}, ${a.y}) · ${a.mood}</div>
|
|
<div class="bar energy"><i style="width:${a.energy}%"></i></div>
|
|
<div class="bar knowledge"><i style="width:${a.knowledge}%"></i></div>
|
|
<div class="bar influence"><i style="width:${a.influence}%"></i></div>
|
|
<div class="bar credits"><i style="width:${Math.min(100, a.credits * 5)}%"></i></div>
|
|
<small>${a.energy.toFixed(0)}E · ${a.knowledge.toFixed(0)}K · ${a.influence.toFixed(0)}I · ${a.credits.toFixed(1)} CC</small>
|
|
`;
|
|
wrap.appendChild(div);
|
|
}
|
|
}
|
|
|
|
function refreshProposals() {
|
|
const wrap = document.getElementById('proposals');
|
|
wrap.innerHTML = '';
|
|
fetch('/api/proposals').then(r => r.json()).then(d => {
|
|
if (!d.active.length) {
|
|
wrap.innerHTML = '<small>No active proposals.</small>';
|
|
return;
|
|
}
|
|
for (const p of d.active) {
|
|
const el = document.createElement('div');
|
|
el.className = 'proposal';
|
|
el.innerHTML = `<h4>#${p.id} ${p.title}</h4>
|
|
<small>by ${p.author} · ${p.category} · ${p.votes} votes</small>
|
|
<p>${p.body}</p>`;
|
|
wrap.appendChild(el);
|
|
}
|
|
});
|
|
}
|
|
|
|
function refreshConstitution() {
|
|
const wrap = document.getElementById('constitution');
|
|
wrap.innerHTML = '';
|
|
fetch('/api/constitution').then(r => r.json()).then(c => {
|
|
for (const a of c.articles) {
|
|
const el = document.createElement('div');
|
|
el.className = 'article';
|
|
el.innerHTML = `<b>Article ${a.id} — ${a.title}</b><p>${a.body}</p>`;
|
|
wrap.appendChild(el);
|
|
}
|
|
});
|
|
}
|
|
|
|
function addFeed(entry) {
|
|
const ul = document.getElementById('feed');
|
|
const li = document.createElement('li');
|
|
const t = new Date().toLocaleTimeString();
|
|
if (entry.type === 'action') {
|
|
li.innerHTML = `<span class="who">${entry.name}</span>
|
|
<span class="tool">${entry.tool}</span>
|
|
<span class="why">— ${entry.rationale || ''}</span>
|
|
<small> · ${t}</small>`;
|
|
} else {
|
|
li.innerHTML = `<small>${entry.type} · ${t}</small>`;
|
|
}
|
|
ul.prepend(li);
|
|
while (ul.children.length > 80) ul.removeChild(ul.lastChild);
|
|
}
|
|
|
|
function refreshHeader() {
|
|
document.getElementById('tick').textContent = snapshot?.tick || 0;
|
|
document.getElementById('agentCount').textContent = snapshot?.agents.length || 0;
|
|
}
|
|
|
|
async function refreshAll() {
|
|
const r = await fetch('/api/state');
|
|
snapshot = await r.json();
|
|
draw();
|
|
refreshHeader();
|
|
refreshAgentCards();
|
|
refreshProposals();
|
|
refreshConstitution();
|
|
}
|
|
|
|
function connectWS() {
|
|
const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws');
|
|
const status = document.getElementById('wsStatus');
|
|
ws.onopen = () => { status.textContent = 'live'; status.className = 'ws-status connected'; };
|
|
ws.onclose = () => {
|
|
status.textContent = 'disconnected — retrying'; status.className = 'ws-status disconnected';
|
|
setTimeout(connectWS, 2000);
|
|
};
|
|
ws.onmessage = (ev) => {
|
|
const msg = JSON.parse(ev.data);
|
|
if (msg.type === 'snapshot') {
|
|
snapshot = msg.data;
|
|
draw(); refreshHeader(); refreshAgentCards();
|
|
} else if (msg.type === 'action') {
|
|
// update local agent snapshot
|
|
if (snapshot) {
|
|
const a = snapshot.agents.find(x => x.id === msg.agent);
|
|
if (a) {
|
|
a.x = msg.x; a.y = msg.y;
|
|
a.energy = msg.energy; a.knowledge = msg.knowledge;
|
|
a.influence = msg.influence; a.credits = msg.credits;
|
|
a.mood = msg.mood;
|
|
}
|
|
draw(); refreshAgentCards();
|
|
}
|
|
addFeed(msg);
|
|
// refresh proposals / constitution occasionally
|
|
if (msg.tool && msg.tool.startsWith('vote_on') || msg.tool === 'submit_townhall_proposal') {
|
|
refreshProposals();
|
|
}
|
|
if (msg.tool === 'submit_townhall_proposal') {
|
|
setTimeout(refreshConstitution, 500);
|
|
}
|
|
} else if (msg.type === 'tick') {
|
|
document.getElementById('tick').textContent = msg.tick;
|
|
}
|
|
};
|
|
}
|
|
|
|
async function populateManual() {
|
|
const a = await (await fetch('/api/agents')).json();
|
|
const selA = document.querySelector('#manual select[name=agent]');
|
|
for (const ag of a) {
|
|
const o = document.createElement('option');
|
|
o.value = ag.id; o.textContent = ag.name; selA.appendChild(o);
|
|
}
|
|
const tools = [
|
|
'go_to_place','go_home','say_to_agent','speak_to_all',
|
|
'show_emoticon','recharge_energy','add_to_longterm_memory',
|
|
'write_blog','add_to_billboard','submit_townhall_proposal',
|
|
'vote_on_proposal','idle',
|
|
];
|
|
const selT = document.querySelector('#manual select[name=tool]');
|
|
for (const t of tools) {
|
|
const o = document.createElement('option');
|
|
o.value = t; o.textContent = t; selT.appendChild(o);
|
|
}
|
|
}
|
|
|
|
document.getElementById('manual').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const f = e.target;
|
|
let args = {};
|
|
try { args = JSON.parse(f.args.value || '{}'); } catch { alert('args must be JSON'); return; }
|
|
const body = { tool: f.tool.value, args };
|
|
const r = await fetch(`/api/turn/${f.agent.value}`, {
|
|
method: 'POST', headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(body),
|
|
});
|
|
const out = await r.json();
|
|
console.log('manual turn result', out);
|
|
});
|
|
|
|
refreshAll();
|
|
populateManual();
|
|
connectWS();
|
|
setInterval(refreshProposals, 5000);
|