diff --git a/CLAUDE.md b/CLAUDE.md index 90ceb71..833471c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,6 +29,14 @@ Date.now/Math.random inside sim). tailnet Ollama when it's back if more are needed). ## Roadmap (agreed with John 2026-08-01) -M1 art-forward Tier-1 slice (current) → M2 homelab tier + platform-decay -treadmill (model tiers force retraining) → M3 VC mandates + hazards. +✅ M1 art-forward Tier-1 slice (save/load, naming, SFX) +✅ M2 platform-decay treadmill: platformAdapt rises per publish, throttles all + view rates (up to 85%), tier-up grants relief — verified loop: publish → + squeeze → train → relief. Infra: 20A sub-panel (+1500W), fiber uplink + (1Gbps/8 ports), Cash Furnace (burns $1.5/s for +40% views; John's idea, + keep it — endgame R&D tree should have a literal cash-conveyor upgrade). +→ M3: VC mandates + rival CEO event system (fictional archetypes: Doom + Prophet, Hype Bro, Open-Weights Zealot, Legacy Giant) + hazards. +Design pillars (John-approved framing): Throughput, Allocation, Attention, +Capital, Expansion. Macro layer (Tier 4-5) = strategic map, NOT city-builder. Cut until earned: Tiers 3–5, immersion cooling, offshore ships, lobbying. diff --git a/public/assets/gen/breaker_panel.png b/public/assets/gen/breaker_panel.png new file mode 100644 index 0000000..53ad901 Binary files /dev/null and b/public/assets/gen/breaker_panel.png differ diff --git a/public/assets/gen/breaker_panel_cut.png b/public/assets/gen/breaker_panel_cut.png new file mode 100644 index 0000000..cf6578c Binary files /dev/null and b/public/assets/gen/breaker_panel_cut.png differ diff --git a/public/assets/gen/cash_furnace.png b/public/assets/gen/cash_furnace.png new file mode 100644 index 0000000..f8dd781 Binary files /dev/null and b/public/assets/gen/cash_furnace.png differ diff --git a/public/assets/gen/cash_furnace_cut.png b/public/assets/gen/cash_furnace_cut.png new file mode 100644 index 0000000..c724bfc Binary files /dev/null and b/public/assets/gen/cash_furnace_cut.png differ diff --git a/public/assets/gen/fiber_modem.png b/public/assets/gen/fiber_modem.png new file mode 100644 index 0000000..5111b58 Binary files /dev/null and b/public/assets/gen/fiber_modem.png differ diff --git a/public/assets/gen/fiber_modem_cut.png b/public/assets/gen/fiber_modem_cut.png new file mode 100644 index 0000000..19d5335 Binary files /dev/null and b/public/assets/gen/fiber_modem_cut.png differ diff --git a/src/main.ts b/src/main.ts index e25dce5..f36eb72 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,6 +11,7 @@ async function boot() { const el = document.getElementById("app")!; const loaded = loadState(); const state = loaded ?? createState(); + let wiped = false; const scene = new Scene(); await scene.init(el); @@ -31,7 +32,7 @@ async function boot() { hud.setWireActive(wireMode); }, onAlloc: (v) => { state.allocTraining = v; }, - onNewGame: () => { clearSave(); location.reload(); }, + onNewGame: () => { wiped = true; clearSave(); location.reload(); }, }); hud.bindBreaker(state); @@ -136,17 +137,19 @@ async function boot() { } requestAnimationFrame(loop); - // autosave - setInterval(() => saveState(state), 10_000); - window.addEventListener("beforeunload", () => saveState(state)); + // autosave (suppressed after a New Game wipe so the wipe actually sticks) + setInterval(() => { if (!wiped) saveState(state); }, 10_000); + window.addEventListener("beforeunload", () => { if (!wiped) saveState(state); }); document.addEventListener("visibilitychange", () => { - if (document.visibilityState === "hidden") saveState(state); + if (document.visibilityState === "hidden" && !wiped) saveState(state); }); // debug/automation handle (window as never as Record).__giga = { state, scene, place: (id: string, x: number, y: number) => place(state, id, x, y), wire: (a: number, b: number) => wire(state, a, b), + stepN: (n: number) => { for (let i = 0; i < n; i++) step(state); }, + newGame: () => { wiped = true; clearSave(); location.reload(); }, }; } diff --git a/src/sim/balance.ts b/src/sim/balance.ts index 2f92c5c..de710c8 100644 --- a/src/sim/balance.ts +++ b/src/sim/balance.ts @@ -31,10 +31,17 @@ export const BAL = { VIRAL_MULT_MAX: 25, CPM: 2.4, // $ per 1000 views + // Platform adaptation treadmill: every published video teaches the platforms' + // spam filters; wariness throttles ALL view rates until you train a fresh model. + ADAPT_PER_VIDEO: 0.03, // wariness gained per publish (soft-capped toward 1) + ADAPT_DECAY_S: 0.0005, // passive wariness loss per second (platforms drift) + ADAPT_MAX_PENALTY: 0.85, // at 100% wariness, views are cut by this fraction + ADAPT_TIER_RELIEF: 0.35, // wariness is multiplied by this on tier-up + // Training / model tiers: tokens required to reach tier i+1, revenue multiplier per tier TIERS: [ { name: "SlopLM-1B", mult: 1 }, - { name: "SlopLM-7B", mult: 2.2, tokens: 6_000 }, + { name: "SlopLM-7B", mult: 2.2, tokens: 4_000 }, { name: "SlopLM-70B", mult: 5, tokens: 40_000 }, { name: "SlopMoE-8x70B", mult: 12, tokens: 250_000 }, ], diff --git a/src/sim/catalog.ts b/src/sim/catalog.ts index 7636c60..f798d68 100644 --- a/src/sim/catalog.ts +++ b/src/sim/catalog.ts @@ -10,6 +10,10 @@ export interface DeviceDef { coolRate?: number; // heat units/s removed (AC) fan?: boolean; // spreads heat (diffusion bonus) uplinkMbps?: number; // router-class device + ports?: number; // wired-device capacity for router-class (default 4) + circuitBonus?: number; // extra breaker watts (electrical infrastructure) + burnRate?: number; // $/s consumed while running (cash furnace) + hypeMult?: number; // bonus view multiplier while burning (stacks multiplicatively) wallOnly?: boolean; footprint: { w: number; h: number }; sprite: string; // key in assets/gen/_cut.png @@ -60,12 +64,33 @@ export const CATALOG: DeviceDef[] = [ coolRate: 14, wallOnly: true, footprint: { w: 1, h: 1 }, sprite: "window_ac", unlockAt: 150, }, + { + id: "breaker_panel", name: "20A Sub-Panel", cost: 350, + desc: "A real electrician? In this economy? +1500W circuit capacity.", + wattsIdle: 0, wattsLoad: 0, tokRate: 0, heatOut: 0, + circuitBonus: 1500, wallOnly: true, footprint: { w: 1, h: 1 }, + sprite: "breaker_panel", unlockAt: 300, + }, + { + id: "fiber_modem", name: "Fiber Uplink", cost: 600, + desc: "1 Gbps of pure slop throughput. 8 ports. The ISP guy asked no questions.", + wattsIdle: 25, wattsLoad: 25, tokRate: 0, heatOut: 0.4, + uplinkMbps: 1000, ports: 8, footprint: { w: 1, h: 1 }, + sprite: "fiber_modem", unlockAt: 500, + }, { id: "server_rack_mini", name: "Half Rack", cost: 1800, desc: "eBay special. 120 tok/s. Your breaker is already crying.", wattsIdle: 200, wattsLoad: 2000, tokRate: 120, heatOut: 20, footprint: { w: 2, h: 2 }, sprite: "server_rack_mini", unlockAt: 900, }, + { + id: "cash_furnace", name: "Cash Furnace", cost: 2500, + desc: "Burns $1.50/s. Views +40% while lit. Marketing, technically. Works during blackouts.", + wattsIdle: 0, wattsLoad: 0, tokRate: 0, heatOut: 6, + burnRate: 1.5, hypeMult: 0.4, footprint: { w: 1, h: 1 }, + sprite: "cash_furnace", unlockAt: 5000, + }, ]; export const DEF = Object.fromEntries(CATALOG.map((d) => [d.id, d])); diff --git a/src/sim/sim.ts b/src/sim/sim.ts index 596e08c..2ab7a2d 100644 --- a/src/sim/sim.ts +++ b/src/sim/sim.ts @@ -75,18 +75,17 @@ export function sell(s: SimState, uid: number) { pushEvent(s, `Sold ${def.name} (+$${Math.round(def.cost * 0.5)})`, "info"); } -export const ROUTER_PORTS = 4; - export function wire(s: SimState, deviceUid: number, routerUid: number): string | null { const dev = s.devices.find((d) => d.uid === deviceUid); const rt = s.devices.find((d) => d.uid === routerUid); if (!dev || !rt) return "missing device"; if (!DEF[dev.defId].tokRate) return "nothing to wire there"; if (!DEF[rt.defId].uplinkMbps) return "that's not a router"; + const ports = DEF[rt.defId].ports ?? 4; const used = s.devices.filter((d) => d.wiredTo === routerUid).length; - if (used >= ROUTER_PORTS) return "router ports full (4)"; + if (used >= ports) return `router ports full (${ports})`; dev.wiredTo = routerUid; - pushEvent(s, `Wired ${DEF[dev.defId].name} to router`, "info"); + pushEvent(s, `Wired ${DEF[dev.defId].name} to ${DEF[rt.defId].name}`, "info"); return null; } @@ -96,8 +95,12 @@ export function resetBreaker(s: SimState) { } export function breakerLimit(s: SimState): number { - const strips = s.devices.filter((d) => d.defId === "power_strip").length; - return BAL.BREAKER_WATTS + strips * 200; + let limit = BAL.BREAKER_WATTS; + for (const d of s.devices) { + if (d.defId === "power_strip") limit += 200; + limit += DEF[d.defId].circuitBonus ?? 0; + } + return limit; } function tierMult(s: SimState): number { @@ -116,7 +119,8 @@ export function step(s: SimState) { const def = DEF[d.defId]; // desired utilization: compute devices run flat out when wired; coolers when on d.util = d.on ? (def.tokRate ? (d.wiredTo !== null ? 1 : 0.1) : 1) : 0; - d.powered = d.on && !s.breakerTripped; + // furnaces don't care about your breaker — fire predates electricity + d.powered = def.burnRate ? d.on : d.on && !s.breakerTripped; if (d.powered) watts += def.wattsIdle + (def.wattsLoad - def.wattsIdle) * d.util; } if (!s.breakerTripped && watts > limit) { @@ -201,7 +205,18 @@ export function step(s: SimState) { if (nextTier && s.trainTokens >= nextTier.tokens!) { s.tier++; s.trainTokens = 0; + s.platformAdapt *= BAL.ADAPT_TIER_RELIEF; + s.adaptStage = 0; pushEvent(s, `🧠 MODEL TIER UP: ${nextTier.name} (revenue x${nextTier.mult})`, "good"); + pushEvent(s, `😎 ${nextTier.name} slop slips right past the spam filters`, "good"); + } + + // platform adaptation drifts back down while you're not spamming + s.platformAdapt = Math.max(0, s.platformAdapt - BAL.ADAPT_DECAY_S * dt); + const stages = [0.25, 0.5, 0.75]; + while (s.adaptStage < stages.length && s.platformAdapt >= stages[s.adaptStage]) { + s.adaptStage++; + pushEvent(s, `🤖 Platforms updated their spam filters (wariness ${Math.round(stages[s.adaptStage - 1] * 100)}%)`, "bad"); } // slop videos @@ -215,14 +230,32 @@ export function step(s: SimState) { born: s.t, views: 0, rate: 0, viral, dead: false, }; s.videos.push(v); + s.platformAdapt = Math.min(1, s.platformAdapt + BAL.ADAPT_PER_VIDEO * (1 - s.platformAdapt)); if (viral > 6) pushEvent(s, `🔥 VIDEO WENT VIRAL (x${viral.toFixed(0)})`, "good"); if (s.videos.length > 40) s.videos.shift(); } + // cash furnaces: burn money, buy attention + let hype = 1; + for (const d of s.devices) { + const def = DEF[d.defId]; + if (!def.burnRate || !d.on) continue; + const burn = def.burnRate * dt; + if (s.cash >= burn) { + s.cash -= burn; + hype *= 1 + (def.hypeMult ?? 0); + d.util = 1; + } else { + d.util = 0; // starved — no cash, no hype + } + } + + const adaptMult = (1 - s.platformAdapt * BAL.ADAPT_MAX_PENALTY) * hype; for (const v of s.videos) { if (v.dead) continue; const age = s.t - v.born; if (age > BAL.VIDEO_LIFETIME_S) { v.dead = true; v.rate = 0; continue; } - v.rate = BAL.VIDEO_BASE_VIEWS_S * v.viral * tm * Math.exp(-age / (BAL.VIDEO_LIFETIME_S / 3)); + v.rate = BAL.VIDEO_BASE_VIEWS_S * v.viral * tm * adaptMult * + Math.exp(-age / (BAL.VIDEO_LIFETIME_S / 3)); v.views += v.rate * dt; income += (v.rate * dt * BAL.CPM) / 1000; } @@ -243,6 +276,7 @@ export function step(s: SimState) { r.uplinkMbps = uplink; r.dataMult = dataMult; r.maxTemp = maxTemp; + r.adapt = s.platformAdapt; } export function shopList(s: SimState) { diff --git a/src/sim/state.ts b/src/sim/state.ts index 7d839ad..4df61d5 100644 --- a/src/sim/state.ts +++ b/src/sim/state.ts @@ -36,6 +36,8 @@ export interface SimState { allocTraining: number; // 0..1 share of compute tier: number; trainTokens: number; + platformAdapt: number; // 0..1 spam-filter wariness + adaptStage: number; // last threshold toasted (0-3) tokenBank: number; videos: Video[]; rates: { @@ -48,6 +50,7 @@ export interface SimState { uplinkMbps: number; dataMult: number; maxTemp: number; + adapt: number; }; events: { msg: string; t: number; kind: string }[]; rng: number; @@ -67,11 +70,14 @@ export function createState(): SimState { allocTraining: 0.25, tier: 0, trainTokens: 0, + platformAdapt: 0, + adaptStage: 0, tokenBank: 0, videos: [], rates: { watts: 0, breakerWatts: BAL.BREAKER_WATTS, tokEff: 0, income: 0, powerCost: 0, demandMbps: 0, uplinkMbps: 0, dataMult: 1, maxTemp: BAL.AMBIENT_C, + adapt: 0, }, events: [], rng: 0x5109a & 0xffffffff, diff --git a/src/ui/hud.css b/src/ui/hud.css index 781eb11..32db078 100644 --- a/src/ui/hud.css +++ b/src/ui/hud.css @@ -80,6 +80,12 @@ #hint { position: absolute; bottom: 12px; right: 215px; color: var(--dim); font-size: 11px; text-align: right; } +#adapt.hot { color: var(--red); } +#flash { position: fixed; inset: 0; pointer-events: none; z-index: 99; + background: radial-gradient(circle at 50% 45%, rgba(245, 158, 11, 0.35), transparent 60%); + animation: flashfade 0.7s forwards; } +@keyframes flashfade { 0% { opacity: 0; } 15% { opacity: 1; } 100% { opacity: 0; } } + #name-modal { position: fixed; inset: 0; background: rgba(5, 5, 10, 0.85); display: flex; align-items: center; justify-content: center; pointer-events: auto; } #name-box { padding: 28px 32px; text-align: center; width: 360px; } #name-box h2 { margin: 0 0 4px; letter-spacing: 0.3em; color: var(--purple); } diff --git a/src/ui/hud.ts b/src/ui/hud.ts index 051ebd5..5dde7db 100644 --- a/src/ui/hud.ts +++ b/src/ui/hud.ts @@ -45,6 +45,7 @@ export class Hud {
SLOP RATE0 tok/s
UPLINK0/0 Mbps
MODELSlopLM-1B
+
ALGO HEAT0%
@@ -173,6 +174,12 @@ export class Hud { q("#tok").textContent = fmt(s.rates.tokEff, 1) + " tok/s"; q("#net").textContent = `${fmt(s.rates.demandMbps, 0)}/${fmt(s.rates.uplinkMbps, 0)} Mbps`; (q("#net") as HTMLElement).style.color = s.rates.dataMult < 1 ? "var(--red)" : ""; + const ad = s.rates.adapt; + q("#adapt").textContent = Math.round(ad * 100) + "%"; + q("#adapt").classList.toggle("hot", ad > 0.5); + (q("#adaptbar") as HTMLElement).style.width = Math.round(ad * 100) + "%"; + (q("#adaptbar") as HTMLElement).style.background = + ad > 0.5 ? "var(--red)" : ad > 0.25 ? "var(--amber)" : "var(--green)"; q("#tier").textContent = BAL.TIERS[s.tier].name; const nt = BAL.TIERS[s.tier + 1]; (q("#trainbar") as HTMLElement).style.width = nt ? Math.min(100, (s.trainTokens / nt.tokens!) * 100) + "%" : "100%"; @@ -198,6 +205,12 @@ export class Hud { for (const ev of s.events.slice(this.lastEventCount)) { if (ev.kind === "bad") sfx.breaker(); else if (ev.kind === "good") ev.msg.includes("TIER UP") ? sfx.tierUp() : sfx.viral(); + if (ev.msg.includes("VIRAL")) { + const f = document.createElement("div"); + f.id = "flash"; + document.body.appendChild(f); + setTimeout(() => f.remove(), 700); + } if (ev.kind === "buy") continue; const t = document.createElement("div"); t.className = "toast " + ev.kind; diff --git a/tools/__pycache__/mb_gen.cpython-314.pyc b/tools/__pycache__/mb_gen.cpython-314.pyc new file mode 100644 index 0000000..a4e0ebb Binary files /dev/null and b/tools/__pycache__/mb_gen.cpython-314.pyc differ diff --git a/tools/mb_add.py b/tools/mb_add.py new file mode 100644 index 0000000..efbe1f1 --- /dev/null +++ b/tools/mb_add.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""One-off: generate additional sprites without re-running the whole batch. +Usage: python3 tools/mb_add.py (edit NEW below per run)""" +import json, pathlib, sys + +sys.path.insert(0, str(pathlib.Path(__file__).parent)) +from mb_gen import ROOT, STYLE, flux_spec, run_batch, upload, beat + +NEW = [ + ("cash_furnace", "a cast iron pot-belly furnace with door open, burning dollar bills visible in the flames, loose stacks of cash on the floor beside it, warm orange fire glow"), +] + +gen = ROOT / "public/assets/gen" +raw = run_batch([flux_spec(n, STYLE + d, 768, 768) for n, d in NEW], gen) +beat("bg removal for new sprites") +cuts = run_batch( + [(f"{n}_cut", "bg_remove_local", upload(gen / f"{n}.png"), + {"resolution": 1024, "background": "transparent"}) for n, _ in NEW], gen) + +# autocrop like the main pipeline +from PIL import Image +for n, _ in NEW: + p = gen / f"{n}_cut.png" + im = Image.open(p).convert("RGBA") + bbox = im.split()[-1].getbbox() + if bbox: + l, t, r, b = bbox + im.crop((max(0, l - 8), max(0, t - 8), min(im.width, r + 8), min(im.height, b + 8))).save(p) + +mpath = ROOT / "tools/mb_manifest.json" +m = json.loads(mpath.read_text()) +m.update(raw); m.update(cuts) +mpath.write_text(json.dumps(m, indent=2)) +beat(f"added {list(raw)} + cuts") diff --git a/tools/mb_manifest.json b/tools/mb_manifest.json index 977545f..433fc14 100644 --- a/tools/mb_manifest.json +++ b/tools/mb_manifest.json @@ -133,5 +133,35 @@ "job": "6f2f27867c46", "asset": "bd5a265f6ede", "path": "/Users/jing/Documents/gigaslop/public/assets/slop/slop_09.png" + }, + "breaker_panel": { + "job": "09c19f1f6f31", + "asset": "c571b9c44b21", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/breaker_panel.png" + }, + "fiber_modem": { + "job": "64652f7c738a", + "asset": "b546204be4f7", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/fiber_modem.png" + }, + "breaker_panel_cut": { + "job": "dfc878ce1239", + "asset": "a61cd4310322", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/breaker_panel_cut.png" + }, + "fiber_modem_cut": { + "job": "8b6d1a75ad4c", + "asset": "4184306b54ba", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/fiber_modem_cut.png" + }, + "cash_furnace": { + "job": "8288ff41ccbb", + "asset": "a17b4a6e504b", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/cash_furnace.png" + }, + "cash_furnace_cut": { + "job": "b8b0755ee459", + "asset": "4dca1b9e28b0", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/cash_furnace_cut.png" } } \ No newline at end of file