M6: GPU card catalog, carriers, switches, storage, VRAM economy
Compute is componentized: 11 parody GPU cards (Ex-Mining P106, GFX 1060 Dumpster, Radish VII Furnace, RTX 4090 Gigachonk, H100 'The Vault', B200 BigChungus...) install into carrier slots — Budget PC takes one, the milk-crate rig four, the half rack eight with rack-only datacenter cards. Cards carry tok/watts/VRAM/heat individually; total cluster VRAM must hold the running model version or all token output starves. Storage devices (SSD shoebox / NVMe hoard / NAS tower) gate training: no checkpoint space, no progress. Network grows a switch layer with per-switch throughput caps between devices and uplinks. Shop grouped by category. Old saves migrate with equivalent card loadouts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
18
CLAUDE.md
@ -63,10 +63,20 @@ Date.now/Math.random inside sim).
|
||||
(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.
|
||||
✅ M6 hardware depth: GPU cards (GPUS in catalog.ts, 11 parody cards
|
||||
P106→BigChungus) install into carrier slots (PC=1, rig=4, half-rack=8
|
||||
+rackClass for datacenter cards); carriers have no intrinsic tok —
|
||||
cards provide tok/watts/VRAM/heat. VRAM totals gate ALL token output
|
||||
vs the running version's needs (TIERS vramGB, floor 0.25). Storage
|
||||
devices (shoebox/NVMe/NAS) gate training checkpoints (TIERS
|
||||
checkpointGB — training stalls without disk). Switch layer:
|
||||
device→switch→uplink with per-switch Mbps caps (no daisy chains).
|
||||
Shop has category headers. Old saves get equivalent card loadouts
|
||||
(save.ts migration).
|
||||
→ M7 backlog: ad-spend slider + email-spam campaigns, ACTIVE scraping
|
||||
minigame, hazards (thermal runaway fire), Tier 2 garage move,
|
||||
subscribers/SaaS churn revenue, Conveyor belt visual, per-carrier
|
||||
PSU capacity, unreliable used cards (degradation).
|
||||
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.
|
||||
|
||||
BIN
public/assets/gen/dumb_switch.png
Normal file
|
After Width: | Height: | Size: 356 KiB |
BIN
public/assets/gen/dumb_switch_cut.png
Normal file
|
After Width: | Height: | Size: 222 KiB |
BIN
public/assets/gen/gpu_budget.png
Normal file
|
After Width: | Height: | Size: 385 KiB |
BIN
public/assets/gen/gpu_budget_cut.png
Normal file
|
After Width: | Height: | Size: 243 KiB |
BIN
public/assets/gen/gpu_datacenter.png
Normal file
|
After Width: | Height: | Size: 530 KiB |
BIN
public/assets/gen/gpu_datacenter_cut.png
Normal file
|
After Width: | Height: | Size: 344 KiB |
BIN
public/assets/gen/gpu_gaming.png
Normal file
|
After Width: | Height: | Size: 458 KiB |
BIN
public/assets/gen/gpu_gaming_cut.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
public/assets/gen/gpu_workstation.png
Normal file
|
After Width: | Height: | Size: 406 KiB |
BIN
public/assets/gen/gpu_workstation_cut.png
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
public/assets/gen/managed_switch.png
Normal file
|
After Width: | Height: | Size: 472 KiB |
BIN
public/assets/gen/managed_switch_cut.png
Normal file
|
After Width: | Height: | Size: 327 KiB |
BIN
public/assets/gen/nas_tower.png
Normal file
|
After Width: | Height: | Size: 599 KiB |
BIN
public/assets/gen/nas_tower_cut.png
Normal file
|
After Width: | Height: | Size: 328 KiB |
BIN
public/assets/gen/nvme_hoard.png
Normal file
|
After Width: | Height: | Size: 360 KiB |
BIN
public/assets/gen/nvme_hoard_cut.png
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
public/assets/gen/ssd_shoebox.png
Normal file
|
After Width: | Height: | Size: 517 KiB |
BIN
public/assets/gen/ssd_shoebox_cut.png
Normal file
|
After Width: | Height: | Size: 468 KiB |
@ -1,6 +1,6 @@
|
||||
import { BAL } from "./sim/balance";
|
||||
import { createState, pushEvent } from "./sim/state";
|
||||
import { step, place, canPlace, wire, sell, grant, raiseRound, clapBack, shipGrade, switchHosting, unlockScrape } from "./sim/sim";
|
||||
import { step, place, canPlace, wire, sell, grant, raiseRound, clapBack, shipGrade, switchHosting, unlockScrape, buyGpu, sellGpu } from "./sim/sim";
|
||||
import { saveState, loadState, clearSave } from "./sim/save";
|
||||
import { DEF } from "./sim/catalog";
|
||||
import { Scene } from "./render/scene";
|
||||
@ -154,10 +154,11 @@ async function boot() {
|
||||
hud.update(state);
|
||||
|
||||
if (!loaded) {
|
||||
// starter bedroom: your old PC and router, not yet wired together
|
||||
// starter bedroom: your old PC (Dumpster inside) and router, not yet wired
|
||||
grant(state, "desk", 5, 7);
|
||||
grant(state, "desk_pc", 7, 7);
|
||||
grant(state, "desk_pc", 7, 7, ["gfx1060"]);
|
||||
grant(state, "router", 2, 1);
|
||||
grant(state, "ssd_shoebox", 0, 7);
|
||||
const names = await hud.showNameModal();
|
||||
state.company = names.company;
|
||||
state.modelName = names.model;
|
||||
@ -194,6 +195,8 @@ async function boot() {
|
||||
ship: (g: "mini" | "pro" | "max") => shipGrade(state, g),
|
||||
host: (id: number | null) => switchHosting(state, id),
|
||||
scrape: (id: string) => unlockScrape(state, id),
|
||||
gpu: (uid: number, id: string) => buyGpu(state, uid, id),
|
||||
sellGpu: (uid: number, slot: number) => sellGpu(state, uid, slot),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -54,11 +54,13 @@ export const BAL = {
|
||||
CLAPBACK_WINDOW_S: 20,
|
||||
CLAPBACK_SUCCESS: 0.6,
|
||||
|
||||
// Training / model tiers: tokens required to reach tier i+1, revenue multiplier per tier
|
||||
// Model versions: tokens = training cost to reach it, vramGB = cluster VRAM
|
||||
// needed to RUN it at full speed, checkpointGB = disk needed WHILE training it.
|
||||
TIERS: [
|
||||
{ name: "SlopLM-1B", mult: 1 },
|
||||
{ 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 },
|
||||
{ name: "v1", mult: 1, vramGB: 0, checkpointGB: 0 },
|
||||
{ name: "v1.5", mult: 2.2, tokens: 4_000, vramGB: 24, checkpointGB: 40 },
|
||||
{ name: "v2", mult: 5, tokens: 40_000, vramGB: 96, checkpointGB: 400 },
|
||||
{ name: "v3", mult: 12, tokens: 250_000, vramGB: 512, checkpointGB: 3_000 },
|
||||
],
|
||||
VRAM_STARVE_FLOOR: 0.25, // token multiplier floor when VRAM-starved
|
||||
};
|
||||
|
||||
@ -2,15 +2,20 @@ export interface DeviceDef {
|
||||
id: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
cat: "compute" | "network" | "storage" | "power" | "cooling" | "misc";
|
||||
cost: number;
|
||||
wattsIdle: number;
|
||||
wattsLoad: number; // at 100% utilization
|
||||
tokRate: number; // tokens/s at full clock (0 = not a compute device)
|
||||
heatOut: number; // heat units/s added to its tile at full load
|
||||
wattsLoad: number; // at 100% utilization (excluding installed GPU cards)
|
||||
tokRate: number; // intrinsic tokens/s (0 for carriers — cards provide it)
|
||||
heatOut: number; // heat units/s added to its tile at full load (excl. cards)
|
||||
gpuSlots?: number; // carrier capacity for GPU cards
|
||||
rackClass?: boolean; // may take rack-only cards
|
||||
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)
|
||||
switchMbps?: number; // switch-class throughput cap (wire devices→switch→uplink)
|
||||
ports?: number; // wired-device capacity for router/switch-class (default 4)
|
||||
storageGB?: number; // checkpoint capacity
|
||||
circuitBonus?: number; // extra breaker watts (electrical infrastructure)
|
||||
burnRate?: number; // $/s consumed while running (cash furnace)
|
||||
hypeMult?: number; // bonus view multiplier while burning (stacks multiplicatively)
|
||||
@ -20,72 +25,149 @@ export interface DeviceDef {
|
||||
unlockAt?: number; // cash-earned-total gate for the shop
|
||||
}
|
||||
|
||||
export interface GpuDef {
|
||||
id: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
cost: number;
|
||||
tok: number; // tokens/s
|
||||
watts: number;
|
||||
vram: number; // GB
|
||||
heat: number; // heat units/s at full load
|
||||
rackOnly?: boolean;
|
||||
sprite: "gpu_budget" | "gpu_gaming" | "gpu_workstation" | "gpu_datacenter";
|
||||
unlockAt?: number;
|
||||
}
|
||||
|
||||
export const GPUS: GpuDef[] = [
|
||||
{ id: "p106", name: "Ex-Mining P106", cost: 25, tok: 8, watts: 90, vram: 6, heat: 0.8, sprite: "gpu_budget",
|
||||
desc: "No video out. Smells like a Kazakh warehouse." },
|
||||
{ id: "gfx1060", name: "nGreedia GFX 1060 Dumpster", cost: 60, tok: 10, watts: 120, vram: 6, heat: 1.2, sprite: "gpu_budget",
|
||||
desc: "The people's slop engine." },
|
||||
{ id: "radish580", name: "AMD Radish RX 580", cost: 80, tok: 14, watts: 185, vram: 8, heat: 2.2, sprite: "gpu_budget",
|
||||
desc: "Cheap tokens, free space heater." },
|
||||
{ id: "mtt_s80", name: "Moore Slops MTT S80", cost: 45, tok: 18, watts: 250, vram: 16, heat: 3.0, sprite: "gpu_gaming",
|
||||
desc: "Mystery-meat silicon. Absurd VRAM for the price, drinks power." },
|
||||
{ id: "sweatbox", name: "nGreedia GFX 2070 Sweatbox", cost: 140, tok: 22, watts: 175, vram: 8, heat: 1.6, sprite: "gpu_gaming",
|
||||
desc: "Runs warm. Runs slop. Runs up your bill." },
|
||||
{ id: "radish7", name: "AMD Radish VII Furnace", cost: 450, tok: 40, watts: 300, vram: 16, heat: 3.5, sprite: "gpu_gaming",
|
||||
desc: "The name is a warranty disclaimer.", unlockAt: 150 },
|
||||
{ id: "chonker", name: "nGreedia RTX 3090 Chonker", cost: 700, tok: 55, watts: 350, vram: 24, heat: 3.2, sprite: "gpu_gaming",
|
||||
desc: "Three slots of pure appetite.", unlockAt: 150 },
|
||||
{ id: "adult", name: "nGreedia A6000 Adult Card", cost: 2200, tok: 90, watts: 300, vram: 48, heat: 2.2, sprite: "gpu_workstation",
|
||||
desc: "Blower-style. HR-approved. Quietly excellent.", unlockAt: 900 },
|
||||
{ id: "gigachonk", name: "nGreedia RTX 4090 Gigachonk", cost: 1800, tok: 110, watts: 450, vram: 24, heat: 4.0, sprite: "gpu_gaming",
|
||||
desc: "Melts its own power connector as a party trick.", unlockAt: 900 },
|
||||
{ id: "vault", name: "nGreedia H100 'The Vault'", cost: 12000, tok: 320, watts: 700, vram: 80, heat: 5.5, rackOnly: true, sprite: "gpu_datacenter",
|
||||
desc: "Export-controlled. Rack only. Your accountant weeps.", unlockAt: 5000 },
|
||||
{ id: "bigchungus", name: "nGreedia B200 BigChungus", cost: 35000, tok: 700, watts: 1000, vram: 192, heat: 7.5, rackOnly: true, sprite: "gpu_datacenter",
|
||||
desc: "One card. One kilowatt. One county's grid.", unlockAt: 25000 },
|
||||
];
|
||||
|
||||
export const GPU_DEF = Object.fromEntries(GPUS.map((g) => [g.id, g]));
|
||||
|
||||
export const CATALOG: DeviceDef[] = [
|
||||
// --- compute carriers (GPUs slot in; click a placed carrier to install cards) ---
|
||||
{
|
||||
id: "desk", name: "Desk", cost: 15,
|
||||
desc: "A surface for your empire. Purely moral support.",
|
||||
wattsIdle: 0, wattsLoad: 0, tokRate: 0, heatOut: 0,
|
||||
footprint: { w: 2, h: 1 }, sprite: "desk",
|
||||
},
|
||||
{
|
||||
id: "desk_pc", name: "Budget PC", cost: 60,
|
||||
desc: "i5 + GTX 1060. Generates 10 tok/s of pure slop.",
|
||||
wattsIdle: 45, wattsLoad: 350, tokRate: 10, heatOut: 3.2,
|
||||
id: "desk_pc", name: "Budget PC", cat: "compute", cost: 60,
|
||||
desc: "i5, one GPU slot, questionable airflow. Cards sold separately.",
|
||||
wattsIdle: 45, wattsLoad: 80, tokRate: 0, heatOut: 0.6, gpuSlots: 1,
|
||||
footprint: { w: 1, h: 1 }, sprite: "desk_pc",
|
||||
},
|
||||
{
|
||||
id: "router", name: "Wi-Fi Router", cost: 40,
|
||||
desc: "100 Mbps uplink. Slop needs pipes. Wire PCs to it.",
|
||||
id: "gpu_rig", name: "Milk-Crate Rig Frame", cat: "compute", cost: 120,
|
||||
desc: "Four open-air GPU slots held together by zip ties and faith.",
|
||||
wattsIdle: 30, wattsLoad: 60, tokRate: 0, heatOut: 0.5, gpuSlots: 4,
|
||||
footprint: { w: 2, h: 1 }, sprite: "gpu_rig", unlockAt: 150,
|
||||
},
|
||||
{
|
||||
id: "server_rack_mini", name: "Half Rack", cat: "compute", cost: 1200,
|
||||
desc: "Eight slots, takes datacenter cards. eBay provenance unclear.",
|
||||
wattsIdle: 150, wattsLoad: 250, tokRate: 0, heatOut: 2, gpuSlots: 8, rackClass: true,
|
||||
footprint: { w: 2, h: 2 }, sprite: "server_rack_mini", unlockAt: 900,
|
||||
},
|
||||
// --- network ---
|
||||
{
|
||||
id: "router", name: "Wi-Fi Router", cat: "network", cost: 40,
|
||||
desc: "100 Mbps uplink, 4 ports. Slop needs pipes.",
|
||||
wattsIdle: 12, wattsLoad: 12, tokRate: 0, heatOut: 0.2,
|
||||
uplinkMbps: 100, footprint: { w: 1, h: 1 }, sprite: "router",
|
||||
},
|
||||
{
|
||||
id: "desk_fan", name: "Desk Fan", cost: 18,
|
||||
desc: "Moves hot air somewhere else. Somewhere is your problem.",
|
||||
wattsIdle: 45, wattsLoad: 45, tokRate: 0, heatOut: 0,
|
||||
fan: true, footprint: { w: 1, h: 1 }, sprite: "desk_fan",
|
||||
id: "dumb_switch", name: "5-Port Dumb Switch", cat: "network", cost: 60,
|
||||
desc: "Wire rigs to it, wire it to an uplink. 400 Mbps through, 5 ports.",
|
||||
wattsIdle: 8, wattsLoad: 8, tokRate: 0, heatOut: 0.1,
|
||||
switchMbps: 400, ports: 5, footprint: { w: 1, h: 1 }, sprite: "dumb_switch", unlockAt: 150,
|
||||
},
|
||||
{
|
||||
id: "power_strip", name: "Power Strip", cost: 12,
|
||||
id: "managed_switch", name: "Managed Switch", cat: "network", cost: 350,
|
||||
desc: "2 Gbps of aggregation, 8 ports, a web UI from 2009.",
|
||||
wattsIdle: 30, wattsLoad: 30, tokRate: 0, heatOut: 0.5,
|
||||
switchMbps: 2000, ports: 8, footprint: { w: 1, h: 1 }, sprite: "managed_switch", unlockAt: 900,
|
||||
},
|
||||
{
|
||||
id: "fiber_modem", name: "Fiber Uplink", cat: "network", cost: 600,
|
||||
desc: "1 Gbps out of the building, 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,
|
||||
},
|
||||
// --- storage (training checkpoints need somewhere to live) ---
|
||||
{
|
||||
id: "ssd_shoebox", name: "SSD Shoebox", cat: "storage", cost: 40,
|
||||
desc: "500 GB of loose SATA drives. Checkpoint storage, technically.",
|
||||
wattsIdle: 5, wattsLoad: 10, tokRate: 0, heatOut: 0.1, storageGB: 500,
|
||||
footprint: { w: 1, h: 1 }, sprite: "ssd_shoebox",
|
||||
},
|
||||
{
|
||||
id: "nvme_hoard", name: "NVMe Hoard", cat: "storage", cost: 180,
|
||||
desc: "2 TB of M.2 sticks in an anti-static tray. Fast checkpoint city.",
|
||||
wattsIdle: 8, wattsLoad: 15, tokRate: 0, heatOut: 0.3, storageGB: 2000,
|
||||
footprint: { w: 1, h: 1 }, sprite: "nvme_hoard", unlockAt: 300,
|
||||
},
|
||||
{
|
||||
id: "nas_tower", name: "NAS Tower", cat: "storage", cost: 700,
|
||||
desc: "12 TB across six bays. One drive is definitely dying.",
|
||||
wattsIdle: 40, wattsLoad: 90, tokRate: 0, heatOut: 1.2, storageGB: 12000,
|
||||
footprint: { w: 1, h: 1 }, sprite: "nas_tower", unlockAt: 900,
|
||||
},
|
||||
// --- power ---
|
||||
{
|
||||
id: "power_strip", name: "Power Strip", cat: "power", cost: 12,
|
||||
desc: "Scorched but trustworthy. +200W breaker headroom (don't ask).",
|
||||
wattsIdle: 0, wattsLoad: 0, tokRate: 0, heatOut: 0.4,
|
||||
footprint: { w: 1, h: 1 }, sprite: "power_strip",
|
||||
},
|
||||
{
|
||||
id: "gpu_rig", name: "Milk-Crate GPU Rig", cost: 420,
|
||||
desc: "Four mismatched GPUs, zero regrets. 45 tok/s, big heat.",
|
||||
wattsIdle: 90, wattsLoad: 900, tokRate: 45, heatOut: 9,
|
||||
footprint: { w: 2, h: 1 }, sprite: "gpu_rig", unlockAt: 150,
|
||||
},
|
||||
{
|
||||
id: "window_ac", name: "Window AC", cost: 260,
|
||||
desc: "Drips on the carpet. Deletes heat from the room.",
|
||||
wattsIdle: 60, wattsLoad: 800, tokRate: 0, heatOut: 0,
|
||||
coolRate: 14, wallOnly: true, footprint: { w: 1, h: 1 },
|
||||
sprite: "window_ac", unlockAt: 150,
|
||||
},
|
||||
{
|
||||
id: "breaker_panel", name: "20A Sub-Panel", cost: 350,
|
||||
id: "breaker_panel", name: "20A Sub-Panel", cat: "power", 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,
|
||||
},
|
||||
// --- cooling ---
|
||||
{
|
||||
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: "desk_fan", name: "Desk Fan", cat: "cooling", cost: 18,
|
||||
desc: "Moves hot air somewhere else. Somewhere is your problem.",
|
||||
wattsIdle: 45, wattsLoad: 45, tokRate: 0, heatOut: 0,
|
||||
fan: true, footprint: { w: 1, h: 1 }, sprite: "desk_fan",
|
||||
},
|
||||
{
|
||||
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: "window_ac", name: "Window AC", cat: "cooling", cost: 260,
|
||||
desc: "Drips on the carpet. Deletes heat from the room.",
|
||||
wattsIdle: 60, wattsLoad: 800, tokRate: 0, heatOut: 0,
|
||||
coolRate: 14, wallOnly: true, footprint: { w: 1, h: 1 },
|
||||
sprite: "window_ac", unlockAt: 150,
|
||||
},
|
||||
// --- misc ---
|
||||
{
|
||||
id: "desk", name: "Desk", cat: "misc", cost: 15,
|
||||
desc: "A surface for your empire. Purely moral support.",
|
||||
wattsIdle: 0, wattsLoad: 0, tokRate: 0, heatOut: 0,
|
||||
footprint: { w: 2, h: 1 }, sprite: "desk",
|
||||
},
|
||||
{
|
||||
id: "cash_furnace", name: "Cash Furnace", cost: 2500,
|
||||
id: "cash_furnace", name: "Cash Furnace", cat: "misc", 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 },
|
||||
@ -93,4 +175,9 @@ export const CATALOG: DeviceDef[] = [
|
||||
},
|
||||
];
|
||||
|
||||
/** carrier or intrinsic-compute check — GPUs make carriers produce */
|
||||
export function isCompute(def: DeviceDef): boolean {
|
||||
return def.tokRate > 0 || (def.gpuSlots ?? 0) > 0;
|
||||
}
|
||||
|
||||
export const DEF = Object.fromEntries(CATALOG.map((d) => [d.id, d]));
|
||||
|
||||
@ -22,7 +22,16 @@ export function loadState(): SimState | null {
|
||||
delete (s as unknown as Record<string, unknown>).v;
|
||||
s.heat = new Float32Array(BAL.ROOM_W * BAL.ROOM_H);
|
||||
s.heat.set((data.heat as number[]).slice(0, s.heat.length));
|
||||
for (const d of s.devices) d.upgrades ??= []; // pre-sockets saves
|
||||
for (const d of s.devices) {
|
||||
d.upgrades ??= []; // pre-sockets saves
|
||||
// pre-GPU-catalog saves: carriers had intrinsic tok — grant equivalent cards
|
||||
if (d.gpus === undefined) {
|
||||
d.gpus =
|
||||
d.defId === "desk_pc" ? ["gfx1060"] :
|
||||
d.defId === "gpu_rig" ? ["sweatbox", "sweatbox"] :
|
||||
d.defId === "server_rack_mini" ? ["chonker", "chonker"] : [];
|
||||
}
|
||||
}
|
||||
return s;
|
||||
} catch (e) {
|
||||
console.warn("corrupt save, starting fresh", e);
|
||||
|
||||
153
src/sim/sim.ts
@ -1,5 +1,5 @@
|
||||
import { BAL } from "./balance";
|
||||
import { CATALOG, DEF, DeviceDef } from "./catalog";
|
||||
import { CATALOG, DEF, DeviceDef, GPUS, GPU_DEF, isCompute } from "./catalog";
|
||||
import { Device, SimState, pushEvent, rand } from "./state";
|
||||
import {
|
||||
computeMods, deviceMods, MODALITIES, SPECS, SPEC_COST_BY_GEN,
|
||||
@ -53,18 +53,33 @@ export function place(s: SimState, defId: string, x: number, y: number): Device
|
||||
s.cash -= def.cost;
|
||||
const d: Device = {
|
||||
uid: s.nextUid++, defId, x, y, on: true, powered: false,
|
||||
wiredTo: null, temp: BAL.AMBIENT_C, perf: 1, util: 0, upgrades: [],
|
||||
wiredTo: null, temp: BAL.AMBIENT_C, perf: 1, util: 0, upgrades: [], gpus: [],
|
||||
};
|
||||
s.devices.push(d);
|
||||
pushEvent(s, `Installed ${def.name}`, "buy");
|
||||
return d;
|
||||
}
|
||||
|
||||
export function cardStats(d: Device) {
|
||||
let tok = 0, watts = 0, vram = 0, heat = 0;
|
||||
for (const id of d.gpus) {
|
||||
const g = GPU_DEF[id];
|
||||
if (!g) continue;
|
||||
tok += g.tok; watts += g.watts; vram += g.vram; heat += g.heat;
|
||||
}
|
||||
return { tok, watts, vram, heat };
|
||||
}
|
||||
|
||||
/** intrinsic + installed-card token rate */
|
||||
export function deviceTokRate(d: Device): number {
|
||||
return DEF[d.defId].tokRate + cardStats(d).tok;
|
||||
}
|
||||
|
||||
/** place without cost/afford checks — starter room setup */
|
||||
export function grant(s: SimState, defId: string, x: number, y: number): Device {
|
||||
export function grant(s: SimState, defId: string, x: number, y: number, gpus: string[] = []): Device {
|
||||
const d: Device = {
|
||||
uid: s.nextUid++, defId, x, y, on: true, powered: false,
|
||||
wiredTo: null, temp: BAL.AMBIENT_C, perf: 1, util: 0, upgrades: [],
|
||||
wiredTo: null, temp: BAL.AMBIENT_C, perf: 1, util: 0, upgrades: [], gpus,
|
||||
};
|
||||
s.devices.push(d);
|
||||
return d;
|
||||
@ -81,17 +96,20 @@ export function sell(s: SimState, uid: number) {
|
||||
pushEvent(s, `Sold ${def.name} (+$${Math.round(def.cost * 0.5)})`, "info");
|
||||
}
|
||||
|
||||
export function wire(s: SimState, deviceUid: number, routerUid: number): string | null {
|
||||
export function wire(s: SimState, deviceUid: number, targetUid: 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 >= ports) return `router ports full (${ports})`;
|
||||
dev.wiredTo = routerUid;
|
||||
pushEvent(s, `Wired ${DEF[dev.defId].name} to ${DEF[rt.defId].name}`, "info");
|
||||
const tgt = s.devices.find((d) => d.uid === targetUid);
|
||||
if (!dev || !tgt) return "missing device";
|
||||
const devDef = DEF[dev.defId], tgtDef = DEF[tgt.defId];
|
||||
const devIsSwitch = !!devDef.switchMbps;
|
||||
if (!isCompute(devDef) && !devIsSwitch) return "nothing to wire there";
|
||||
if (!tgtDef.uplinkMbps && !tgtDef.switchMbps) return "that's not a router or switch";
|
||||
if (devIsSwitch && tgtDef.switchMbps) return "no switch-to-switch daisy chains (yet)";
|
||||
const ports = tgtDef.ports ?? 4;
|
||||
const used = s.devices.filter((d) => d.wiredTo === targetUid).length;
|
||||
if (used >= ports) return `ports full (${ports})`;
|
||||
dev.wiredTo = targetUid;
|
||||
pushEvent(s, `Wired ${devDef.name} to ${tgtDef.name}`, "info");
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -256,7 +274,7 @@ export function buyUpgrade(s: SimState, uid: number, upgId: string): string | nu
|
||||
const d = s.devices.find((x) => x.uid === uid);
|
||||
const upg = SOCKETS.find((u) => u.id === upgId);
|
||||
if (!d || !upg) return "missing device or upgrade";
|
||||
if (!DEF[d.defId].tokRate) return "only compute hardware takes upgrades";
|
||||
if (!isCompute(DEF[d.defId])) return "only compute hardware takes upgrades";
|
||||
if (d.upgrades.length >= SOCKET_SLOTS) return "no free sockets";
|
||||
if (d.upgrades.includes(upgId)) return "already installed";
|
||||
if (s.cash < upg.cost) return "can't afford it";
|
||||
@ -315,6 +333,34 @@ export function switchHosting(s: SimState, id: number | null): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buyGpu(s: SimState, uid: number, gpuId: string): string | null {
|
||||
const d = s.devices.find((x) => x.uid === uid);
|
||||
const g = GPU_DEF[gpuId];
|
||||
if (!d || !g) return "missing carrier or card";
|
||||
const def = DEF[d.defId];
|
||||
if (!def.gpuSlots) return "that doesn't take GPU cards";
|
||||
if (d.gpus.length >= def.gpuSlots) return "no free GPU slots";
|
||||
if (g.rackOnly && !def.rackClass) return "datacenter card — rack only";
|
||||
if ((g.unlockAt ?? 0) > s.earnedTotal) return `locked — earn $${g.unlockAt} total first`;
|
||||
if (s.cash < g.cost) return "can't afford it";
|
||||
s.cash -= g.cost;
|
||||
d.gpus.push(gpuId);
|
||||
pushEvent(s, `🎮 ${g.name} installed in ${def.name} (+${g.tok} tok/s)`, "info");
|
||||
return null;
|
||||
}
|
||||
|
||||
export function sellGpu(s: SimState, uid: number, slot: number): string | null {
|
||||
const d = s.devices.find((x) => x.uid === uid);
|
||||
if (!d || slot < 0 || slot >= d.gpus.length) return "no card there";
|
||||
const g = GPU_DEF[d.gpus[slot]];
|
||||
d.gpus.splice(slot, 1);
|
||||
s.cash += Math.round(g.cost * 0.5);
|
||||
pushEvent(s, `Sold ${g.name} (+$${Math.round(g.cost * 0.5)})`, "info");
|
||||
return null;
|
||||
}
|
||||
|
||||
export { GPUS };
|
||||
|
||||
export function unlockScrape(s: SimState, id: string): string | null {
|
||||
const node = SCRAPE_OPS.find((n) => n.id === id);
|
||||
if (!node) return "unknown rig";
|
||||
@ -358,13 +404,14 @@ export function step(s: SimState) {
|
||||
for (const d of s.devices) {
|
||||
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.util = d.on ? (isCompute(def) ? (d.wiredTo !== null ? 1 : 0.1) : 1) : 0;
|
||||
// furnaces don't care about your breaker — fire predates electricity
|
||||
d.powered = def.burnRate ? d.on : d.on && !s.breakerTripped;
|
||||
if (d.powered) {
|
||||
const dm = deviceMods(d.upgrades);
|
||||
const loadScale = def.tokRate ? mods.wattsPerTokMult * dm.watts : 1;
|
||||
const loadScale = isCompute(def) ? mods.wattsPerTokMult * dm.watts : 1;
|
||||
watts += def.wattsIdle + (def.wattsLoad - def.wattsIdle) * d.util * loadScale;
|
||||
watts += cardStats(d).watts * d.util * mods.wattsPerTokMult * dm.watts;
|
||||
}
|
||||
}
|
||||
if (!s.breakerTripped && watts > limit) {
|
||||
@ -398,9 +445,10 @@ export function step(s: SimState) {
|
||||
for (const d of s.devices) {
|
||||
const def = DEF[d.defId];
|
||||
if (!d.powered) continue;
|
||||
if (def.heatOut)
|
||||
heat[idx(d.x, d.y)] += def.heatOut * d.util * d.perf * dt * 2.2 *
|
||||
(def.tokRate ? mods.heatMult * deviceMods(d.upgrades).heat : 1);
|
||||
const cardHeat = cardStats(d).heat;
|
||||
if (def.heatOut || cardHeat)
|
||||
heat[idx(d.x, d.y)] += (def.heatOut + cardHeat) * d.util * d.perf * dt * 2.2 *
|
||||
(isCompute(def) ? mods.heatMult * deviceMods(d.upgrades).heat : 1);
|
||||
if (def.coolRate)
|
||||
for (let x = 0; x < W; x++) for (let y = 0; y < H; y++) {
|
||||
const dist = Math.abs(x - d.x) + Math.abs(y - d.y);
|
||||
@ -432,29 +480,56 @@ export function step(s: SimState) {
|
||||
let maxTemp = BAL.AMBIENT_C;
|
||||
for (const d of s.devices) {
|
||||
const def = DEF[d.defId];
|
||||
d.temp = BAL.AMBIENT_C + s.heat[idx(d.x, d.y)] + (def.tokRate ? d.util * 18 : 0);
|
||||
d.temp = BAL.AMBIENT_C + s.heat[idx(d.x, d.y)] + (isCompute(def) ? d.util * 18 : 0);
|
||||
maxTemp = Math.max(maxTemp, d.temp);
|
||||
if (def.tokRate) {
|
||||
if (isCompute(def)) {
|
||||
const over = d.temp - BAL.THROTTLE_C;
|
||||
d.perf = over <= 0 ? 1 : Math.max(BAL.THROTTLE_FLOOR, 1 - over / 30);
|
||||
}
|
||||
}
|
||||
|
||||
// --- data / uplink ---
|
||||
// --- data topology: device → (switch →) uplink ---
|
||||
let uplink = 0;
|
||||
for (const d of s.devices)
|
||||
if (DEF[d.defId].uplinkMbps && d.powered) uplink += DEF[d.defId].uplinkMbps!;
|
||||
let tokRaw = 0;
|
||||
const mbpsPerTok = BAL.MBPS_PER_TOK * mods.mbpsPerTokMult;
|
||||
const byId = new Map(s.devices.map((d) => [d.uid, d]));
|
||||
let tokDirect = 0;
|
||||
const perSwitch = new Map<number, number>(); // switch uid -> raw tok/s arriving
|
||||
for (const d of s.devices) {
|
||||
const def = DEF[d.defId];
|
||||
if (def.tokRate && d.powered && d.wiredTo !== null) {
|
||||
const routerAlive = s.devices.some((r) => r.uid === d.wiredTo && r.powered);
|
||||
if (routerAlive) tokRaw += def.tokRate * d.perf * mods.tokRateMult * deviceMods(d.upgrades).tok;
|
||||
if (!isCompute(def) || !d.powered || d.wiredTo === null) continue;
|
||||
const tgt = byId.get(d.wiredTo);
|
||||
if (!tgt || !tgt.powered) continue;
|
||||
const devTok = deviceTokRate(d) * d.perf * mods.tokRateMult * deviceMods(d.upgrades).tok;
|
||||
const tgtDef = DEF[tgt.defId];
|
||||
if (tgtDef.uplinkMbps) {
|
||||
tokDirect += devTok;
|
||||
} else if (tgtDef.switchMbps) {
|
||||
// switch must itself reach a live uplink
|
||||
const up = tgt.wiredTo !== null ? byId.get(tgt.wiredTo) : undefined;
|
||||
if (up && up.powered && DEF[up.defId].uplinkMbps)
|
||||
perSwitch.set(tgt.uid, (perSwitch.get(tgt.uid) ?? 0) + devTok);
|
||||
}
|
||||
}
|
||||
const demand = tokRaw * BAL.MBPS_PER_TOK * mods.mbpsPerTokMult;
|
||||
let tokViaSwitches = 0;
|
||||
for (const [swUid, tokSum] of perSwitch) {
|
||||
const cap = DEF[byId.get(swUid)!.defId].switchMbps!;
|
||||
tokViaSwitches += tokSum * Math.min(1, cap / Math.max(0.001, tokSum * mbpsPerTok));
|
||||
}
|
||||
const tokReaching = tokDirect + tokViaSwitches;
|
||||
const demand = tokReaching * mbpsPerTok;
|
||||
const dataMult = demand > 0 ? Math.min(1, uplink / demand) : 1;
|
||||
const tokEff = tokRaw * dataMult;
|
||||
|
||||
// --- VRAM: your cluster has to actually hold the weights ---
|
||||
let vramHave = 0;
|
||||
for (const d of s.devices) if (d.powered && isCompute(DEF[d.defId])) vramHave += cardStats(d).vram;
|
||||
const vramNeed = BAL.TIERS[s.tier].vramGB;
|
||||
const vramMult = vramNeed > 0 && vramHave < vramNeed
|
||||
? Math.max(BAL.VRAM_STARVE_FLOOR, vramHave / vramNeed)
|
||||
: 1;
|
||||
|
||||
const tokEff = tokReaching * dataMult * vramMult;
|
||||
|
||||
// --- economy: compute splits three ways (slop / training / hosting) ---
|
||||
const tm = tierMult(s);
|
||||
@ -465,15 +540,25 @@ export function step(s: SimState) {
|
||||
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
|
||||
// training fills the research bank; the Lab decides what it buys.
|
||||
// checkpoints for the version you're training toward need disk to live on.
|
||||
let storageGB = 0;
|
||||
for (const d of s.devices)
|
||||
if (d.powered && DEF[d.defId].storageGB) storageGB += DEF[d.defId].storageGB!;
|
||||
const ckptNeed = BAL.TIERS[s.tier + 1]?.checkpointGB ?? 0;
|
||||
const trainStalled = tokTrain > 0 && ckptNeed > 0 && storageGB < ckptNeed;
|
||||
if (trainStalled && !s.rates.trainStalled)
|
||||
pushEvent(s, `💾 TRAINING STALLED — ${ckptNeed}GB of checkpoint space needed, ${storageGB}GB online`, "bad");
|
||||
const src = DATA_SOURCES[s.dataSource];
|
||||
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
|
||||
if (!trainStalled) {
|
||||
s.trainTokens += tokTrain * dataRate;
|
||||
income -= tokTrain * src.costPerTok; // licensed data bills per token
|
||||
}
|
||||
|
||||
// platform adaptation drifts back down while you're not spamming
|
||||
s.platformAdapt = Math.max(0, s.platformAdapt - BAL.ADAPT_DECAY_S * mods.adaptDecayMult * dt);
|
||||
@ -665,6 +750,10 @@ export function step(s: SimState) {
|
||||
r.adapt = s.platformAdapt;
|
||||
r.hostIncome += (hostIncome - r.hostIncome) * sm;
|
||||
r.hostTok = tokHost;
|
||||
r.vramHave = vramHave;
|
||||
r.vramNeed = vramNeed;
|
||||
r.storageGB = storageGB;
|
||||
r.trainStalled = trainStalled;
|
||||
}
|
||||
|
||||
export function shopList(s: SimState) {
|
||||
|
||||
@ -12,6 +12,7 @@ export interface Device {
|
||||
perf: number; // thermal multiplier 0..1
|
||||
util: number; // current utilization 0..1
|
||||
upgrades: string[]; // socket upgrade ids
|
||||
gpus: string[]; // installed GPU card ids (carriers only)
|
||||
}
|
||||
|
||||
export interface Video {
|
||||
@ -86,6 +87,10 @@ export interface SimState {
|
||||
adapt: number;
|
||||
hostIncome: number; // $/s from API hosting
|
||||
hostTok: number; // tok/s allocated to hosting
|
||||
vramHave: number;
|
||||
vramNeed: number;
|
||||
storageGB: number;
|
||||
trainStalled: boolean;
|
||||
};
|
||||
events: { msg: string; t: number; kind: string }[];
|
||||
rng: number;
|
||||
@ -140,6 +145,7 @@ export function createState(): SimState {
|
||||
watts: 0, breakerWatts: BAL.BREAKER_WATTS, tokEff: 0, income: 0,
|
||||
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,
|
||||
},
|
||||
events: [],
|
||||
rng: 0x5109a & 0xffffffff,
|
||||
|
||||
@ -47,6 +47,7 @@
|
||||
padding: 10px; overflow-y: auto; display: flex; flex-direction: column; gap: 8px;
|
||||
}
|
||||
#shop h3 { margin: 0 0 2px; font-size: 11px; color: var(--dim); letter-spacing: 0.15em; }
|
||||
.shop-cat { font-size: 9px; color: var(--dim); letter-spacing: 0.2em; margin: 4px 0 -2px; }
|
||||
.shop-item {
|
||||
display: flex; gap: 8px; align-items: center; border: 1px solid var(--edge); border-radius: 8px;
|
||||
padding: 6px; cursor: pointer; background: #12121f;
|
||||
@ -118,7 +119,10 @@
|
||||
.node.buy:hover { border-color: var(--cyan); }
|
||||
.node.owned { border-color: var(--green); background: #0f2018; }
|
||||
.node.dim { opacity: 0.45; }
|
||||
#devpanel { position: absolute; right: 215px; bottom: 12px; width: 230px; padding: 10px; z-index: 40; }
|
||||
#devpanel { position: absolute; right: 215px; bottom: 12px; width: 250px; max-height: 70vh; overflow-y: auto; padding: 10px; z-index: 40; }
|
||||
.gpu-row { display: flex; align-items: center; gap: 7px; }
|
||||
.gpu-row img { width: 30px; height: 30px; object-fit: contain; flex: none; }
|
||||
.gpu-row span { flex: 1; min-width: 0; }
|
||||
#devpanel .devstats { color: var(--dim); font-size: 10.5px; margin-bottom: 6px; }
|
||||
#devpanel .devbtns { display: flex; gap: 6px; margin-bottom: 4px; }
|
||||
#devpanel h4 { margin: 8px 0 4px; font-size: 10px; color: var(--dim); letter-spacing: 0.15em; }
|
||||
|
||||
@ -54,6 +54,7 @@ export class Hud {
|
||||
<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">VRAM</span><span class="val" id="vram">0GB</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>
|
||||
@ -131,8 +132,16 @@ export class Hud {
|
||||
if (cacheKey === this.lockedCache) return;
|
||||
this.lockedCache = cacheKey;
|
||||
const shop = this.root.querySelector("#shop") as HTMLElement;
|
||||
shop.querySelectorAll(".shop-item").forEach((e) => e.remove());
|
||||
shop.querySelectorAll(".shop-item, .shop-cat").forEach((e) => e.remove());
|
||||
let lastCat = "";
|
||||
for (const it of items) {
|
||||
if (it.def.cat !== lastCat) {
|
||||
lastCat = it.def.cat;
|
||||
const h = document.createElement("div");
|
||||
h.className = "shop-cat";
|
||||
h.textContent = lastCat.toUpperCase();
|
||||
shop.appendChild(h);
|
||||
}
|
||||
const el = document.createElement("div");
|
||||
el.className = "shop-item" + (it.locked || !it.affordable ? " dim" : "") +
|
||||
(this.selectedShop === it.def.id ? " selected" : "");
|
||||
@ -262,6 +271,11 @@ export class Hud {
|
||||
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)" : "";
|
||||
q("#vram").textContent = s.rates.vramNeed > 0
|
||||
? `${Math.round(s.rates.vramHave)}/${s.rates.vramNeed}GB`
|
||||
: `${Math.round(s.rates.vramHave)}GB`;
|
||||
(q("#vram") as HTMLElement).style.color =
|
||||
s.rates.vramNeed > 0 && s.rates.vramHave < s.rates.vramNeed ? "var(--red)" : "";
|
||||
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";
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
// Model Lab modal, Corporate R&D modal, and the device inspector panel.
|
||||
import { BAL } from "../sim/balance";
|
||||
import { SimState } from "../sim/state";
|
||||
import { DEF } from "../sim/catalog";
|
||||
import { DEF, GPUS, GPU_DEF, isCompute } 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,
|
||||
shipGrade, switchHosting, unlockScrape, buyGpu, sellGpu, cardStats, deviceTokRate,
|
||||
} from "../sim/sim";
|
||||
import { pushEvent } from "../sim/state";
|
||||
import { sfx } from "./sfx";
|
||||
@ -59,6 +59,8 @@ export class Panels {
|
||||
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 "gpu": err = buyGpu(s, this.selectedDevice!, arg!); break;
|
||||
case "sellgpu": err = sellGpu(s, this.selectedDevice!, Number(arg)); break;
|
||||
case "toggle": err = toggleDevice(s, this.selectedDevice!); break;
|
||||
case "sell-dev":
|
||||
if (this.selectedDevice !== null) { sell(s, this.selectedDevice); sfx.sell(); this.selectedDevice = null; }
|
||||
@ -197,15 +199,30 @@ export class Panels {
|
||||
|
||||
private deviceHtml(s: SimState, d: (typeof s.devices)[number]): string {
|
||||
const def = DEF[d.defId];
|
||||
const isCompute = def.tokRate > 0;
|
||||
const compute = isCompute(def);
|
||||
const cards = cardStats(d);
|
||||
return `<div class="panel" id="devpanel">
|
||||
<div class="mhead"><b>${def.name}</b><button class="mclose" data-cmd="close">✕</button></div>
|
||||
<div class="devstats">${Math.round(d.temp)}°C · ${Math.round(d.util * 100)}% util · perf ${(d.perf * 100).toFixed(0)}%${isCompute ? ` · ${def.tokRate} tok/s base` : ""}</div>
|
||||
<div class="devstats">${Math.round(d.temp)}°C · ${Math.round(d.util * 100)}% util · perf ${(d.perf * 100).toFixed(0)}%${compute ? ` · ${deviceTokRate(d)} tok/s · ${cards.vram}GB VRAM` : ""}</div>
|
||||
<div class="devbtns">
|
||||
<button class="chip" data-cmd="toggle">${d.on ? "⏻ Turn OFF" : "⏻ Turn ON"}</button>
|
||||
<button class="chip" data-cmd="sell-dev">💸 Sell (+$${Math.round(def.cost * 0.5)})</button>
|
||||
</div>
|
||||
${isCompute ? `<h4>SOCKETS (${d.upgrades.length}/${SOCKET_SLOTS})</h4>
|
||||
${def.gpuSlots ? `<h4>GPU SLOTS (${d.gpus.length}/${def.gpuSlots})</h4>
|
||||
${d.gpus.map((g, i) => {
|
||||
const gd = GPU_DEF[g];
|
||||
return `<div class="node owned gpu-row"><img src="/assets/gen/${gd.sprite}_cut.png"/>
|
||||
<span>${gd.name}<small>${gd.tok} tok/s · ${gd.watts}W · ${gd.vram}GB</small></span>
|
||||
<button class="mclose" data-cmd="sellgpu" data-arg="${i}" title="sell +$${Math.round(gd.cost * 0.5)}">💸</button></div>`;
|
||||
}).join("")}
|
||||
${d.gpus.length < def.gpuSlots
|
||||
? `<h4>INSTALL CARD</h4>` + GPUS
|
||||
.filter((g) => (g.unlockAt ?? 0) <= s.earnedTotal && (!g.rackOnly || def.rackClass))
|
||||
.map((g) => `<button class="node buy gpu-row ${s.cash >= g.cost ? "" : "dim"}" data-cmd="gpu" data-arg="${g.id}">
|
||||
<img src="/assets/gen/${g.sprite}_cut.png"/>
|
||||
<span>${g.name}<small>${g.tok} tok/s · ${g.watts}W · ${g.vram}GB · heat ${g.heat} · $${fmt(g.cost)}</small></span></button>`).join("")
|
||||
: ""}` : ""}
|
||||
${compute ? `<h4>SOCKETS (${d.upgrades.length}/${SOCKET_SLOTS})</h4>
|
||||
${d.upgrades.map((u) => `<div class="node owned">${SOCKETS.find((x) => x.id === u)?.name}</div>`).join("")}
|
||||
${d.upgrades.length < SOCKET_SLOTS
|
||||
? SOCKETS.filter((u) => !d.upgrades.includes(u.id)).map((u) =>
|
||||
|
||||
@ -7,7 +7,15 @@ 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"),
|
||||
("dumb_switch", "a small cheap plastic 5-port ethernet switch with green blinking LEDs and a few patch cables"),
|
||||
("managed_switch", "a professional 1U rackmount managed network switch with many ports and orange status LEDs"),
|
||||
("ssd_shoebox", "an old shoebox overflowing with loose SATA SSDs and tangled cables"),
|
||||
("nvme_hoard", "a small stack of NVMe M.2 drives in an open anti-static tray next to a drive dock"),
|
||||
("nas_tower", "a black tower NAS storage server with six visible drive bays, one bay open"),
|
||||
("gpu_budget", "a single old dusty budget graphics card with one small fan, product photo style"),
|
||||
("gpu_gaming", "a chunky three-fan RGB gaming graphics card, product photo style"),
|
||||
("gpu_workstation", "a sleek blower-style professional workstation graphics card, product photo style"),
|
||||
("gpu_datacenter", "a gold-and-black datacenter AI accelerator card with no fans and huge heatsink, product photo style"),
|
||||
]
|
||||
|
||||
gen = ROOT / "public/assets/gen"
|
||||
|
||||
@ -163,5 +163,95 @@
|
||||
"job": "b8b0755ee459",
|
||||
"asset": "4dca1b9e28b0",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/cash_furnace_cut.png"
|
||||
},
|
||||
"dumb_switch": {
|
||||
"job": "de748d933b5c",
|
||||
"asset": "1a0094eb7f96",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/dumb_switch.png"
|
||||
},
|
||||
"managed_switch": {
|
||||
"job": "6d85ce892caa",
|
||||
"asset": "b437cbfd5af3",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/managed_switch.png"
|
||||
},
|
||||
"ssd_shoebox": {
|
||||
"job": "7a5498cc727c",
|
||||
"asset": "02dda990f16a",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/ssd_shoebox.png"
|
||||
},
|
||||
"nvme_hoard": {
|
||||
"job": "c8a558397e22",
|
||||
"asset": "2517650e8e27",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/nvme_hoard.png"
|
||||
},
|
||||
"nas_tower": {
|
||||
"job": "0b761ebdfd51",
|
||||
"asset": "0ae309c969cf",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/nas_tower.png"
|
||||
},
|
||||
"gpu_budget": {
|
||||
"job": "5797ff60d08f",
|
||||
"asset": "ee8d177d9b2c",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/gpu_budget.png"
|
||||
},
|
||||
"gpu_gaming": {
|
||||
"job": "d172ef98c60e",
|
||||
"asset": "ef655a215943",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/gpu_gaming.png"
|
||||
},
|
||||
"gpu_workstation": {
|
||||
"job": "32b6727a33ce",
|
||||
"asset": "96a0766be6e4",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/gpu_workstation.png"
|
||||
},
|
||||
"gpu_datacenter": {
|
||||
"job": "389c880dde5d",
|
||||
"asset": "e00f0d50f1a0",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/gpu_datacenter.png"
|
||||
},
|
||||
"dumb_switch_cut": {
|
||||
"job": "9ec139f6f2c2",
|
||||
"asset": "91d7c42d8b54",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/dumb_switch_cut.png"
|
||||
},
|
||||
"ssd_shoebox_cut": {
|
||||
"job": "071bf7447817",
|
||||
"asset": "7d6ae66ee81a",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/ssd_shoebox_cut.png"
|
||||
},
|
||||
"managed_switch_cut": {
|
||||
"job": "8b57abde338a",
|
||||
"asset": "baa658fc5a9b",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/managed_switch_cut.png"
|
||||
},
|
||||
"nvme_hoard_cut": {
|
||||
"job": "c5648b749db8",
|
||||
"asset": "4adf7e4ea4a8",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/nvme_hoard_cut.png"
|
||||
},
|
||||
"nas_tower_cut": {
|
||||
"job": "236a7fe8e056",
|
||||
"asset": "1458a7a44746",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/nas_tower_cut.png"
|
||||
},
|
||||
"gpu_budget_cut": {
|
||||
"job": "be38471a9965",
|
||||
"asset": "0de616292080",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/gpu_budget_cut.png"
|
||||
},
|
||||
"gpu_gaming_cut": {
|
||||
"job": "3003117b1d74",
|
||||
"asset": "54d7f9674d34",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/gpu_gaming_cut.png"
|
||||
},
|
||||
"gpu_workstation_cut": {
|
||||
"job": "ee7e89728247",
|
||||
"asset": "fbd1d43a03af",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/gpu_workstation_cut.png"
|
||||
},
|
||||
"gpu_datacenter_cut": {
|
||||
"job": "33f517d4fdca",
|
||||
"asset": "1d45b47a2cf7",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/gpu_datacenter_cut.png"
|
||||
}
|
||||
}
|
||||