-
Notifications
You must be signed in to change notification settings - Fork 0
Description
/**
- ArgOS Visualization Module
- Provides a visualization layer for the reality-bending agent simulation
*/
import {
Position,
Environmental,
SensoryData,
Memory,
Goals,
Actions,
CognitiveState,
RealityFlux,
Communication,
Learning,
Social
} from './argos-framework.js';
export class ArgOSVisualizer {
/**
- Create a new visualizer for ArgOS simulation
- @param {string} canvasId - ID of the canvas element to render to
- @param {object} world - BitECS world object
- @param {number} pixelsPerUnit - Scale factor for rendering (pixels per world unit)
*/
constructor(canvasId, world, pixelsPerUnit = 5) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.world = world;
this.pixelsPerUnit = pixelsPerUnit;
this.boundarySize = 100;
// Set canvas size based on boundary
this.canvas.width = this.boundarySize * this.pixelsPerUnit;
this.canvas.height = this.boundarySize * this.pixelsPerUnit;
// Track selected entity for detailed inspection
this.selectedEntity = null;
// Animation frame request id
this.animationFrameId = null;
// Set up event listeners
this.setupEventListeners();
// Colors for different entity types
this.colors = {
agent: '#3498db', // Blue
resource: '#2ecc71', // Green
obstacle: '#7f8c8d', // Gray
hazard: '#e74c3c', // Red
selected: '#f39c12', // Orange
perception: 'rgba(52, 152, 219, 0.1)', // Light blue for perception radius
memory: 'rgba(155, 89, 182, 0.3)', // Purple for memory
communication: 'rgba(241, 196, 15, 0.3)', // Yellow for communication
realityFlux: 'rgba(155, 89, 182, 0.7)' // Vibrant purple for reality flux
};
}
/**
-
Set up mouse event listeners for entity selection
*/
setupEventListeners() {
this.canvas.addEventListener('click', (event) => {
const rect = this.canvas.getBoundingClientRect();
const x = (event.clientX - rect.left) / this.pixelsPerUnit;
const y = (event.clientY - rect.top) / this.pixelsPerUnit;// Find closest entity
let closestEntity = null;
let closestDistance = Infinity;// Check all entities with positions
for (let i = 0; i < this.world.entities.length; i++) {
const entity = i;// Skip if doesn't have position
if (!Position[entity]) continue;const entityX = Position.x[entity];
const entityY = Position.y[entity];const dx = entityX - x;
const dy = entityY - y;
const distance = Math.sqrt(dx * dx + dy * dy);// Check if this is closer than current closest
if (distance < closestDistance && distance < 3) { // Within 3 units
closestEntity = entity;
closestDistance = distance;
}
}this.selectedEntity = closestEntity;
});
}
/**
- Start rendering the simulation
*/
start() {
this.render();
}
/**
- Stop rendering the simulation
*/
stop() {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
}
/**
- Render a single frame of the simulation
*/
render() {
// Clear canvas
this.ctx.fillStyle = '#ecf0f1'; // Light background
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Draw reality flux effects
this.drawRealityFluxEffects();
// Draw all entities
this.drawEnvironmentalEntities();
this.drawAgents();
// Draw UI elements
this.drawSelectedEntityDetails();
this.drawSimulationInfo();
// Request next frame
this.animationFrameId = requestAnimationFrame(() => this.render());
}
/**
- Draw reality-bending visual effects
*/
drawRealityFluxEffects() {
// Draw a subtle grid pattern that distorts near reality flux events
this.ctx.strokeStyle = 'rgba(200, 200, 200, 0.3)';
this.ctx.lineWidth = 1;
const gridSize = 10; // Grid spacing in world units
// Find entities with active reality flux
const activeFluxEntities = [];
for (let i = 0; i < this.world.entities.length; i++) {
if (RealityFlux[i] && RealityFlux.effectType[i] !== 0) {
activeFluxEntities.push({
x: Position.x[i],
y: Position.y[i],
effect: RealityFlux.effectType[i],
duration: RealityFlux.duration[i]
});
}
}
// Draw reality wave effect if active
if (this.world.realityWave && this.world.realityWave.active) {
const wave = this.world.realityWave;
// Draw wave front
if (wave.direction === 'horizontal') {
const waveX = wave.x * this.pixelsPerUnit;
// Create a gradient for the wave
const gradient = this.ctx.createLinearGradient(
waveX - 15, 0,
waveX + 15, 0
);
gradient.addColorStop(0, 'rgba(155, 89, 182, 0)');
gradient.addColorStop(0.5, 'rgba(155, 89, 182, 0.5)');
gradient.addColorStop(1, 'rgba(155, 89, 182, 0)');
this.ctx.fillStyle = gradient;
this.ctx.fillRect(waveX - 15, 0, 30, this.canvas.height);
// Add oscillating pattern to wave
this.ctx.strokeStyle = 'rgba(155, 89, 182, 0.7)';
this.ctx.beginPath();
for (let y = 0; y < this.canvas.height; y += 10) {
const offset = Math.sin(y * wave.frequency + this.world.time * 0.1) * wave.amplitude;
if (y === 0) {
this.ctx.moveTo(waveX + offset, y);
} else {
this.ctx.lineTo(waveX + offset, y);
}
}
this.ctx.stroke();
} else {
const waveY = wave.y * this.pixelsPerUnit;
const gradient = this.ctx.createLinearGradient(
0, waveY - 15,
0, waveY + 15
);
gradient.addColorStop(0, 'rgba(155, 89, 182, 0)');
gradient.addColorStop(0.5, 'rgba(155, 89, 182, 0.5)');
gradient.addColorStop(1, 'rgba(155, 89, 182, 0)');
this.ctx.fillStyle = gradient;
this.ctx.fillRect(0, waveY - 15, this.canvas.width, 30);
// Add oscillating pattern to wave
this.ctx.strokeStyle = 'rgba(155, 89, 182, 0.7)';
this.ctx.beginPath();
for (let x = 0; x < this.canvas.width; x += 10) {
const offset = Math.sin(x * wave.frequency + this.world.time * 0.1) * wave.amplitude;
if (x === 0) {
this.ctx.moveTo(x, waveY + offset);
} else {
this.ctx.lineTo(x, waveY + offset);
}
}
this.ctx.stroke();
}
// Draw wave particles
if (wave.particles) {
this.ctx.fillStyle = 'rgba(155, 89, 182, 0.7)';
for (let i = 0; i < wave.particles.length; i++) {
const particle = wave.particles[i];
const size = particle.size * (0.5 + 0.5 * Math.sin(this.world.time * 0.2 + i * 0.5));
this.ctx.beginPath();
this.ctx.arc(
particle.x * this.pixelsPerUnit,
particle.y * this.pixelsPerUnit,
size * this.pixelsPerUnit,
0, 2 * Math.PI
);
this.ctx.fill();
}
}
}
// Draw vertical grid lines with distortion
for (let x = 0; x < this.boundarySize; x += gridSize) {
this.ctx.beginPath();
for (let y = 0; y < this.boundarySize; y += 1) {
let distortedX = x;
// Apply distortion from nearby flux entities
for (const flux of activeFluxEntities) {
const dx = x - flux.x;
const dy = y - flux.y;
const distance = Math.sqrt(dx*dx + dy*dy);
if (distance < 20) { // Effect radius
const strength = (1 - distance/20) * 3 * Math.sin(this.world.time/10);
distortedX += strength * Math.sin(y/5);
}
}
if (y === 0) {
this.ctx.moveTo(distortedX * this.pixelsPerUnit, y * this.pixelsPerUnit);
} else {
this.ctx.lineTo(distortedX * this.pixelsPerUnit, y * this.pixelsPerUnit);
}
}
this.ctx.stroke();
}
// Draw horizontal grid lines with distortion
for (let y = 0; y < this.boundarySize; y += gridSize) {
this.ctx.beginPath();
for (let x = 0; x < this.boundarySize; x += 1) {
let distortedY = y;
// Apply distortion from nearby flux entities
for (const flux of activeFluxEntities) {
const dx = x - flux.x;
const dy = y - flux.y;
const distance = Math.sqrt(dx*dx + dy*dy);
if (distance < 20) { // Effect radius
const strength = (1 - distance/20) * 3 * Math.sin(this.world.time/10);
distortedY += strength * Math.sin(x/5);
}
}
if (x === 0) {
this.ctx.moveTo(x * this.pixelsPerUnit, distortedY * this.pixelsPerUnit);
} else {
this.ctx.lineTo(x * this.pixelsPerUnit, distortedY * this.pixelsPerUnit);
}
}
this.ctx.stroke();
}
// Draw reality flux auras
for (const flux of activeFluxEntities) {
const effectRadius = 15;
const flickerIntensity = 0.7 + 0.3 * Math.sin(this.world.time/5);
this.ctx.beginPath();
this.ctx.arc(
flux.x * this.pixelsPerUnit,
flux.y * this.pixelsPerUnit,
effectRadius * this.pixelsPerUnit * flickerIntensity,
0, 2 * Math.PI
);
// Different colors for different effects
let fluxColor;
switch (flux.effect) {
case 1: // Teleport
fluxColor = 'rgba(155, 89, 182, ' + flickerIntensity/3 + ')';
break;
case 2: // Phase
fluxColor = 'rgba(41, 128, 185, ' + flickerIntensity/3 + ')';
break;
case 3: // Transform
fluxColor = 'rgba(230, 126, 34, ' + flickerIntensity/3 + ')';
break;
}
this.ctx.fillStyle = fluxColor;
this.ctx.fill();
}
}
/**
-
Draw all environmental entities (resources, obstacles, hazards)
*/
drawEnvironmentalEntities() {
for (let i = 0; i < this.world.entities.length; i++) {
// Skip if not an environmental entity
if (!Environmental[i]) continue;const x = Position.x[i] * this.pixelsPerUnit;
const y = Position.y[i] * this.pixelsPerUnit;
const type = Environmental.type[i];
const isSelected = i === this.selectedEntity;// Determine color based on type
let color;
switch (type) {
case 0: // Resource
color = this.colors.resource;
break;
case 1: // Obstacle
color = this.colors.obstacle;
break;
case 2: // Hazard
color = this.colors.hazard;
break;
default:
color = 'black';
}// Draw different shapes based on type
this.ctx.fillStyle = color;
this.ctx.strokeStyle = isSelected ? this.colors.selected : color;
this.ctx.lineWidth = isSelected ? 2 : 1;// Check for reality flux phasing effect
const isPhasing = RealityFlux[i] && RealityFlux.effectType[i] === 2;
if (isPhasing) {
this.ctx.globalAlpha = 0.3 + 0.2 * Math.sin(this.world.time/5);
}switch (type) {
case 0: // Resource (diamond)
const size = 1.5 * this.pixelsPerUnit;
this.ctx.beginPath();
this.ctx.moveTo(x, y - size);
this.ctx.lineTo(x + size, y);
this.ctx.lineTo(x, y + size);
this.ctx.lineTo(x - size, y);
this.ctx.closePath();
this.ctx.fill();
if (isSelected) this.ctx.stroke();
break;case 1: // Obstacle (square)
const obstacleSize = 2 * this.pixelsPerUnit;
this.ctx.fillRect(x - obstacleSize/2, y - obstacleSize/2, obstacleSize, obstacleSize);
if (isSelected) {
this.ctx.strokeRect(x - obstacleSize/2, y - obstacleSize/2, obstacleSize, obstacleSize);
}
break;case 2: // Hazard (triangle)
const radius = 2 * this.pixelsPerUnit;
this.ctx.beginPath();
this.ctx.moveTo(x, y - radius);
this.ctx.lineTo(x + radius * 0.866, y + radius * 0.5);
this.ctx.lineTo(x - radius * 0.866, y + radius * 0.5);
this.ctx.closePath();
this.ctx.fill();
if (isSelected) this.ctx.stroke();
break;
}// Reset opacity if it was changed
if (isPhasing) {
this.ctx.globalAlpha = 1.0;
}
}
}
/**
-
Draw all agents
*/
drawAgents() {
for (let i = 0; i < this.world.entities.length; i++) {
// Skip if not an agent (has cognitive components)
if (!SensoryData[i] || !Memory[i] || !Goals[i]) continue;const x = Position.x[i] * this.pixelsPerUnit;
const y = Position.y[i] * this.pixelsPerUnit;
const isSelected = i === this.selectedEntity;// Draw perception radius if this is the selected agent
if (isSelected) {
const perceptionRadius = SensoryData.radius[i] * this.pixelsPerUnit;
this.ctx.beginPath();
this.ctx.arc(x, y, perceptionRadius, 0, 2 * Math.PI);
this.ctx.fillStyle = this.colors.perception;
this.ctx.fill();
}// Draw agent group indicator (small ring around agent)
if (Social && Social[i]) {
const groupId = Social.groupId[i];
let groupColor;// Different colors for different groups
switch(groupId % 3) {
case 0: groupColor = 'rgba(231, 76, 60, 0.3)'; break; // Red
case 1: groupColor = 'rgba(46, 204, 113, 0.3)'; break; // Green
case 2: groupColor = 'rgba(52, 152, 219, 0.3)'; break; // Blue
}this.ctx.beginPath();
this.ctx.arc(x, y, 2.2 * this.pixelsPerUnit, 0, 2 * Math.PI);
this.ctx.fillStyle = groupColor;
this.ctx.fill();
}// Draw agent body (circle)
this.ctx.beginPath();
this.ctx.arc(x, y, 1.5 * this.pixelsPerUnit, 0, 2 * Math.PI);
this.ctx.fillStyle = this.colors.agent;
this.ctx.fill();if (isSelected) {
this.ctx.strokeStyle = this.colors.selected;
this.ctx.lineWidth = 2;
this.ctx.stroke();
}// Draw direction indicator (where the agent is headed)
if (Goals[i]) {
const targetX = Goals.targetX[i] * this.pixelsPerUnit;
const targetY = Goals.targetY[i] * this.pixelsPerUnit;// Only draw if target is not the current position
const dx = targetX - x;
const dy = targetY - y;
if (dxdx + dydy > 1) {
// Normalize direction vector
const length = Math.sqrt(dxdx + dydy);
const dirX = dx / length;
const dirY = dy / length;// Draw line indicating direction this.ctx.beginPath(); this.ctx.moveTo(x, y); this.ctx.lineTo(x + dirX * 2 * this.pixelsPerUnit, y + dirY * 2 * this.pixelsPerUnit); this.ctx.strokeStyle = isSelected ? this.colors.selected : 'rgba(52, 152, 219, 0.7)'; this.ctx.lineWidth = 1; this.ctx.stroke();}
}// Draw social connections (allies and rivals)
if (Social && Social[i] && isSelected) {
// Draw lines to allies
for (let j = 0; j < 5; j++) {
const allyId = Social.allies[i * 5 + j];
if (allyId === 0) continue;// Skip if ally doesn't have position if (!Position[allyId]) continue; const allyX = Position.x[allyId] * this.pixelsPerUnit; const allyY = Position.y[allyId] * this.pixelsPerUnit; // Draw a green dashed line to ally this.ctx.beginPath(); this.ctx.moveTo(x, y); this.ctx.lineTo(allyX, allyY); this.ctx.strokeStyle = 'rgba(46, 204, 113, 0.6)'; this.ctx.lineWidth = 1; this.ctx.setLineDash([5, 3]); this.ctx.stroke(); this.ctx.setLineDash([]);}
// Draw lines to rivals
for (let j = 0; j < 5; j++) {
const rivalId = Social.rivals[i * 5 + j];
if (rivalId === 0) continue;// Skip if rival doesn't have position if (!Position[rivalId]) continue; const rivalX = Position.x[rivalId] * this.pixelsPerUnit; const rivalY = Position.y[rivalId] * this.pixelsPerUnit; // Draw a red dashed line to rival this.ctx.beginPath(); this.ctx.moveTo(x, y); this.ctx.lineTo(rivalX, rivalY); this.ctx.strokeStyle = 'rgba(231, 76, 60, 0.6)'; this.ctx.lineWidth = 1; this.ctx.setLineDash([2, 3]); this.ctx.stroke(); this.ctx.setLineDash([]);}
}// Draw communication indicator
if (Communication[i] && Communication.sending[i] === 1) {
const commRadius = 3 * this.pixelsPerUnit;
this.ctx.beginPath();
this.ctx.arc(x, y, commRadius, 0, 2 * Math.PI);
this.ctx.strokeStyle = this.colors.communication;
this.ctx.lineWidth = 2;
this.ctx.stroke();// Draw animated communication rings for effect
const pulseSize = (1 + 0.3 * Math.sin(this.world.time * 0.2)) * commRadius;
this.ctx.beginPath();
this.ctx.arc(x, y, pulseSize, 0, 2 * Math.PI);
this.ctx.strokeStyle = 'rgba(241, 196, 15, 0.2)';
this.ctx.stroke();
}// Show state labels for learning agents
if (Learning && Learning[i]) {
// Draw small indicator of learning state
const stateColors = [
'rgba(26, 188, 156, 0.7)', // Teal
'rgba(241, 196, 15, 0.7)', // Yellow
'rgba(231, 76, 60, 0.7)' // Red
];const state = Learning.lastState[i];
const colorIndex = Math.min(2, Math.floor(state / 4));this.ctx.fillStyle = stateColors[colorIndex];
this.ctx.beginPath();
this.ctx.arc(x, y - 2.5 * this.pixelsPerUnit, 0.7 * this.pixelsPerUnit, 0, 2 * Math.PI);
this.ctx.fill();
}
}
}
/**
- Draw details about the selected entity
*/
drawSelectedEntityDetails() {
if (this.selectedEntity === null) return;
const entity = this.selectedEntity;
const margin = 10;
const lineHeight = 20;
let yPos = margin;
// Set up text style
this.ctx.font = '14px Arial';
this.ctx.fillStyle = '#333';
this.ctx.textBaseline = 'top';
// Basic entity info
this.ctx.fillText(`Entity ID: ${entity}`, margin, yPos);
yPos += lineHeight;
// Position
if (Position[entity]) {
const x = Position.x[entity].toFixed(2);
const y = Position.y[entity].toFixed(2);
this.ctx.fillText(`Position: (${x}, ${y})`, margin, yPos);
yPos += lineHeight;
}
// Environmental entity info
if (Environmental[entity]) {
const types = ['Resource', 'Obstacle', 'Hazard'];
const type = types[Environmental.type[entity]] || 'Unknown';
const value = Environmental.value[entity];
this.ctx.fillText(`Type: ${type}`, margin, yPos);
yPos += lineHeight;
this.ctx.fillText(`Value: ${value}`, margin, yPos);
yPos += lineHeight;
}
// Agent cognitive info
if (SensoryData[entity] && Memory[entity] && Goals[entity]) {
this.ctx.fillText('Cognitive Agent:', margin, yPos);
yPos += lineHeight;
// Goal info
const goalTypes = ['Explore', 'Collect', 'Avoid', 'Communicate'];
const goalType = goalTypes[Goals.primaryType[entity]] || 'Unknown';
this.ctx.fillText(`Current Goal: ${goalType} (Priority: ${Goals.priority[entity]})`, margin, yPos);
yPos += lineHeight;
// Memory info
const memories = Memory.capacity[entity];
this.ctx.fillText(`Memory Capacity: ${memories}`, margin, yPos);
yPos += lineHeight;
// Learning info
if (Learning && Learning[entity]) {
this.ctx.fillText('Learning:', margin, yPos);
yPos += lineHeight;
const learningRate = Learning.learningRate[entity].toFixed(2);
const exploration = Learning.explorationRate[entity].toFixed(2);
const rewardAccum = Learning.rewardAccumulator[entity].toFixed(1);
this.ctx.fillText(`Learning Rate: ${learningRate}`, margin + 10, yPos);
yPos += lineHeight;
this.ctx.fillText(`Exploration Rate: ${exploration}`, margin + 10, yPos);
yPos += lineHeight;
this.ctx.fillText(`Total Reward: ${rewardAccum}`, margin + 10, yPos);
yPos += lineHeight;
// Show top actions by Q-value for current state
const state = Learning.lastState[entity];
this.ctx.fillText(`Current State: ${state}`, margin + 10, yPos);
yPos += lineHeight;
// Find best action for current state
let bestAction = 0;
let bestQValue = Number.NEGATIVE_INFINITY;
for (let a = 0; a < 4; a++) {
const qValue = Learning.qValues[entity * 40 + state * 4 + a];
if (qValue > bestQValue) {
bestQValue = qValue;
bestAction = a;
}
}
const actionNames = ['Explore', 'Collect', 'Avoid', 'Communicate'];
this.ctx.fillText(`Best Action: ${actionNames[bestAction]} (Q: ${bestQValue.toFixed(1)})`, margin + 10, yPos);
yPos += lineHeight;
}
// Social info
if (Social && Social[entity]) {
this.ctx.fillText('Social Dynamics:', margin, yPos);
yPos += lineHeight;
const trustLevel = Social.trustLevel[entity];
const groupId = Social.groupId[entity];
const cooperationCount = Social.cooperationCount[entity];
const groupNames = ['Red Group', 'Green Group', 'Blue Group'];
this.ctx.fillText(`Group: ${groupNames[groupId % 3]}`, margin + 10, yPos);
yPos += lineHeight;
this.ctx.fillText(`Trust Level: ${trustLevel}`, margin + 10, yPos);
yPos += lineHeight;
this.ctx.fillText(`Cooperation Count: ${cooperationCount}`, margin + 10, yPos);
yPos += lineHeight;
// Count allies and rivals
let allyCount = 0;
let rivalCount = 0;
for (let j = 0; j < 5; j++) {
if (Social.allies[entity * 5 + j] !== 0) allyCount++;
if (Social.rivals[entity * 5 + j] !== 0) rivalCount++;
}
this.ctx.fillText(`Allies: ${allyCount}, Rivals: ${rivalCount}`, margin + 10, yPos);
yPos += lineHeight;
}
// Emotional state
if (CognitiveState[entity]) {
let emotionalState = 'Neutral';
const emotional = CognitiveState.emotionalState[entity];
if (emotional < 30) emotionalState = 'Cautious';
else if (emotional > 70) emotionalState = 'Bold';
this.ctx.fillText(`Emotional State: ${emotionalState} (${emotional}/100)`, margin, yPos);
yPos += lineHeight;
const adaptability = CognitiveState.adaptability[entity];
this.ctx.fillText(`Adaptability: ${adaptability}/100`, margin, yPos);
yPos += lineHeight;
}
}
// Reality flux info
if (RealityFlux[entity]) {
const fluxEffects = ['None', 'Teleport', 'Phase', 'Transform'];
const effectType = fluxEffects[RealityFlux.effectType[entity]] || 'Unknown';
const duration = RealityFlux.duration[entity];
const stability = RealityFlux.stability[entity];
if (RealityFlux.effectType[entity] > 0) {
this.ctx.fillStyle = '#e74c3c'; // Red for active effects
this.ctx.fillText(`Reality Effect: ${effectType} (${duration} ticks)`, margin, yPos);
} else {
this.ctx.fillText(`Reality Stability: ${stability}%`, margin, yPos);
}
yPos += lineHeight;
}
}
/**
- Draw general simulation information
*/
drawSimulationInfo() {
const margin = 10;
const lineHeight = 20;
let yPos = this.canvas.height - margin - lineHeight * 3;
// Set up text style
this.ctx.font = '14px Arial';
this.ctx.fillStyle = '#333';
this.ctx.textBaseline = 'top';
// Simulation time
this.ctx.fillText(`Simulation Time: ${this.world.time}`, margin, yPos);
yPos += lineHeight;
// Time until next reality shift
const timeUntilShift = 150 - (this.world.time % 150);
this.ctx.fillText(`Next Reality Shift: ${timeUntilShift} ticks`, margin, yPos);
yPos += lineHeight;
// Number of entities
const entityCount = this.world.entities.length;
this.ctx.fillText(`Entities: ${entityCount}`, margin, yPos);
}
}