From 9f26d6ccc6d028306202bb69ff84793acaa94745 Mon Sep 17 00:00:00 2001 From: Bbeierle12 Date: Tue, 1 Jul 2025 12:23:56 -0700 Subject: [PATCH 1/2] feat: add initial genesis architect modules --- functions/src/index.ts | 40 +++++++++++++++++++ package.json | 4 +- src/components/intervention/control-panel.tsx | 38 ++++++++++++++++++ src/types/ecosys.ts | 17 ++++++++ 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 functions/src/index.ts create mode 100644 src/components/intervention/control-panel.tsx create mode 100644 src/types/ecosys.ts diff --git a/functions/src/index.ts b/functions/src/index.ts new file mode 100644 index 0000000..156d403 --- /dev/null +++ b/functions/src/index.ts @@ -0,0 +1,40 @@ +import * as functions from 'firebase-functions' +import * as admin from 'firebase-admin' + +admin.initializeApp() +const db = admin.firestore() + +export const calculatePlayerRewards = functions.firestore + .document('simulations/{simId}') + .onUpdate(async (change, context) => { + const beforeData = change.before.data() || {} + const afterData = change.after.data() || {} + + const noveltyEventsBefore = beforeData.majorNoveltyCount || 0 + const noveltyEventsAfter = afterData.majorNoveltyCount || 0 + + if (noveltyEventsAfter > noveltyEventsBefore) { + const logs = await db + .collection('intervention_logs') + .where('simulationId', '==', context.params.simId) + .where('timestamp', '<', afterData.lastNoveltyTimestamp) + .orderBy('timestamp', 'desc') + .limit(1) + .get() + + if (!logs.empty) { + const interventionData = logs.docs[0].data() + const playerId = interventionData.playerId + + const points = 1000 + const playerRef = db.collection('players').doc(playerId) + + await db.runTransaction(async tx => { + const playerDoc = await tx.get(playerRef) + const newScore = (playerDoc.data()?.score || 0) + points + tx.update(playerRef, { score: newScore }) + }) + } + } + return null + }) diff --git a/package.json b/package.json index 7d94604..3dae06f 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,9 @@ "tailwindcss-animate": "^1.0.7", "three": "0.168.0", "xlsx": "^0.18.5", - "zod": "^3.24.2" + "zod": "^3.24.2", + "firebase-admin": "^12.0.0", + "firebase-functions": "^4.4.1" }, "devDependencies": { "@types/js-yaml": "^4.0.9", diff --git a/src/components/intervention/control-panel.tsx b/src/components/intervention/control-panel.tsx new file mode 100644 index 0000000..4bc207c --- /dev/null +++ b/src/components/intervention/control-panel.tsx @@ -0,0 +1,38 @@ +'use client' + +import React from 'react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' + +interface ControlPanelProps { + onTriggerCatastrophe?: () => void + onAlterEnvironment?: () => void + onSeedLife?: () => void +} + +const ControlPanel: React.FC = ({ + onTriggerCatastrophe, + onAlterEnvironment, + onSeedLife, +}) => { + return ( + + + Interventions + + + + + + + + ) +} + +export default ControlPanel diff --git a/src/types/ecosys.ts b/src/types/ecosys.ts new file mode 100644 index 0000000..c900980 --- /dev/null +++ b/src/types/ecosys.ts @@ -0,0 +1,17 @@ +export interface Player { + id: string + score: number +} + +export interface Simulation { + majorNoveltyCount?: number + lastNoveltyTimestamp?: number +} + +export interface InterventionLog { + playerId: string + simulationId: string + timestamp: number + actionType: string + parameters?: Record +} From b8bcc104703698a587b53cb6ca832820110527b0 Mon Sep 17 00:00:00 2001 From: Bbeierle12 Date: Tue, 1 Jul 2025 12:51:48 -0700 Subject: [PATCH 2/2] Add core Firebase simulation skeleton --- docs/firestore_schema.md | 28 +++++++++ firestore.rules | 9 +++ functions/index.js | 130 +++++++++++++++++++++++++++++++++++++++ public/index.html | 12 ++++ public/main.js | 44 +++++++++++++ public/modules/Agent.js | 27 ++++++++ public/modules/Genome.js | 29 +++++++++ 7 files changed, 279 insertions(+) create mode 100644 docs/firestore_schema.md create mode 100644 firestore.rules create mode 100644 functions/index.js create mode 100644 public/index.html create mode 100644 public/main.js create mode 100644 public/modules/Agent.js create mode 100644 public/modules/Genome.js diff --git a/docs/firestore_schema.md b/docs/firestore_schema.md new file mode 100644 index 0000000..12fb0aa --- /dev/null +++ b/docs/firestore_schema.md @@ -0,0 +1,28 @@ +# EcosysX Firestore Schema + +## collection: `simulations` +- **docId:** `world-01` +- **fields:** + - `tickCount`: Number + - `isRunning`: Boolean + - `worldSize`: Map `{ width: Number, height: Number }` + - `config`: Map (holds parameters like mutation rates, energy costs, etc.) + +## collection: `agents` +- **docId:** `[agentId]` +- **fields:** + - `simulationId`: String + - `speciesType`: String ("HERBIVORE" or "CARNIVORE") + - `isAlive`: Boolean + - `age`: Number + - `energy`: Number + - `location`: GeoPoint `{ x: Number, y: Number }` + - `state`: String (e.g., "FORAGING", "FLEEING", "SEEKING_MATE") + - `genome`: Map `{ genes: Array }` + - `phenotype`: Map `{ size: Number, color: String, speed: Number, sensoryRange: Number, isPredator: Boolean }` + +## collection: `environment` +- **docId:** `world-01-terrain` +- **fields:** + - `grid`: Array (A flattened 2D array representing terrain cost per cell) + - `dimensions`: Map `{ width: Number, height: Number }` diff --git a/firestore.rules b/firestore.rules new file mode 100644 index 0000000..69aeb65 --- /dev/null +++ b/firestore.rules @@ -0,0 +1,9 @@ +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read: if true; + allow write: if request.auth != null; + } + } +} diff --git a/functions/index.js b/functions/index.js new file mode 100644 index 0000000..4eecfa3 --- /dev/null +++ b/functions/index.js @@ -0,0 +1,130 @@ +const functions = require('firebase-functions'); +const admin = require('firebase-admin'); +admin.initializeApp(); +const db = admin.firestore(); + +const agentStates = { + FORAGING: (agent, world) => { + const nearestPredator = findNearest(agent, world.agents.filter(a => a.phenotype.isPredator), agent.phenotype.sensoryRange); + if (nearestPredator) { + agent.state = 'FLEEING'; + return; + } + const target = findNearest(agent, world.resources, agent.phenotype.sensoryRange); + moveTowards(agent, target, agent.phenotype.speed, world.worldSize); + }, + FLEEING: (agent, world) => { + const nearestPredator = findNearest(agent, world.agents.filter(a => a.phenotype.isPredator), agent.phenotype.sensoryRange); + if (nearestPredator) { + moveAwayFrom(agent, nearestPredator, agent.phenotype.speed, world.worldSize); + } else { + agent.state = 'FORAGING'; + } + }, + HUNTING: (agent, world) => { + const target = findNearest(agent, world.agents.filter(a => !a.phenotype.isPredator), agent.phenotype.sensoryRange); + moveTowards(agent, target, agent.phenotype.speed, world.worldSize); + } +}; + +function findNearest(agent, targets, range) { + let minDist = Infinity; + let nearest = null; + for (const t of targets) { + const dist = Math.hypot(agent.location.x - t.location.x, agent.location.y - t.location.y); + if (dist < minDist && dist <= range) { + minDist = dist; + nearest = t; + } + } + return nearest; +} + +function moveTowards(agent, target, speed, worldSize) { + if (!target) return; + const dx = target.location.x - agent.location.x; + const dy = target.location.y - agent.location.y; + const len = Math.hypot(dx, dy) || 1; + agent.location.x = Math.min(worldSize.width, Math.max(0, agent.location.x + (dx / len) * speed)); + agent.location.y = Math.min(worldSize.height, Math.max(0, agent.location.y + (dy / len) * speed)); +} + +function moveAwayFrom(agent, threat, speed, worldSize) { + if (!threat) return; + const dx = agent.location.x - threat.location.x; + const dy = agent.location.y - threat.location.y; + const len = Math.hypot(dx, dy) || 1; + agent.location.x = Math.min(worldSize.width, Math.max(0, agent.location.x + (dx / len) * speed)); + agent.location.y = Math.min(worldSize.height, Math.max(0, agent.location.y + (dy / len) * speed)); +} + +exports.simulationTick = functions.runWith({ timeoutSeconds: 300, memory: '1GB' }).pubsub.schedule('every 1 minutes').onRun(async () => { + const simRef = db.collection('simulations').doc('world-01'); + const simDoc = await simRef.get(); + if (!simDoc.exists || !simDoc.data().isRunning) return null; + + const config = simDoc.data().config; + const agentDocs = await db.collection('agents').get(); + const resourceDocs = await db.collection('resources').get(); + + const world = { + agents: agentDocs.docs.map(doc => ({ id: doc.id, ...doc.data() })), + resources: resourceDocs.docs.map(doc => ({ id: doc.id, ...doc.data() })), + worldSize: config.worldSize + }; + + const batch = db.batch(); + + world.agents.forEach(agent => { + if (!agent.isAlive) return; + + agent.energy -= (config.metabolicCostBase + (agent.phenotype.speed * config.metabolicCostSpeedFactor)); + agent.age += 1; + + if (agent.energy <= 0 || agent.age > agent.phenotype.maxLifespan) { + agent.isAlive = false; + batch.delete(db.collection('agents').doc(agent.id)); + return; + } + + const stateLogic = agent.speciesType === 'CARNIVORE' ? agentStates.HUNTING : agentStates[agent.state]; + if (stateLogic) stateLogic(agent, world); + + if (agent.speciesType === 'HERBIVORE') { + const eatenIndex = world.resources.findIndex(r => Math.hypot(agent.location.x - r.location.x, agent.location.y - r.location.y) < agent.phenotype.size); + if (eatenIndex > -1) { + agent.energy += world.resources[eatenIndex].energyValue; + batch.delete(db.collection('resources').doc(world.resources[eatenIndex].id)); + world.resources.splice(eatenIndex, 1); + } + } else { + const preyIndex = world.agents.findIndex(p => p.isAlive && p.speciesType === 'HERBIVORE' && Math.hypot(agent.location.x - p.location.x, agent.location.y - p.location.y) < agent.phenotype.size); + if (preyIndex > -1) { + agent.energy += world.agents[preyIndex].energy; + world.agents[preyIndex].isAlive = false; + batch.delete(db.collection('agents').doc(world.agents[preyIndex].id)); + } + } + + if (agent.energy > config.reproEnergyCost) { + agent.energy -= config.reproEnergyCost; + // placeholder for reproduction logic + } + + batch.update(db.collection('agents').doc(agent.id), { location: agent.location, energy: agent.energy, age: agent.age, state: agent.state }); + }); + + if (Math.random() < config.foodReplenishRate && world.resources.length < config.maxResources) { + const newResId = db.collection('resources').doc().id; + batch.set(db.collection('resources').doc(newResId), { + location: { x: Math.random() * world.worldSize.width, y: Math.random() * world.worldSize.height }, + energyValue: config.foodEnergyValue + }); + } + + batch.update(simRef, { tickCount: admin.firestore.FieldValue.increment(1), populationSize: world.agents.filter(a => a.isAlive).length }); + + await batch.commit(); + console.log('Tick complete.'); + return null; +}); diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..9ff597f --- /dev/null +++ b/public/index.html @@ -0,0 +1,12 @@ + + + + + EcosysX Real-Time Viewer + + + + + + + diff --git a/public/main.js b/public/main.js new file mode 100644 index 0000000..0d70b3b --- /dev/null +++ b/public/main.js @@ -0,0 +1,44 @@ +import { initializeApp } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-app.js"; +import { getFirestore, collection, onSnapshot } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-firestore.js"; + +const firebaseConfig = { /* YOUR FIREBASE CONFIG HERE */ }; +const app = initializeApp(firebaseConfig); +const db = getFirestore(app); + +const canvas = document.getElementById('world-canvas'); +const ctx = canvas.getContext('2d'); +canvas.width = 800; +canvas.height = 600; + +let worldState = { agents: [], resources: [] }; + +onSnapshot(collection(db, 'agents'), (snapshot) => { + worldState.agents = snapshot.docs.map(doc => doc.data()); + requestAnimationFrame(drawWorld); +}); + +onSnapshot(collection(db, 'resources'), (snapshot) => { + worldState.resources = snapshot.docs.map(doc => doc.data()); +}); + +function drawWorld() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = 'lime'; + worldState.resources.forEach(res => { + ctx.beginPath(); + ctx.arc(res.location.x, res.location.y, 3, 0, 2 * Math.PI); + ctx.fill(); + }); + + worldState.agents.forEach(agent => { + if (agent.isAlive) { + ctx.beginPath(); + ctx.arc(agent.location.x, agent.location.y, agent.phenotype.size, 0, 2 * Math.PI); + ctx.fillStyle = agent.phenotype.color; + ctx.fill(); + ctx.strokeStyle = `rgba(255, 255, 255, ${agent.energy / 500})`; + ctx.lineWidth = 2; + ctx.stroke(); + } + }); +} diff --git a/public/modules/Agent.js b/public/modules/Agent.js new file mode 100644 index 0000000..22c3d6f --- /dev/null +++ b/public/modules/Agent.js @@ -0,0 +1,27 @@ +import { Genome } from './Genome.js'; + +export class Agent { + constructor(id, config) { + this.id = id; + this.config = config; + Object.assign(this, config); + const genomeConfig = { length: config.genomeLength, h1: config.genome?.haplotype1, h2: config.genome?.haplotype2 }; + this.genome = new Genome(genomeConfig); + this.phenotype = {}; + this.decodePhenotype(); + } + + decodePhenotype() { + this.phenotype.speed = 1 + (this.genome.getEffectiveGene(0) * 4); + this.phenotype.sensoryRange = 30 + (this.genome.getEffectiveGene(1) * 120); + this.phenotype.size = this.speciesType === 'CARNIVORE' ? 7 : 5; + const r = this.speciesType === 'CARNIVORE' ? 200 : 50; + const g = Math.floor(this.genome.getEffectiveGene(3) * 150); + const b = 50; + this.phenotype.color = `rgb(${r},${g},${b})`; + } + + update(worldState) { + // Behavior handled in cloud function state machine + } +} diff --git a/public/modules/Genome.js b/public/modules/Genome.js new file mode 100644 index 0000000..5808242 --- /dev/null +++ b/public/modules/Genome.js @@ -0,0 +1,29 @@ +export class Genome { + constructor(config) { + this.haplotype1 = config.h1 || Array.from({ length: config.length }, () => Math.random()); + this.haplotype2 = config.h2 || Array.from({ length: config.length }, () => Math.random()); + this.length = config.length; + } + + getEffectiveGene(index) { + return (this.haplotype1[index] + this.haplotype2[index]) / 2; + } + + static crossover(p1, p2) { + const crossoverPoint = Math.floor(Math.random() * (p1.length - 1)) + 1; + const off1_h1 = [...p1.haplotype1.slice(0, crossoverPoint), ...p2.haplotype1.slice(crossoverPoint)]; + const off1_h2 = [...p1.haplotype2.slice(0, crossoverPoint), ...p2.haplotype2.slice(crossoverPoint)]; + return new Genome({ length: p1.length, h1: off1_h1, h2: off1_h2 }); + } + + getMutatedCopy(mutationRate) { + const mutate = (haplotype) => haplotype.map(gene => { + if (Math.random() < mutationRate) { + const mutation = (Math.random() - 0.5) * 0.1; + return Math.max(0, Math.min(1, gene + mutation)); + } + return gene; + }); + return new Genome({ length: this.length, h1: mutate(this.haplotype1), h2: mutate(this.haplotype2) }); + } +}