diff --git a/CLAUDE.md b/CLAUDE.md index edcda7f..7d68caf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,9 +54,19 @@ Date.now/Math.random inside sim). Regulatory Capture (lawsuit immune, 2x adapt decay). • Sockets: click device → inspector panel (stats, on/off, sell, 2 upgrade slots: fans/undervolt/risers). -→ M5 candidates: hazards (thermal runaway fire), Tier 2 garage move - (bigger room = new backdrop + grid refit), subscribers/SaaS second - revenue stream with churn, endgame Conveyor visual (belt sprite). +✅ M5 model line + API market: player names their model at incorporation + (versions 1/1.5/2/3 = the old tier ladder); ship grades mini/pro/MAX + from the training bank (margin/demand/distill-risk per grade, GRADES in + research.ts); 3-way compute split train/host/slop (allocHosting); + rotating third-party market (rollMarketModel), hot drops crater + incumbent margins ~28%; hosting your own grade rolls distillation + (margin cut 60% that version — re-ship on a newer version to restore + the moat); scraping ops cash ladder (puppeteer→proxies→botnet→captcha) + vs severity-1..3 IP-ban outages. Market panel = M key. +→ M6 backlog: ad-spend slider + email-spam campaign buttons (one-shot + view bursts w/ ban risk), ACTIVE scraping minigame, hazards (thermal + runaway fire), Tier 2 garage move, subscribers/SaaS churn revenue, + Conveyor belt visual. 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/src/main.ts b/src/main.ts index d6b2ec4..93a5f57 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { BAL } from "./sim/balance"; import { createState, pushEvent } from "./sim/state"; -import { step, place, canPlace, wire, sell, grant, raiseRound, clapBack } from "./sim/sim"; +import { step, place, canPlace, wire, sell, grant, raiseRound, clapBack, shipGrade, switchHosting, unlockScrape } from "./sim/sim"; import { saveState, loadState, clearSave } from "./sim/save"; import { DEF } from "./sim/catalog"; import { Scene } from "./render/scene"; @@ -35,6 +35,7 @@ async function boot() { hud.setWireActive(wireMode); }, onAlloc: (v) => { state.allocTraining = v; }, + onAllocHost: (v) => { state.allocHosting = v; }, onNewGame: () => { wiped = true; clearSave(); location.reload(); }, onRaise: () => { const err = raiseRound(state); @@ -49,6 +50,7 @@ async function boot() { const panels = new Panels(state); (hud.root.querySelector("#lab-btn") as HTMLButtonElement).onclick = () => panels.toggle("lab"); (hud.root.querySelector("#rnd-btn") as HTMLButtonElement).onclick = () => panels.toggle("rnd"); + (hud.root.querySelector("#market-btn") as HTMLButtonElement).onclick = () => panels.toggle("market"); const cancel = () => { shopSel = null; hud.selectedShop = null; hud.lockedCache = ""; @@ -63,6 +65,7 @@ async function boot() { } if (e.key === "l" || e.key === "L") panels.toggle("lab"); if (e.key === "r" || e.key === "R") panels.toggle("rnd"); + if (e.key === "m" || e.key === "M") panels.toggle("market"); }); scene.app.canvas.addEventListener("pointermove", (e) => { @@ -155,9 +158,11 @@ async function boot() { grant(state, "desk", 5, 7); grant(state, "desk_pc", 7, 7); grant(state, "router", 2, 1); - state.company = await hud.showNameModal(); + const names = await hud.showNameModal(); + state.company = names.company; + state.modelName = names.model; document.title = `${state.company} — GigaSlop`; - pushEvent(state, `${state.company} is live. Press W and wire the PC to the router.`, "info"); + pushEvent(state, `${state.company} is live. ${state.modelName} 1 awaits its slop. Wire the PC to the router (W).`, "info"); saveState(state); } else { document.title = `${state.company} — GigaSlop`; @@ -186,6 +191,9 @@ async function boot() { data: (k: DataSource) => setDataSource(state, k), rnd: (id: string) => unlockRnd(state, id), upg: (uid: number, id: string) => buyUpgrade(state, uid, id), + ship: (g: "mini" | "pro" | "max") => shipGrade(state, g), + host: (id: number | null) => switchHosting(state, id), + scrape: (id: string) => unlockScrape(state, id), }; } diff --git a/src/sim/research.ts b/src/sim/research.ts index 726c80d..e8cfe47 100644 --- a/src/sim/research.ts +++ b/src/sim/research.ts @@ -123,6 +123,79 @@ export const RND: RndNode[] = [ mods: { lawsuitImmune: true, adaptDecayMult: 2 } }, ]; +// ---------- Model grades (ship a version in up to three sizes) ---------- + +export interface GradeDef { + id: "mini" | "pro" | "max"; + suffix: string; + costMult: number; // × the version's training cost to ship + margin: number; // $/token when hosted + demand: number; // tok/s the market will buy + distillChance: number; // per distill-roll while hosted + hypeMult?: number; // MAX carries prestige +} + +export const GRADES: GradeDef[] = [ + { id: "mini", suffix: "-mini", costMult: 0.25, margin: 0.0008, demand: 220, distillChance: 0.15 }, + { id: "pro", suffix: "-pro", costMult: 0.5, margin: 0.002, demand: 90, distillChance: 0.3 }, + { id: "max", suffix: "-MAX", costMult: 0.9, margin: 0.005, demand: 35, distillChance: 0.5, hypeMult: 1.1 }, +]; + +export const VERSION_NAMES = ["1", "1.5", "2", "3"]; +export const DISTILL_MARGIN_CUT = 0.4; // distilled grade keeps 40% margin (this version) + +// ---------- Scraping ops (cash-paid data acquisition ladder) ---------- + +export interface ScrapeNode { + id: string; + name: string; + cash: number; + desc: string; + requires?: string; + scrapedRateAdd: number; // additive bonus to scraped training rate + banLevel: number; // blocks IP-ban events of severity <= banLevel +} + +export const SCRAPE_OPS: ScrapeNode[] = [ + { id: "puppeteer", name: "Shitty Puppeteer Scripts", cash: 100, scrapedRateAdd: 0.1, banLevel: 0, + desc: "headless-chrome:latest. Scraped training +10%. Gets IP banned constantly." }, + { id: "proxy_pool", name: "Proxy Pool", cash: 400, requires: "puppeteer", scrapedRateAdd: 0.1, banLevel: 1, + desc: "1,000 datacenter IPs. +10% more, shrugs off basic bans." }, + { id: "resi_botnet", name: "Residential Botnet", cash: 1500, requires: "proxy_pool", scrapedRateAdd: 0.25, banLevel: 2, + desc: "Someone's smart fridge is scraping Reddit for you. +25%, survives serious bans." }, + { id: "captcha_farm", name: "Captcha Farm", cash: 4000, requires: "resi_botnet", scrapedRateAdd: 0.4, banLevel: 3, + desc: "Click the crosswalks, all of them, forever. +40%, effectively unbannable." }, +]; + +// ---------- The API hosting market ---------- + +export interface MarketModel { + id: number; + name: string; + flag: string; + margin: number; // $/tok + demand: number; // tok/s cap + born: number; + yours?: "mini" | "pro" | "max"; // set when this row is one of your shipped grades +} + +const LAB_NAMES = ["Qwoon", "DeepSlop", "Kimi-Chi", "GLM-Slop", "Yi-Haw", "Baidoodle", "MoonSlop", "Zhipoo", "StepSlop", "MiniMaxx", "Doubaozled", "SloPangu"]; +const LAB_SIZES = ["-7B", "-72B", "-236B", "-R2", "-Turbo", "-VL", "-Air", "-Ω"]; +const LAB_FLAGS = ["🇨🇳", "🇨🇳", "🇨🇳", "🇨🇳", "🇺🇸", "🇫🇷", "🇰🇷"]; + +export function rollMarketModel(rand: () => number, id: number, t: number): MarketModel { + return { + id, + name: LAB_NAMES[Math.floor(rand() * LAB_NAMES.length)] + LAB_SIZES[Math.floor(rand() * LAB_SIZES.length)], + flag: LAB_FLAGS[Math.floor(rand() * LAB_FLAGS.length)], + margin: 0.0008 + rand() * 0.0017, + demand: 60 + Math.floor(rand() * 240), + born: t, + }; +} + +export const MODEL_NAME_POOL = ["Goonita", "SlopGPT", "Chudini", "BrainrotNet", "YapLM", "GigaYap", "Sloppo", "DerpSeek", "Goonami", "Sludge", "Chunguscore", "GoonDiffusion"]; + // ---------- Device socket upgrades ---------- export interface SocketUpgrade { diff --git a/src/sim/sim.ts b/src/sim/sim.ts index d2de922..9f0fafd 100644 --- a/src/sim/sim.ts +++ b/src/sim/sim.ts @@ -4,6 +4,7 @@ import { Device, SimState, pushEvent, rand } from "./state"; import { computeMods, deviceMods, MODALITIES, SPECS, SPEC_COST_BY_GEN, DATA_SOURCES, COLLAPSE_CHANCE, RND, SOCKETS, SOCKET_SLOTS, DataSource, + GRADES, VERSION_NAMES, DISTILL_MARGIN_CUT, SCRAPE_OPS, rollMarketModel, } from "./research"; import SLOP_TITLE_COUNT_JSON from "../data/sloptitles.json"; @@ -192,12 +193,13 @@ export function unlockGen(s: SimState): string | null { if (s.spec) pushEvent(s, "🧪 New base model — your old specialization tuning is gone", "info"); s.spec = null; s.collapsed = false; + const label = `${s.modelName} ${VERSION_NAMES[s.tier]}`; if (s.dataSource === "own" && rand(s) < COLLAPSE_CHANCE) { s.collapsed = true; - pushEvent(s, `🌀 MODEL COLLAPSE — ${next.name} trained on its own slop outputs pure gibberish (revenue halved this gen)`, "bad"); + pushEvent(s, `🌀 MODEL COLLAPSE — ${label} trained on its own slop outputs pure gibberish (revenue halved this version)`, "bad"); } - pushEvent(s, `🧠 MODEL TIER UP: ${next.name} (revenue x${next.mult})`, "good"); - pushEvent(s, `😎 ${next.name} slop slips right past the spam filters`, "good"); + pushEvent(s, `🧠 ${label} RELEASED (revenue x${next.mult})`, "good"); + pushEvent(s, `😎 ${label} slop slips right past the spam filters`, "good"); return null; } @@ -222,7 +224,7 @@ export function setSpec(s: SimState, id: string): string | null { if (s.trainTokens < cost) return `need ${cost} training tokens`; s.trainTokens -= cost; s.spec = id; - pushEvent(s, `🧪 ${BAL.TIERS[s.tier].name} fine-tuned: ${node.name}`, "good"); + pushEvent(s, `🧪 ${s.modelName} ${VERSION_NAMES[s.tier]} fine-tuned: ${node.name}`, "good"); return null; } @@ -272,6 +274,60 @@ export function toggleDevice(s: SimState, uid: number): string | null { return null; } +export function shipGrade(s: SimState, gradeId: "mini" | "pro" | "max"): string | null { + const grade = GRADES.find((g) => g.id === gradeId); + if (!grade) return "unknown grade"; + const existing = s.shippedGrades.find((g) => g.grade === gradeId); + if (existing && existing.version === s.tier) return "that grade is already on this version"; + const basis = BAL.TIERS[s.tier].tokens ?? 2000; // v1 ships off a small basis + const cost = Math.round(basis * grade.costMult); + if (s.trainTokens < cost) return `need ${cost} training tokens`; + s.trainTokens -= cost; + const label = `${s.modelName} ${VERSION_NAMES[s.tier]}${grade.suffix}`; + if (existing) { + existing.version = s.tier; + existing.distilled = false; // fresh weights, fresh moat + pushEvent(s, `🚀 ${label} SHIPPED — distillation moat restored`, "good"); + } else { + s.shippedGrades.push({ grade: gradeId, version: s.tier, distilled: false }); + pushEvent(s, `🚀 ${label} SHIPPED — now hostable on the API market`, "good"); + } + return null; +} + +export function switchHosting(s: SimState, id: number | null): string | null { + if (id === s.hostedId) return "already serving that"; + if (id !== null) { + if (id < 0) { + const grade = GRADES[-id - 1]; + if (!grade || !s.shippedGrades.find((g) => g.grade === grade.id)) return "ship that grade first"; + } else if (!s.market.find((m) => m.id === id)) { + return "that model is gone"; + } + } + s.hostedId = id; + if (id === null) { + pushEvent(s, "API hosting stopped", "info"); + } else { + s.hostSwitchAt = s.t + 8; + pushEvent(s, "📦 Loading weights onto the cluster (8s)...", "info"); + } + return null; +} + +export function unlockScrape(s: SimState, id: string): string | null { + const node = SCRAPE_OPS.find((n) => n.id === id); + if (!node) return "unknown rig"; + if (s.scrapeNodes.includes(id)) return "already running"; + if (node.requires && !s.scrapeNodes.includes(node.requires)) + return `requires ${SCRAPE_OPS.find((n) => n.id === node.requires)?.name}`; + if (s.cash < node.cash) return "can't afford it"; + s.cash -= node.cash; + s.scrapeNodes.push(id); + pushEvent(s, `🕷 Scraping ops: ${node.name} online`, "good"); + return null; +} + function clapCost(s: SimState): number { return 50 * (s.round + 1); } @@ -400,15 +456,23 @@ export function step(s: SimState) { const dataMult = demand > 0 ? Math.min(1, uplink / demand) : 1; const tokEff = tokRaw * dataMult; - // --- economy --- + // --- economy: compute splits three ways (slop / training / hosting) --- const tm = tierMult(s); - const tokInf = tokEff * (1 - s.allocTraining) * dt; - const tokTrain = tokEff * s.allocTraining * dt; + const trainShare = Math.min(1, s.allocTraining); + const hostShare = Math.min(s.allocHosting, 1 - trainShare); + const tokInf = tokEff * (1 - trainShare - hostShare) * dt; + const tokTrain = tokEff * trainShare * dt; + const tokHost = tokEff * hostShare; // tok/s let income = tokInf * BAL.TOK_PRICE * tm; // baseline API drip // training fills the research bank; the Lab decides what it buys const src = DATA_SOURCES[s.dataSource]; - s.trainTokens += tokTrain * src.rate; + let dataRate = src.rate; + if (s.dataSource === "scraped") { + dataRate += s.scrapeNodes.reduce((a, id) => a + (SCRAPE_OPS.find((n) => n.id === id)?.scrapedRateAdd ?? 0), 0); + if (s.t < s.scrapeBanUntil) dataRate *= 0.15; // pipeline is IP-banned + } + s.trainTokens += tokTrain * dataRate; income -= tokTrain * src.costPerTok; // licensed data bills per token // platform adaptation drifts back down while you're not spamming @@ -462,8 +526,80 @@ export function step(s: SimState) { } income *= priceFx; + // --- API hosting market --- + if (s.nextMarketT === 0) { + // seed the launch-day market + for (let i = 0; i < 3; i++) + s.market.push(rollMarketModel(() => rand(s), s.nextMarketId++, s.t)); + s.nextMarketT = s.t + 90 + rand(s) * 120; + } else if (s.t >= s.nextMarketT) { + const drop = rollMarketModel(() => rand(s), s.nextMarketId++, s.t); + for (const m of s.market) m.margin *= 0.72; // hot drop craters the incumbents + s.market.push(drop); + while (s.market.length > 5) { + const i = s.market.findIndex((m) => m.id !== s.hostedId); + if (i < 0) break; + s.market.splice(i, 1); + } + pushEvent(s, `${drop.flag} ${drop.name} just dropped — hosting margins crater`, "rival"); + s.nextMarketT = s.t + 90 + rand(s) * 150; + } + for (const m of s.market) m.margin *= 1 - 0.0003 * dt; // slow fade as hype cools + + let hostIncome = 0; + let hostingOwnMax = false; + const loading = s.hostSwitchAt !== null && s.t < s.hostSwitchAt; + if (s.hostSwitchAt !== null && s.t >= s.hostSwitchAt) { + s.hostSwitchAt = null; + pushEvent(s, "📦 Weights loaded — serving traffic", "info"); + } + if (s.hostedId !== null && !loading && tokHost > 0) { + if (s.hostedId < 0) { + const grade = GRADES[-s.hostedId - 1]; + const shipped = s.shippedGrades.find((g) => g.grade === grade.id); + if (shipped) { + const margin = grade.margin * (shipped.distilled ? DISTILL_MARGIN_CUT : 1); + hostIncome = Math.min(tokHost, grade.demand) * margin * priceFx; + if (grade.hypeMult && !shipped.distilled) hostingOwnMax = grade.id === "max"; + // distillation roll — hosting your own weights is how they steal them + if (s.nextDistillT === 0) s.nextDistillT = s.t + 100 + rand(s) * 120; + else if (s.t >= s.nextDistillT) { + if (!shipped.distilled && rand(s) < grade.distillChance) { + shipped.distilled = true; + const thief = rollMarketModel(() => rand(s), s.nextMarketId++, s.t); + thief.margin *= 1.3; + s.market.push(thief); + pushEvent(s, `🥷 YOU GOT DISTILLED — ${thief.flag} ${thief.name} is a suspiciously good clone of ${s.modelName} ${VERSION_NAMES[shipped.version]}${GRADES.find((g) => g.id === shipped.grade)!.suffix} (margin gutted this version)`, "bad"); + } + s.nextDistillT = s.t + 100 + rand(s) * 120; + } + } + } else { + const m = s.market.find((x) => x.id === s.hostedId); + if (m) hostIncome = Math.min(tokHost, m.demand) * m.margin * priceFx; + else { s.hostedId = null; pushEvent(s, "Hosted model delisted — allocate elsewhere", "info"); } + } + income += hostIncome * dt; + } + + // --- scraping ops: IP bans while living off scraped data --- + if (s.dataSource === "scraped" && s.videos.length > 0) { + if (s.nextBanT === 0) s.nextBanT = s.t + 70 + rand(s) * 80; + else if (s.t >= s.nextBanT) { + const severity = 1 + Math.floor(rand(s) * 3); + const banLevel = Math.max(0, ...s.scrapeNodes.map((id) => SCRAPE_OPS.find((n) => n.id === id)?.banLevel ?? 0)); + if (severity > banLevel) { + s.scrapeBanUntil = s.t + 45; + pushEvent(s, `🚫 IP BANNED (severity ${severity}) — scraping pipeline offline 45s`, "bad"); + } else { + pushEvent(s, "🔁 Ban wave hit — proxies rotated, the scrape continues", "info"); + } + s.nextBanT = s.t + 70 + rand(s) * 80; + } + } + const adaptMult = (1 - s.platformAdapt * BAL.ADAPT_MAX_PENALTY) * hype * viewFx * - mods.viewsPerVideoMult * mods.conveyorHype; + mods.viewsPerVideoMult * mods.conveyorHype * (hostingOwnMax ? 1.1 : 1); for (const v of s.videos) { if (v.dead) continue; const age = s.t - v.born; @@ -527,6 +663,8 @@ export function step(s: SimState) { r.dataMult = dataMult; r.maxTemp = maxTemp; r.adapt = s.platformAdapt; + r.hostIncome += (hostIncome - r.hostIncome) * sm; + r.hostTok = tokHost; } export function shopList(s: SimState) { diff --git a/src/sim/state.ts b/src/sim/state.ts index 8af3706..d7c3824 100644 --- a/src/sim/state.ts +++ b/src/sim/state.ts @@ -54,6 +54,20 @@ export interface SimState { // Corporate R&D perkPoints: number; rndNodes: string[]; + // Model line & API market + modelName: string; + shippedGrades: { grade: "mini" | "pro" | "max"; version: number; distilled: boolean }[]; + allocHosting: number; // 0..1 share of compute sold on the API market + market: import("./research").MarketModel[]; + nextMarketId: number; + hostedId: number | null; // market row id currently hosted (negative ids = your grades) + hostSwitchAt: number | null; // weights loading until this sim-time + nextMarketT: number; + nextDistillT: number; + // Scraping ops + scrapeNodes: string[]; + scrapeBanUntil: number; // scraped training offline until this sim-time + nextBanT: number; // breaker grace bookkeeping overSince: number | null; dieselResetAt: number | null; @@ -70,6 +84,8 @@ export interface SimState { dataMult: number; maxTemp: number; adapt: number; + hostIncome: number; // $/s from API hosting + hostTok: number; // tok/s allocated to hosting }; events: { msg: string; t: number; kind: string }[]; rng: number; @@ -104,6 +120,18 @@ export function createState(): SimState { collapsed: false, perkPoints: 0, rndNodes: [], + modelName: "SlopGPT", + shippedGrades: [], + allocHosting: 0, + market: [], + nextMarketId: 1, + hostedId: null, + hostSwitchAt: null, + nextMarketT: 0, + nextDistillT: 0, + scrapeNodes: [], + scrapeBanUntil: 0, + nextBanT: 0, overSince: null, dieselResetAt: null, tokenBank: 0, @@ -111,7 +139,7 @@ export function createState(): SimState { 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, + adapt: 0, hostIncome: 0, hostTok: 0, }, events: [], rng: 0x5109a & 0xffffffff, diff --git a/src/ui/hud.ts b/src/ui/hud.ts index 4ae2a11..5aaeba5 100644 --- a/src/ui/hud.ts +++ b/src/ui/hud.ts @@ -3,6 +3,7 @@ import { BAL } from "../sim/balance"; import { SimState } from "../sim/state"; import { shopList, resetBreaker } from "../sim/sim"; import { sfx, setMuted, isMuted } from "./sfx"; +import { MODEL_NAME_POOL, VERSION_NAMES } from "../sim/research"; import TITLES from "../data/sloptitles.json"; export interface HudCallbacks { @@ -10,6 +11,7 @@ export interface HudCallbacks { onToggleOverlay: (which: "heat" | "power" | "data" | "grid") => void; onWireTool: () => void; onAlloc: (trainShare: number) => void; + onAllocHost: (hostShare: number) => void; onNewGame: () => void; onRaise: () => void; onClapback: () => void; @@ -24,6 +26,10 @@ export function randomCompanyName(): string { return pick(NAME_A) + pick(NAME_B) + pick(NAME_C); } +export function randomModelName(): string { + return MODEL_NAME_POOL[Math.floor(Math.random() * MODEL_NAME_POOL.length)]; +} + const fmt = (n: number, d = 2) => n >= 1e6 ? (n / 1e6).toFixed(1) + "M" : n >= 1e3 ? (n / 1e3).toFixed(1) + "K" : n.toFixed(d); @@ -47,6 +53,7 @@ export class Hud {
…and so does the model you'll inflict on the world.
+