M8: warehouse tier, immersion cooling, SaaS subscribers

The move ladder continues: garage -> 20x20 warehouse with 8kW
three-phase. Immersion tank answers the fire era — eight rack-class
slots under mineral oil, 85% heat reduction, cannot ignite (verified:
8x H100 flat-out at 82C, zero fires). CRAC unit and AIO liquid loop
fill out the cooling ladder. New recurring revenue: SaaS subscribers
funnel in from live view rates and churn on latency (serving consumes
inference tokens before the video pipeline — overselling your compute
bleeds customers), on model collapse, and on outages (breaker trips
instantly cost 15% of subscribers).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-08-01 16:24:19 +10:00
parent e4f0ddf0f2
commit 0309f578a1
15 changed files with 126 additions and 18 deletions

View File

@ -82,9 +82,16 @@ Date.now/Math.random inside sim).
fire within 3 tiles then is spent). Flaky used cards (p106/mtt_s80/
radish580) die under load. Ad-spend slider (log-diminishing hype) +
email Spam Blast (instant views, 60s cooldown, 15% blacklist risk).
→ M8 backlog: ACTIVE scraping minigame, subscribers/SaaS churn revenue,
warehouse room (tier 3), Conveyor belt visual, per-carrier PSU,
liquid/immersion cooling for racks (fires make it necessary).
✅ M8: warehouse tier (20x20, 8kW, MOVES ladder has from-fields now);
immersion tank (fire-immune carrier, card heat x0.15, 8 rack-class
slots) + CRAC unit + AIO socket; SaaS subscribers — signups from live
view rate (SAAS.CONV), churn on latency (serve demand > inference
share), collapse, and breaker trips (-15% instant); serving eats
inference tokens BEFORE the video pipeline. SUBS stat in HUD with
latency warning.
→ M9 backlog: ACTIVE scraping minigame, Conveyor belt visual,
per-carrier PSU, Tier 4 campus + strategic map layer, win/lose
states, sound design pass, Steam packaging decision.
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: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 KiB

View File

@ -59,4 +59,15 @@ export const BAL = {
{ name: "v3", mult: 12, tokens: 250_000, vramGB: 512, checkpointGB: 3_000 },
],
VRAM_STARVE_FLOOR: 0.25, // token multiplier floor when VRAM-starved
// SaaS: subscribers funnel in from views, churn out on latency/downtime.
SAAS: {
CONV: 0.003, // signups per view
CHURN_S: 0.00033, // ~2%/min baseline
SUB_REV_S: 0.004, // $/s per subscriber
TOK_PER_SUB: 0.02, // tok/s each subscriber's requests consume (from slop share)
LATENCY_CHURN_MULT: 3, // when serve demand exceeds inference supply
COLLAPSE_CHURN_MULT: 3, // gibberish product
TRIP_HIT: 0.15, // subscribers lost instantly on a breaker trip
},
};

View File

@ -17,6 +17,8 @@ export interface DeviceDef {
ports?: number; // wired-device capacity for router/switch-class (default 4)
storageGB?: number; // checkpoint capacity
circuitBonus?: number; // extra breaker watts (electrical infrastructure)
fireImmune?: boolean; // cannot thermally run away (immersion)
cardHeatMult?: number; // carrier-level multiplier on installed-card heat
burnRate?: number; // $/s consumed while running (cash furnace)
hypeMult?: number; // bonus view multiplier while burning (stacks multiplicatively)
wallOnly?: boolean;
@ -87,6 +89,13 @@ export const CATALOG: DeviceDef[] = [
wattsIdle: 150, wattsLoad: 250, tokRate: 0, heatOut: 2, gpuSlots: 8, rackClass: true,
footprint: { w: 2, h: 2 }, sprite: "server_rack_mini", unlockAt: 900,
},
{
id: "immersion_tank", name: "Immersion Tank", cat: "compute", cost: 4000,
desc: "Eight slots under mineral oil. 85% less heat, cannot catch fire. Smells like chips.",
wattsIdle: 250, wattsLoad: 400, tokRate: 0, heatOut: 1, gpuSlots: 8, rackClass: true,
fireImmune: true, cardHeatMult: 0.15,
footprint: { w: 2, h: 2 }, sprite: "immersion_tank", unlockAt: 5000,
},
// --- network ---
{
id: "router", name: "Wi-Fi Router", cat: "network", cost: 40,
@ -166,6 +175,13 @@ export const CATALOG: DeviceDef[] = [
coolRate: 14, wallOnly: true, footprint: { w: 1, h: 1 },
sprite: "window_ac", unlockAt: 150,
},
{
id: "crac_unit", name: "CRAC Unit", cat: "cooling", cost: 1500,
desc: "Precision air for people whose 'computer room' is real now.",
wattsIdle: 200, wattsLoad: 1600, tokRate: 0, heatOut: 0,
coolRate: 30, footprint: { w: 1, h: 1 },
sprite: "crac_unit", unlockAt: 2500,
},
// --- misc ---
{
id: "desk", name: "Desk", cat: "misc", cost: 15,

View File

@ -212,6 +212,7 @@ export const SOCKETS: SocketUpgrade[] = [
{ id: "extra_fan", name: "Extra Fans", cost: 80, desc: "Heat -25%", heatMult: 0.75 },
{ id: "undervolt", name: "Undervolt", cost: 150, desc: "Watts -15%, tokens -5%", wattsMult: 0.85, tokMult: 0.95 },
{ id: "risers", name: "Riser Cards", cost: 250, desc: "Tokens +15%, heat +15%", tokMult: 1.15, heatMult: 1.15 },
{ id: "aio_loop", name: "AIO Liquid Loop", cost: 400, desc: "Heat -50%", heatMult: 0.5 },
];
export const SOCKET_SLOTS = 2;

View File

@ -25,8 +25,14 @@ export const ROOMS: Record<string, RoomDef> = {
w: 14, h: 14, tileW: 58.5, tileH: 29.5, origin: { x: 510, y: 330 },
imgW: 1024, imgH: 768, breakerBase: 2400,
},
warehouse: {
id: "warehouse", name: "The Warehouse", image: "/assets/gen/warehouse.png",
w: 20, h: 20, tileW: 41, tileH: 20.5, origin: { x: 510, y: 330 },
imgW: 1024, imgH: 768, breakerBase: 8000,
},
};
export const MOVES: { to: string; cost: number; unlockAt: number; blurb: string }[] = [
{ to: "garage", cost: 3000, unlockAt: 2500, blurb: "Mom wants her bedroom back. 14x14 of concrete, 2.4kW service." },
export const MOVES: { from: string; to: string; cost: number; unlockAt: number; blurb: string }[] = [
{ from: "bedroom", to: "garage", cost: 3000, unlockAt: 2500, blurb: "Mom wants her bedroom back. 14x14 of concrete, 2.4kW service." },
{ from: "garage", to: "warehouse", cost: 25000, unlockAt: 15000, blurb: "Industrial unit, zoned 'flexible'. 20x20 tiles, 8kW three-phase." },
];

View File

@ -187,6 +187,7 @@ export function moveRoom(s: SimState, to: string): string | null {
const mv = MOVES.find((m) => m.to === to);
if (!mv || !ROOMS[to]) return "nowhere to move";
if (s.room === to) return "you already live here";
if (mv.from !== s.room) return `that move starts from the ${ROOMS[mv.from].name}`;
if (s.earnedTotal < mv.unlockAt) return `earn $${mv.unlockAt} total first`;
if (s.cash < mv.cost) return "can't afford the bond";
s.cash -= mv.cost;
@ -503,6 +504,10 @@ export function step(s: SimState) {
const loss = Math.round(BAL.CHECKPOINT_LOSS * 100);
s.trainTokens *= 1 - BAL.CHECKPOINT_LOSS;
pushEvent(s, `⚡ BREAKER TRIPPED — checkpoint corrupted (-${loss}% training)`, "bad");
if (s.subs >= 1) {
s.subs *= 1 - BAL.SAAS.TRIP_HIT;
pushEvent(s, `📱 Outage! ${Math.round(BAL.SAAS.TRIP_HIT * 100)}% of subscribers rage-quit`, "bad");
}
}
}
} else if (watts <= limit) {
@ -523,9 +528,10 @@ export function step(s: SimState) {
const def = DEF[d.defId];
if (!d.powered) continue;
// packed cards choke airflow — heat rises superlinearly, and throttling
// does NOT cool a crammed chassis (that's how runaways happen)
// does NOT cool a crammed chassis (that's how runaways happen).
// Immersion tanks dampen card heat instead (cardHeatMult).
const packMult = 1 + 0.15 * Math.max(0, d.gpus.length - 1);
const cardHeat = cardStats(d).heat * packMult;
const cardHeat = cardStats(d).heat * packMult * (def.cardHeatMult ?? 1);
if (def.heatOut || cardHeat)
heat[idx(d.x, d.y)] += (def.heatOut + cardHeat) * d.util * dt * 2.2 *
(isCompute(def) ? mods.heatMult * deviceMods(d.upgrades).heat : 1);
@ -566,7 +572,7 @@ export function step(s: SimState) {
const over = d.temp - BAL.THROTTLE_C;
d.perf = over <= 0 ? 1 : Math.max(BAL.THROTTLE_FLOOR, 1 - over / 30);
// thermal runaway: sustained >95°C under load ignites the hardware
if (d.temp > 95 && d.util > 0.5) {
if (d.temp > 95 && d.util > 0.5 && !DEF[d.defId].fireImmune) {
d.hotSince ??= s.t;
if (s.t - d.hotSince > 10) igniteDevice(s, d);
} else {
@ -655,9 +661,12 @@ export function step(s: SimState) {
pushEvent(s, `🤖 Platforms updated their spam filters (wariness ${Math.round(stages[s.adaptStage - 1] * 100)}%)`, "bad");
}
// SaaS serving eats inference first — paying customers get the tokens
const serveNeed = s.subs * BAL.SAAS.TOK_PER_SUB * dt;
const saasLatency = s.subs > 1 && serveNeed > tokInf;
// slop videos — richer modalities cost more tokens per video but earn far more
const videoCost = BAL.VIDEO_COST_TOK * mods.videoCostMult;
s.tokenBank += tokInf;
s.tokenBank += Math.max(0, tokInf - serveNeed);
while (s.tokenBank >= videoCost) {
s.tokenBank -= videoCost;
const viralChance = BAL.VIRAL_CHANCE + mods.viralChanceAdd;
@ -791,6 +800,7 @@ export function step(s: SimState) {
const adaptMult = (1 - s.platformAdapt * BAL.ADAPT_MAX_PENALTY) * hype * adHype * viewFx *
mods.viewsPerVideoMult * mods.conveyorHype * (hostingOwnMax ? 1.1 : 1);
let liveViewRate = 0;
for (const v of s.videos) {
if (v.dead) continue;
const age = s.t - v.born;
@ -799,9 +809,28 @@ export function step(s: SimState) {
Math.exp(-age / (BAL.VIDEO_LIFETIME_S / 3));
v.views += v.rate * dt;
s.totalViews += v.rate * dt;
liveViewRate += v.rate;
income += (v.rate * dt * BAL.CPM) / 1000;
}
// --- SaaS: the "Instant Girlfriend App" wrapper business ---
let subsNet = 0;
if (s.shippedGrades.length > 0) {
const signups = liveViewRate * BAL.SAAS.CONV * dt;
let churn = BAL.SAAS.CHURN_S;
if (saasLatency) churn *= BAL.SAAS.LATENCY_CHURN_MULT;
if (s.collapsed) churn *= BAL.SAAS.COLLAPSE_CHURN_MULT;
const before = s.subs;
s.subs = Math.max(0, s.subs + signups - s.subs * churn * dt);
subsNet = (s.subs - before) / dt;
income += s.subs * BAL.SAAS.SUB_REV_S * dt;
const stages = [100, 1_000, 10_000, 100_000];
while (s.subStage < stages.length && s.subs >= stages[s.subStage]) {
s.subStage++;
pushEvent(s, `🎉 ${stages[s.subStage - 1].toLocaleString()} SUBSCRIBERS — the wrapper prints money`, "good");
}
}
// VC mandate resolution
if (s.mandate) {
const got = s.totalViews - s.mandate.startViews;
@ -860,6 +889,8 @@ export function step(s: SimState) {
r.vramNeed = vramNeed;
r.storageGB = storageGB;
r.trainStalled = trainStalled;
r.subsNet += (subsNet - r.subsNet) * sm;
r.saasLatency = saasLatency;
}
export function shopList(s: SimState) {

View File

@ -33,6 +33,8 @@ export interface SimState {
fires: { x: number; y: number; until: number }[];
adSpend: number; // $/s marketing burn
lastSpamT: number; // last email-spam blast
subs: number; // SaaS subscribers
subStage: number; // last milestone toasted
t: number;
tick: number;
cash: number;
@ -97,6 +99,8 @@ export interface SimState {
vramNeed: number;
storageGB: number;
trainStalled: boolean;
subsNet: number; // subscribers/s net growth
saasLatency: boolean;
};
events: { msg: string; t: number; kind: string }[];
rng: number;
@ -109,6 +113,8 @@ export function createState(): SimState {
fires: [],
adSpend: 0,
lastSpamT: -999,
subs: 0,
subStage: 0,
t: 0,
tick: 0,
cash: BAL.START_CASH,
@ -156,6 +162,7 @@ export function createState(): SimState {
powerCost: 0, demandMbps: 0, uplinkMbps: 0, dataMult: 1, maxTemp: BAL.AMBIENT_C,
adapt: 0, hostIncome: 0, hostTok: 0,
vramHave: 0, vramNeed: 0, storageGB: 0, trainStalled: false,
subsNet: 0, saasLatency: false,
},
events: [],
rng: 0x5109a & 0xffffffff,

View File

@ -43,6 +43,7 @@ export class Hud {
selectedShop: string | null = null;
lastEventCount = 0;
lockedCache = "";
lastRoom = "bedroom";
constructor(cb: HudCallbacks) {
this.cb = cb;
@ -56,6 +57,7 @@ export class Hud {
<div class="stat"><span class="label">HOTTEST</span><span class="val" id="temp">24°C</span></div>
<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">VIEWS</span><span class="val" id="views">0</span></div>
<div class="stat"><span class="label">SUBS</span><span class="val" id="subs">0</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">HOSTING</span><span class="val" id="host">$0/s</span></div>
<div class="stat"><span class="label">VRAM</span><span class="val" id="vram">0GB</span></div>
@ -131,7 +133,7 @@ export class Hud {
};
(this.root.querySelector("#spam") as HTMLButtonElement).onclick = () => cb.onSpam();
(this.root.querySelector("#move-hq") as HTMLButtonElement).onclick = () => {
const mv = MOVES[0];
const mv = MOVES.find((m) => m.from === this.lastRoom);
if (mv && confirm(`${ROOMS[mv.to].name}: ${mv.blurb}$${mv.cost}?`)) cb.onMove(mv.to);
};
}
@ -244,6 +246,11 @@ export class Hud {
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)" : "";
q("#views").textContent = fmt(s.totalViews, 0);
q("#subs").textContent = fmt(s.subs, 0) + (s.rates.saasLatency ? " ⚠" : "");
(q("#subs") as HTMLElement).style.color =
s.rates.saasLatency ? "var(--red)" : s.rates.subsNet > 0.01 ? "var(--green)" : "";
(q("#subs") as HTMLElement).title =
s.rates.saasLatency ? "LATENCY — subscribers churning; free up inference compute" : "SaaS subscribers";
// VC raise chip / mandate progress
const raise = q("#raise"), mandate = q("#mandate");
@ -268,10 +275,11 @@ export class Hud {
const cd = Math.max(0, 60 - (s.t - s.lastSpamT));
spam.textContent = cd > 0 ? `📧 Spam Blast (${Math.ceil(cd)}s)` : `📧 Spam Blast ($${40 * (s.round + 1)})`;
spam.classList.toggle("on", cd <= 0);
const mv = MOVES.find((m) => m.to !== s.room);
this.lastRoom = s.room;
const mv = MOVES.find((m) => m.from === s.room);
(q("#move-hq") as HTMLElement).style.display =
mv && s.room === "bedroom" && s.earnedTotal >= mv.unlockAt ? "block" : "none";
if (mv) q("#move-hq").textContent = `🚚 Move HQ ($${mv.cost})`;
mv && s.earnedTotal >= mv.unlockAt ? "block" : "none";
if (mv) q("#move-hq").textContent = `🚚 ${ROOMS[mv.to].name} ($${fmt(mv.cost, 0)})`;
// clap-back window
const clap = q("#clap");

View File

@ -7,14 +7,15 @@ sys.path.insert(0, str(pathlib.Path(__file__).parent))
from mb_gen import ROOT, STYLE, flux_spec, run_batch, upload, beat
NEW = [
("extinguisher", "a red CO2 fire extinguisher standing on the floor"),
("immersion_tank", "an open-top industrial immersion cooling tank filled with glowing clear mineral oil, server boards submerged inside, tubes and a small pump, sci-fi green glow"),
("crac_unit", "a tall white industrial precision air conditioning unit for server rooms, floor standing, vent grilles, small status screen"),
]
ROOMS = [
("garage", "empty isometric double garage interior for a video game, 2:1 isometric projection, "
"two visible walls meeting at back corner, sealed metal roller door on one wall, bare concrete "
"floor with subtle square grid lines, exposed stud walls, hanging work light, night time, "
"moody blue-purple lighting with one neon strip, no vehicles, no furniture, no people, no text, "
("warehouse", "empty isometric industrial warehouse interior for a video game, 2:1 isometric projection, "
"two visible corrugated metal walls meeting at back corner, huge bare concrete floor with subtle "
"square grid lines, steel roof trusses hinted at top, high windows, sodium work lights, night time, "
"moody blue-purple lighting with cyan accents, no shelving, no vehicles, no people, no text, "
"clean detailed painterly game art", 1024, 768),
]

View File

@ -263,5 +263,25 @@
"job": "d8ff5530b60b",
"asset": "4a55508fae90",
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/extinguisher_cut.png"
},
"immersion_tank": {
"job": "59c0446c8226",
"asset": "5830eb712b59",
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/immersion_tank.png"
},
"crac_unit": {
"job": "a6d5df83247e",
"asset": "9568c9c5917d",
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/crac_unit.png"
},
"immersion_tank_cut": {
"job": "e4dde2353757",
"asset": "9b2a341a7b99",
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/immersion_tank_cut.png"
},
"crac_unit_cut": {
"job": "c15472693bd1",
"asset": "00c76075efec",
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/crac_unit_cut.png"
}
}