M2: platform-decay treadmill, infra tier, Cash Furnace

Platforms adapt to your slop (ALGO HEAT): each publish raises spam-filter
wariness which throttles all view rates up to 85%; training a new model
tier slips past the filters. Verified loop: publish -> squeeze -> train ->
relief. New hardware: 20A sub-panel (+1500W), fiber uplink (1Gbps, 8
ports), Cash Furnace (burns $1.50/s for +40% views, breaker-immune).
Fix: New Game wipe no longer clobbered by the beforeunload autosave.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-08-01 10:05:30 +10:00
parent d2bdffaa16
commit 32a2d3a248
17 changed files with 182 additions and 16 deletions

View File

@ -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 35, immersion cooling, offshore ships, lobbying.

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

View File

@ -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<string, unknown>).__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(); },
};
}

View File

@ -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 },
],

View File

@ -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/<sprite>_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]));

View File

@ -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) {

View File

@ -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,

View File

@ -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); }

View File

@ -45,6 +45,7 @@ export class Hud {
<div class="stat"><span class="label">SLOP RATE</span><span class="val" id="tok">0 tok/s</span></div>
<div class="stat"><span class="label">UPLINK</span><span class="val" id="net">0/0 Mbps</span></div>
<div class="stat"><span class="label">MODEL</span><span class="val" id="tier">SlopLM-1B</span><div class="bar train"><div id="trainbar"></div></div></div>
<div class="stat" title="Platform spam-filter wariness. Publishing raises it, views drop as it climbs. Train a new model tier for relief."><span class="label">ALGO HEAT</span><span class="val" id="adapt">0%</span><div class="bar"><div id="adaptbar"></div></div></div>
<button id="breaker-reset">RESET BREAKER</button>
</div>
<div id="controls" class="panel">
@ -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;

Binary file not shown.

34
tools/mb_add.py Normal file
View File

@ -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")

View File

@ -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"
}
}