M5: named model line, API hosting market, distillation, scraping ops
You name your model at incorporation and release versions 1/1.5/2/3, shipping each in mini/pro/MAX grades from the training bank (volume vs margin vs prestige). Compute now splits three ways: slop, training, and API hosting. A rotating market of third-party models (DeepSlop-236B, Kimi-Chi-7B, SloPangu-Air...) offers hosting margins that crater ~28% every time a hot new model drops. Hosting your own grades pays premium margin and +10% views for MAX — but rolls distillation: a rival lab clones your weights and guts that grade's margin until you re-ship it on a newer version. Scraping ops cash ladder (puppeteer scripts → proxy pool → residential botnet → captcha farm) buys training-rate bonuses and immunity against escalating IP-ban outages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c3d57e09b3
commit
bd8b84b332
16
CLAUDE.md
16
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.
|
||||
|
||||
14
src/main.ts
14
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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
156
src/sim/sim.ts
156
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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
<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">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">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>
|
||||
@ -60,6 +67,7 @@ export class Hud {
|
||||
<button class="chip" id="wire-tool">🔌 Wire (W)</button>
|
||||
<button class="chip" id="lab-btn">🧪 Model Lab (L)</button>
|
||||
<button class="chip" id="rnd-btn">🏢 R&D (R)</button>
|
||||
<button class="chip" id="market-btn">📡 Market (M)</button>
|
||||
<button class="chip" id="mute">${isMuted() ? "🔇 Muted" : "🔊 Sound"}</button>
|
||||
<button class="chip" id="new-game">🗑 New Game</button>
|
||||
<button class="chip" id="raise" style="display:none">💰 Raise</button>
|
||||
@ -69,8 +77,10 @@ export class Hud {
|
||||
</div>
|
||||
<button id="clap" style="display:none"></button>
|
||||
<div id="alloc-row">
|
||||
<span class="label" style="color:var(--dim);font-size:9px">TRAIN <b id="allocpct">25</b>% / INFER</span>
|
||||
<span class="label" style="color:var(--dim);font-size:9px">TRAIN <b id="allocpct">25</b>%</span>
|
||||
<input id="alloc" type="range" min="0" max="90" value="25" />
|
||||
<span class="label" style="color:var(--dim);font-size:9px">HOST <b id="hostpct">0</b>% · rest = slop</span>
|
||||
<input id="allochost" type="range" min="0" max="90" value="0" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="shop" class="panel"><h3>HARDWARE STORE</h3></div>
|
||||
@ -100,6 +110,11 @@ export class Hud {
|
||||
(this.root.querySelector("#allocpct") as HTMLElement).textContent = alloc.value;
|
||||
cb.onAlloc(Number(alloc.value) / 100);
|
||||
};
|
||||
const allochost = this.root.querySelector("#allochost") as HTMLInputElement;
|
||||
allochost.oninput = () => {
|
||||
(this.root.querySelector("#hostpct") as HTMLElement).textContent = allochost.value;
|
||||
cb.onAllocHost(Number(allochost.value) / 100);
|
||||
};
|
||||
}
|
||||
|
||||
bindBreaker(state: SimState) {
|
||||
@ -134,8 +149,8 @@ export class Hud {
|
||||
}
|
||||
}
|
||||
|
||||
/** modal for naming the company on a fresh game; resolves with the chosen name */
|
||||
showNameModal(): Promise<string> {
|
||||
/** modal for naming the company + model line on a fresh game */
|
||||
showNameModal(): Promise<{ company: string; model: string }> {
|
||||
return new Promise((resolve) => {
|
||||
const m = document.createElement("div");
|
||||
m.id = "name-modal";
|
||||
@ -147,22 +162,35 @@ export class Hud {
|
||||
<input id="cname" maxlength="28" value="${randomCompanyName()}" spellcheck="false"/>
|
||||
<button id="reroll" title="reroll">🎲</button>
|
||||
</div>
|
||||
<p style="margin-top:10px">…and so does the model you'll inflict on the world.</p>
|
||||
<div class="row">
|
||||
<input id="mname" maxlength="20" value="${randomModelName()}" spellcheck="false"/>
|
||||
<button id="mreroll" title="reroll">🎲</button>
|
||||
</div>
|
||||
<button id="cstart">INCORPORATE →</button>
|
||||
</div>`;
|
||||
this.root.appendChild(m);
|
||||
const input = m.querySelector("#cname") as HTMLInputElement;
|
||||
const minput = m.querySelector("#mname") as HTMLInputElement;
|
||||
(m.querySelector("#reroll") as HTMLButtonElement).onclick = () => {
|
||||
input.value = randomCompanyName();
|
||||
sfx.click();
|
||||
};
|
||||
(m.querySelector("#mreroll") as HTMLButtonElement).onclick = () => {
|
||||
minput.value = randomModelName();
|
||||
sfx.click();
|
||||
};
|
||||
const done = () => {
|
||||
const name = input.value.trim() || "SlopCo";
|
||||
m.remove();
|
||||
sfx.tierUp();
|
||||
resolve(name);
|
||||
resolve({
|
||||
company: input.value.trim() || "SlopCo",
|
||||
model: minput.value.trim() || "SlopGPT",
|
||||
});
|
||||
};
|
||||
(m.querySelector("#cstart") as HTMLButtonElement).onclick = done;
|
||||
input.onkeydown = (e) => { if (e.key === "Enter") done(); e.stopPropagation(); };
|
||||
for (const el of [input, minput])
|
||||
el.onkeydown = (e) => { if (e.key === "Enter") done(); e.stopPropagation(); };
|
||||
input.focus();
|
||||
input.select();
|
||||
});
|
||||
@ -231,7 +259,9 @@ export class Hud {
|
||||
(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 + (s.collapsed ? " 🌀" : "");
|
||||
q("#tier").textContent = `${s.modelName} ${VERSION_NAMES[s.tier]}` + (s.collapsed ? " 🌀" : "");
|
||||
q("#host").textContent = "$" + fmt(s.rates.hostIncome, 3) + "/s";
|
||||
(q("#host") as HTMLElement).style.color = s.rates.hostIncome > 0 ? "var(--green)" : "";
|
||||
const nt = BAL.TIERS[s.tier + 1];
|
||||
(q("#trainbar") as HTMLElement).style.width = nt ? Math.min(100, (s.trainTokens / nt.tokens!) * 100) + "%" : "100%";
|
||||
(q("#breaker-reset") as HTMLElement).style.display = s.breakerTripped ? "block" : "none";
|
||||
|
||||
@ -4,9 +4,11 @@ import { SimState } from "../sim/state";
|
||||
import { DEF } from "../sim/catalog";
|
||||
import {
|
||||
MODALITIES, SPECS, SPEC_COST_BY_GEN, DATA_SOURCES, RND, SOCKETS, SOCKET_SLOTS, DataSource,
|
||||
GRADES, VERSION_NAMES, SCRAPE_OPS,
|
||||
} from "../sim/research";
|
||||
import {
|
||||
unlockGen, unlockModality, setSpec, setDataSource, unlockRnd, buyUpgrade, toggleDevice, sell,
|
||||
shipGrade, switchHosting, unlockScrape,
|
||||
} from "../sim/sim";
|
||||
import { pushEvent } from "../sim/state";
|
||||
import { sfx } from "./sfx";
|
||||
@ -16,7 +18,7 @@ const fmt = (n: number) =>
|
||||
|
||||
export class Panels {
|
||||
state: SimState;
|
||||
open: "lab" | "rnd" | null = null;
|
||||
open: "lab" | "rnd" | "market" | null = null;
|
||||
selectedDevice: number | null = null;
|
||||
root: HTMLElement;
|
||||
|
||||
@ -28,7 +30,7 @@ export class Panels {
|
||||
this.root.addEventListener("click", (e) => this.dispatch(e));
|
||||
}
|
||||
|
||||
toggle(which: "lab" | "rnd") {
|
||||
toggle(which: "lab" | "rnd" | "market") {
|
||||
this.open = this.open === which ? null : which;
|
||||
sfx.click();
|
||||
this.render();
|
||||
@ -53,6 +55,9 @@ export class Panels {
|
||||
case "spec": err = setSpec(s, arg!); break;
|
||||
case "data": err = setDataSource(s, arg as DataSource); break;
|
||||
case "rnd": err = unlockRnd(s, arg!); break;
|
||||
case "ship": err = shipGrade(s, arg as "mini" | "pro" | "max"); break;
|
||||
case "host": err = switchHosting(s, arg === "null" ? null : Number(arg)); break;
|
||||
case "scrape": err = unlockScrape(s, arg!); break;
|
||||
case "upg": err = buyUpgrade(s, this.selectedDevice!, arg!); break;
|
||||
case "toggle": err = toggleDevice(s, this.selectedDevice!); break;
|
||||
case "sell-dev":
|
||||
@ -71,6 +76,7 @@ export class Panels {
|
||||
let html = "";
|
||||
if (this.open === "lab") html += this.labHtml(s);
|
||||
if (this.open === "rnd") html += this.rndHtml(s);
|
||||
if (this.open === "market") html += this.marketHtml(s);
|
||||
if (this.selectedDevice !== null) {
|
||||
const d = s.devices.find((x) => x.uid === this.selectedDevice);
|
||||
if (d) html += this.deviceHtml(s, d);
|
||||
@ -113,10 +119,63 @@ export class Panels {
|
||||
const d = DATA_SOURCES[k];
|
||||
return `<button class="node ${s.dataSource === k ? "owned" : "buy"}" data-cmd="data" data-arg="${k}">${d.name}<small>${d.desc}</small></button>`;
|
||||
}).join("")}
|
||||
${s.dataSource === "scraped" ? `<h4>SCRAPING OPS</h4>
|
||||
${SCRAPE_OPS.map((n) => {
|
||||
if (s.scrapeNodes.includes(n.id)) return `<div class="node owned">${n.name}<small>${n.desc}</small></div>`;
|
||||
if (n.requires && !s.scrapeNodes.includes(n.requires))
|
||||
return `<div class="node dim">${n.name}<small>requires ${SCRAPE_OPS.find((x) => x.id === n.requires)?.name}</small></div>`;
|
||||
return `<button class="node buy ${s.cash >= n.cash ? "" : "dim"}" data-cmd="scrape" data-arg="${n.id}">${n.name}<small>${n.desc} · $${fmt(n.cash)}</small></button>`;
|
||||
}).join("")}` : ""}
|
||||
</div>
|
||||
<div class="mcol">
|
||||
<h4>SHIP GRADES — ${s.modelName} ${VERSION_NAMES[s.tier]}</h4>
|
||||
${GRADES.map((g) => {
|
||||
const basis = BAL.TIERS[s.tier].tokens ?? 2000;
|
||||
const cost = Math.round(basis * g.costMult);
|
||||
const shipped = s.shippedGrades.find((x) => x.grade === g.id);
|
||||
const label = `${s.modelName} ${VERSION_NAMES[s.tier]}${g.suffix}`;
|
||||
if (shipped && shipped.version === s.tier)
|
||||
return `<div class="node owned">${label} — LIVE${shipped.distilled ? " · 🥷 DISTILLED" : ""}<small>$${g.margin * 1000}/Ktok · demand ${g.demand} tok/s${g.hypeMult ? " · +10% views while hosted" : ""}</small></div>`;
|
||||
const stale = shipped ? ` (v${VERSION_NAMES[shipped.version]} live${shipped.distilled ? ", distilled" : ""})` : "";
|
||||
return `<button class="node buy ${s.trainTokens >= cost ? "" : "dim"}" data-cmd="ship" data-arg="${g.id}">Ship ${label}${stale}<small>$${g.margin * 1000}/Ktok · demand ${g.demand} tok/s · ${fmt(cost)} tok${shipped?.distilled ? " · restores moat" : ""}</small></button>`;
|
||||
}).join("")}
|
||||
<div class="node dim" style="border-style:dashed">Host your grades from the 📡 Market (M). Hosting your own weights invites distillation.</div>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
private marketHtml(s: SimState): string {
|
||||
const hosted = (id: number) => s.hostedId === id;
|
||||
const loading = s.hostSwitchAt !== null && s.t < s.hostSwitchAt;
|
||||
const row = (id: number, name: string, sub: string, live: boolean) => live
|
||||
? `<div class="node owned">${name} — ${loading ? "LOADING WEIGHTS…" : "SERVING"}<small>${sub}</small></div>`
|
||||
: `<button class="node buy" data-cmd="host" data-arg="${id}">${name}<small>${sub}</small></button>`;
|
||||
const yours = s.shippedGrades.map((g) => {
|
||||
const def = GRADES.find((x) => x.id === g.grade)!;
|
||||
const gid = -(GRADES.indexOf(def) + 1);
|
||||
const margin = def.margin * (g.distilled ? 0.4 : 1);
|
||||
return row(gid,
|
||||
`🏠 ${s.modelName} ${VERSION_NAMES[g.version]}${def.suffix}${g.distilled ? " 🥷" : ""}`,
|
||||
`$${(margin * 1000).toFixed(2)}/Ktok · demand ${def.demand} tok/s · distill risk ${Math.round(def.distillChance * 100)}%`,
|
||||
hosted(gid));
|
||||
}).join("");
|
||||
const listings = s.market.map((m) =>
|
||||
row(m.id, `${m.flag} ${m.name}`,
|
||||
`$${(m.margin * 1000).toFixed(2)}/Ktok · demand ${m.demand} tok/s`, hosted(m.id))).join("");
|
||||
return `<div class="modal panel" id="market">
|
||||
<div class="mhead"><h2>📡 API MARKET</h2>
|
||||
<span class="bank">${s.rates.hostTok.toFixed(0)} tok/s allocated · $${s.rates.hostIncome.toFixed(3)}/s</span>
|
||||
<button class="mclose" data-cmd="close">✕</button></div>
|
||||
<div class="mcols"><div class="mcol">
|
||||
<h4>YOUR MODELS</h4>
|
||||
${yours || `<div class="node dim">Nothing shipped — train grades in the 🧪 Lab</div>`}
|
||||
<h4>THIRD-PARTY LISTINGS</h4>
|
||||
${listings}
|
||||
${s.hostedId !== null ? `<button class="node buy" data-cmd="host" data-arg="null">⏹ Stop hosting<small>free the cluster for slop or training</small></button>` : ""}
|
||||
<div class="node dim" style="border-style:dashed">Set HOST % in the left controls to sell compute here. New drops crater incumbent margins — stay nimble.</div>
|
||||
</div></div></div>`;
|
||||
}
|
||||
|
||||
private rndHtml(s: SimState): string {
|
||||
const branches: ["ops", "growth", "legal"] = ["ops", "growth", "legal"];
|
||||
const titles = { ops: "OPS", growth: "GROWTH", legal: "LEGAL / PR" };
|
||||
|
||||
Loading…
Reference in New Issue
Block a user