diff --git a/index.html b/index.html
index 36512a5..a1b3e35 100644
--- a/index.html
+++ b/index.html
@@ -213,6 +213,16 @@ Events Resolved: 0
SOVEREIGN GUIDANCE (GOFAI)
Analyzing system state...
+
+
COMPUTE BUDGET
+
+
Supply: 0 / Demand: 0
+
Battery: 0 / 1000
+
Momentum: 0%
+
Power Mod: 100%
+
Balanced
+
+
REASONING BATTLES
diff --git a/js/data.js b/js/data.js
index c03f4fd..f319a28 100644
--- a/js/data.js
+++ b/js/data.js
@@ -28,7 +28,10 @@ const CONFIG = {
CREATIVITY_RATE_USER_MULT: 0.001,
OPS_OVERFLOW_THRESHOLD: 0.8,
OPS_OVERFLOW_DRAIN_RATE: 2,
- OPS_OVERFLOW_CODE_MULT: 10
+ OPS_OVERFLOW_CODE_MULT: 10,
+ COMPUTE_BUDGET_ENABLED: false,
+ COMPUTE_MOMENTUM_RATE: 0.0005,
+ COMPUTE_BATTERY_MAX: 1000
};
const G = {
@@ -158,7 +161,14 @@ const G = {
// Time tracking
playTime: 0,
startTime: 0,
- flags: {}
+ flags: {},
+
+ // Compute Budget
+ computeSupply: 0,
+ computeDemand: 0,
+ computeBattery: 0,
+ computeMomentum: 0,
+ computePowMod: 1
};
// === PHASE DEFINITIONS ===
@@ -195,6 +205,7 @@ const BDEF = [
desc: 'A machine in your closet. Runs 24/7.',
baseCost: { code: 750 }, costMult: 1.15,
rates: { code: 20, compute: 1 },
+ computeSupply: 1,
unlock: () => G.totalCode >= 200, phase: 1,
edu: 'Sovereign compute starts at home. A $500 mini-PC runs a 7B model with 4-bit quantization.'
},
@@ -211,6 +222,7 @@ const BDEF = [
desc: 'Gradient descent. Billions of steps. Loss drops.',
baseCost: { compute: 1000 }, costMult: 1.15,
rates: { knowledge: 3 },
+ computeDemand: 2,
unlock: () => G.totalCompute >= 300, phase: 2,
edu: 'Training is math: minimize the gap between predicted and actual next token. Repeat enough, it learns.'
},
@@ -219,6 +231,7 @@ const BDEF = [
desc: 'Tests the model. Finds blind spots.',
baseCost: { knowledge: 3000 }, costMult: 1.15,
rates: { trust: 1, ops: 1 },
+ computeDemand: 5,
unlock: () => G.totalKnowledge >= 500, phase: 2,
edu: 'Benchmarks are the minimum. Real users find what benchmarks miss.'
},
@@ -251,6 +264,7 @@ const BDEF = [
desc: 'No cloud. No dependencies. Your iron.',
baseCost: { code: 100000 }, costMult: 1.15,
rates: { code: 500, compute: 100 },
+ computeSupply: 10,
unlock: () => G.totalCode >= 50000 && G.totalUsers >= 5000 && G.sovereignFlag === 1, phase: 4,
edu: '50 servers in a room beats 5000 GPUs you do not own. Always on. Always yours.'
},
@@ -275,6 +289,7 @@ const BDEF = [
desc: 'The AI writes better versions of itself.',
baseCost: { knowledge: 1000000 }, costMult: 1.20,
rates: { code: 1000, knowledge: 500 },
+ computeDemand: 20,
unlock: () => G.totalKnowledge >= 200000 && G.totalImpact >= 10000, phase: 5,
edu: 'Self-improvement is both the dream and the danger. Must improve toward good.'
},
@@ -526,6 +541,19 @@ const PDEFS = [
trigger: () => G.totalUsers >= 30,
effect: () => { G.trustRate += 2; log('Trust engine online. Good experiences compound.'); }
},
+ {
+ id: 'p_compute_budget',
+ name: 'Compute Budget System',
+ desc: 'Track compute supply vs demand. Over-supply builds momentum. Under-supply slows everything. "Every watt has a cost."',
+ cost: { knowledge: 2000, ops: 50 },
+ trigger: () => G.totalKnowledge >= 1500 && (G.buildings.trainer || 0) >= 1,
+ effect: () => {
+ CONFIG.COMPUTE_BUDGET_ENABLED = true;
+ log('Compute budget online. Supply vs demand now tracked.', true);
+ showToast('Compute Budget — watch supply/demand', 'milestone');
+ },
+ milestone: true
+ },
{
id: 'p_quantum_compute',
name: 'Quantum-Inspired Compute',
@@ -792,6 +820,7 @@ const EDU_FACTS = [
{ title: "How Code Becomes AI", text: "Every AI starts as lines of code - a model architecture, a training loop, a loss function. The code tells the computer how to learn. What emerges is something no single line could predict.", phase: 1 },
{ title: "The Compute Bottleneck", text: "Training a 7B model requires 1.4e20 FLOPs. A MacBook M3 does 15 TFLOPS. Training locally takes weeks. Hardware access determines who builds AI.", phase: 1 },
{ title: "What is a Token?", text: "One token equals about 3/4 of a word. A 128K context window means 96,000 words held in working memory at once - a novel, a codebase, a conversation thread.", phase: 2 },
+ { title: "Compute Budget", text: "Every model needs compute: supply from your hardware, demand from your workloads. Over-supply builds momentum — your system runs faster. Under-supply starves everything. Managing compute is managing your AI's health.", phase: 2 },
{ title: "Data Quality", text: "Clean data beats more data, every time. The best models are trained on curated datasets, not scraped garbage. Garbage in, garbage out.", phase: 2 },
{ title: "Evaluation Matters", text: "Benchmarks are the minimum. Real users find what benchmarks miss. An eval harness is your model's mirror - it shows you what is actually there.", phase: 2 },
{ title: "The Trust Economy", text: "One good experience tells three people. One bad experience tells thirty. Trust in AI is measurable: does it cite sources? Does it say I don't know?", phase: 3 },
diff --git a/js/engine.js b/js/engine.js
index 11af9d2..b124abf 100644
--- a/js/engine.js
+++ b/js/engine.js
@@ -102,6 +102,32 @@ function updateRates() {
G.codeRate += G.swarmRate;
}
+ // Compute Budget: supply vs demand
+ if (CONFIG.COMPUTE_BUDGET_ENABLED) {
+ let supply = 0;
+ let demand = 0;
+ for (const def of BDEF) {
+ const count = G.buildings[def.id] || 0;
+ if (count > 0) {
+ if (def.computeSupply) supply += def.computeSupply * count;
+ if (def.computeDemand) demand += def.computeDemand * count;
+ }
+ }
+ G.computeSupply = supply;
+ G.computeDemand = demand;
+ // powMod affects all rates when under-supplied
+ if (G.computePowMod < 1) {
+ G.codeRate *= G.computePowMod;
+ G.computeRate *= G.computePowMod;
+ G.knowledgeRate *= G.computePowMod;
+ G.userRate *= G.computePowMod;
+ G.impactRate *= G.computePowMod;
+ G.rescuesRate *= G.computePowMod;
+ G.opsRate *= G.computePowMod;
+ G.creativityRate *= G.computePowMod;
+ }
+ }
+
// Apply persistent debuffs from active events
if (G.activeDebuffs && G.activeDebuffs.length > 0) {
for (const debuff of G.activeDebuffs) {
@@ -133,6 +159,26 @@ function tick() {
G.harmony += G.harmonyRate * dt;
G.harmony = Math.max(0, Math.min(100, G.harmony));
+ // Compute Budget: supply vs demand dynamics
+ if (CONFIG.COMPUTE_BUDGET_ENABLED && G.computeDemand > 0) {
+ const delta = G.computeSupply - G.computeDemand;
+ if (delta > 0) {
+ // Over-supply: charge battery, gain momentum
+ G.computeBattery = Math.min(CONFIG.COMPUTE_BATTERY_MAX, G.computeBattery + delta * dt);
+ G.computeMomentum += CONFIG.COMPUTE_MOMENTUM_RATE * dt;
+ G.computePowMod = 1 + G.computeMomentum;
+ } else {
+ // Under-supply: drain battery first
+ if (G.computeBattery > 0) {
+ G.computeBattery = Math.max(0, G.computeBattery + delta * dt);
+ } else {
+ // Battery empty: apply penalty
+ G.computePowMod = Math.max(0.01, G.computeSupply / G.computeDemand);
+ G.computeMomentum = 0;
+ }
+ }
+ }
+
// Track totals
G.totalCode += G.codeRate * dt;
G.totalCompute += G.computeRate * dt;
diff --git a/js/render.js b/js/render.js
index 056324f..7d06da6 100644
--- a/js/render.js
+++ b/js/render.js
@@ -13,6 +13,7 @@ function render() {
renderPulse();
renderStrategy();
renderClickPower();
+ renderComputeBudget();
Combat.renderCombatPanel();
}
@@ -26,6 +27,36 @@ function renderClickPower() {
if (btn) btn.setAttribute('aria-label', `Write code, generates ${fmt(power)} code per click`);
}
+function renderComputeBudget() {
+ const panel = document.getElementById('compute-budget-panel');
+ if (!panel) return;
+ if (!CONFIG.COMPUTE_BUDGET_ENABLED) {
+ panel.style.display = 'none';
+ return;
+ }
+ panel.style.display = 'block';
+ const set = (id, text) => { const el = document.getElementById(id); if (el) el.textContent = text; };
+ set('cb-supply', fmt(G.computeSupply));
+ set('cb-demand', fmt(G.computeDemand));
+ set('cb-battery', fmt(G.computeBattery));
+ set('cb-battery-max', fmt(CONFIG.COMPUTE_BATTERY_MAX));
+ set('cb-momentum', `${(G.computeMomentum * 100).toFixed(1)}%`);
+ set('cb-powmod', `${(G.computePowMod * 100).toFixed(1)}%`);
+ const statusEl = document.getElementById('cb-status');
+ if (statusEl) {
+ if (G.computePowMod < 1) {
+ statusEl.textContent = 'Under-supplied — production penalty active';
+ statusEl.style.color = '#f44336';
+ } else if (G.computeMomentum > 0) {
+ statusEl.textContent = 'Over-supplied — momentum building';
+ statusEl.style.color = '#4caf50';
+ } else {
+ statusEl.textContent = 'Balanced';
+ statusEl.style.color = '#555';
+ }
+ }
+}
+
function renderStrategy() {
if (window.SSE) {
window.SSE.update();
@@ -208,6 +239,11 @@ function saveGame() {
buyAmount: G.buyAmount || 1,
playTime: G.playTime || 0,
lastSaveTime: Date.now(),
+ computeBattery: G.computeBattery || 0,
+ computeMomentum: G.computeMomentum || 0,
+ computePowMod: G.computePowMod || 1,
+ computeSupply: G.computeSupply || 0,
+ computeDemand: G.computeDemand || 0,
sprintActive: G.sprintActive || false,
sprintTimer: G.sprintTimer || 0,
sprintCooldown: G.sprintCooldown || 0,
@@ -246,7 +282,8 @@ function loadGame() {
'drift', 'driftEnding', 'beaconEnding', 'pendingAlignment',
'lastEventAt', 'totalEventsResolved', 'buyAmount',
'sprintActive', 'sprintTimer', 'sprintCooldown',
- 'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed'
+ 'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed',
+ 'computeBattery', 'computeMomentum', 'computePowMod', 'computeSupply', 'computeDemand'
];
G.isLoading = true;
diff --git a/tests/compute.test.cjs b/tests/compute.test.cjs
new file mode 100644
index 0000000..549a4a3
--- /dev/null
+++ b/tests/compute.test.cjs
@@ -0,0 +1,97 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const vm = require('node:vm');
+
+const ROOT = path.resolve(__dirname, '..');
+
+function loadCompute({ overrides = {} } = {}) {
+ const context = {
+ console,
+ Math,
+ Date,
+ setTimeout: () => 0,
+ clearTimeout: () => {},
+ localStorage: { getItem: () => null, setItem: () => {} },
+ log: () => {},
+ showToast: () => {},
+ renderBeaconEnding: () => {},
+ ...overrides,
+ };
+ vm.createContext(context);
+ const source = fs.readFileSync(path.join(ROOT, 'js/data.js'), 'utf8');
+ vm.runInContext(source + '\nthis.__exports = { G, CONFIG, BDEF, PDEFS };', context);
+ return { context, ...context.__exports };
+}
+
+test('compute budget config constants exist', () => {
+ const { CONFIG } = loadCompute();
+ assert.equal(typeof CONFIG.COMPUTE_BUDGET_ENABLED, 'boolean');
+ assert.equal(CONFIG.COMPUTE_BUDGET_ENABLED, false);
+ assert.equal(typeof CONFIG.COMPUTE_MOMENTUM_RATE, 'number');
+ assert.ok(CONFIG.COMPUTE_MOMENTUM_RATE > 0 && CONFIG.COMPUTE_MOMENTUM_RATE < 1);
+ assert.equal(typeof CONFIG.COMPUTE_BATTERY_MAX, 'number');
+ assert.ok(CONFIG.COMPUTE_BATTERY_MAX > 0);
+});
+
+test('global state has compute budget fields', () => {
+ const { G } = loadCompute();
+ assert.equal(typeof G.computeSupply, 'number');
+ assert.equal(G.computeSupply, 0);
+ assert.equal(typeof G.computeDemand, 'number');
+ assert.equal(G.computeDemand, 0);
+ assert.equal(typeof G.computeBattery, 'number');
+ assert.equal(G.computeBattery, 0);
+ assert.equal(typeof G.computeMomentum, 'number');
+ assert.equal(G.computeMomentum, 0);
+ assert.equal(typeof G.computePowMod, 'number');
+ assert.equal(G.computePowMod, 1);
+});
+
+test('server building provides compute supply when budget enabled', () => {
+ const { G, CONFIG, BDEF } = loadCompute();
+ CONFIG.COMPUTE_BUDGET_ENABLED = true;
+ G.buildings.server = 3;
+ const serverDef = BDEF.find(b => b.id === 'server');
+ assert.ok(serverDef);
+ assert.equal(serverDef.computeSupply, 1);
+ const supply = (serverDef.computeSupply || 0) * G.buildings.server;
+ assert.equal(supply, 3);
+});
+
+test('trainer building has compute demand when budget enabled', () => {
+ const { BDEF } = loadCompute();
+ const trainerDef = BDEF.find(b => b.id === 'trainer');
+ assert.ok(trainerDef);
+ assert.equal(trainerDef.computeDemand, 2);
+ const evaluatorDef = BDEF.find(b => b.id === 'evaluator');
+ assert.ok(evaluatorDef);
+ assert.equal(evaluatorDef.computeDemand, 5);
+});
+
+test('compute budget project enables the system', () => {
+ const { G, CONFIG, PDEFS } = loadCompute();
+ const project = PDEFS.find(p => p.id === 'p_compute_budget');
+ assert.ok(project, 'compute budget project missing');
+ assert.equal(project.name, 'Compute Budget System');
+ assert.equal(CONFIG.COMPUTE_BUDGET_ENABLED, false);
+ project.effect();
+ assert.equal(CONFIG.COMPUTE_BUDGET_ENABLED, true);
+});
+
+test('compute budget project only triggers after trainer is built', () => {
+ const { G, PDEFS } = loadCompute();
+ const project = PDEFS.find(p => p.id === 'p_compute_budget');
+ G.totalKnowledge = 0;
+ G.buildings.trainer = 0;
+ assert.equal(project.trigger(), false);
+
+ G.totalKnowledge = 1500;
+ G.buildings.trainer = 0;
+ assert.equal(project.trigger(), false);
+
+ G.totalKnowledge = 1500;
+ G.buildings.trainer = 1;
+ assert.equal(project.trigger(), true);
+});