diff --git a/CLAUDE.md b/CLAUDE.md index 7d68caf..77cc250 100644 --- a/CLAUDE.md +++ b/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. diff --git a/public/assets/gen/dumb_switch.png b/public/assets/gen/dumb_switch.png new file mode 100644 index 0000000..3512b48 Binary files /dev/null and b/public/assets/gen/dumb_switch.png differ diff --git a/public/assets/gen/dumb_switch_cut.png b/public/assets/gen/dumb_switch_cut.png new file mode 100644 index 0000000..710c224 Binary files /dev/null and b/public/assets/gen/dumb_switch_cut.png differ diff --git a/public/assets/gen/gpu_budget.png b/public/assets/gen/gpu_budget.png new file mode 100644 index 0000000..10ba7b1 Binary files /dev/null and b/public/assets/gen/gpu_budget.png differ diff --git a/public/assets/gen/gpu_budget_cut.png b/public/assets/gen/gpu_budget_cut.png new file mode 100644 index 0000000..cfcd052 Binary files /dev/null and b/public/assets/gen/gpu_budget_cut.png differ diff --git a/public/assets/gen/gpu_datacenter.png b/public/assets/gen/gpu_datacenter.png new file mode 100644 index 0000000..b723508 Binary files /dev/null and b/public/assets/gen/gpu_datacenter.png differ diff --git a/public/assets/gen/gpu_datacenter_cut.png b/public/assets/gen/gpu_datacenter_cut.png new file mode 100644 index 0000000..f3cec98 Binary files /dev/null and b/public/assets/gen/gpu_datacenter_cut.png differ diff --git a/public/assets/gen/gpu_gaming.png b/public/assets/gen/gpu_gaming.png new file mode 100644 index 0000000..e74d908 Binary files /dev/null and b/public/assets/gen/gpu_gaming.png differ diff --git a/public/assets/gen/gpu_gaming_cut.png b/public/assets/gen/gpu_gaming_cut.png new file mode 100644 index 0000000..d316584 Binary files /dev/null and b/public/assets/gen/gpu_gaming_cut.png differ diff --git a/public/assets/gen/gpu_workstation.png b/public/assets/gen/gpu_workstation.png new file mode 100644 index 0000000..90ac597 Binary files /dev/null and b/public/assets/gen/gpu_workstation.png differ diff --git a/public/assets/gen/gpu_workstation_cut.png b/public/assets/gen/gpu_workstation_cut.png new file mode 100644 index 0000000..a49a31f Binary files /dev/null and b/public/assets/gen/gpu_workstation_cut.png differ diff --git a/public/assets/gen/managed_switch.png b/public/assets/gen/managed_switch.png new file mode 100644 index 0000000..070155e Binary files /dev/null and b/public/assets/gen/managed_switch.png differ diff --git a/public/assets/gen/managed_switch_cut.png b/public/assets/gen/managed_switch_cut.png new file mode 100644 index 0000000..a9bf495 Binary files /dev/null and b/public/assets/gen/managed_switch_cut.png differ diff --git a/public/assets/gen/nas_tower.png b/public/assets/gen/nas_tower.png new file mode 100644 index 0000000..a1e60d9 Binary files /dev/null and b/public/assets/gen/nas_tower.png differ diff --git a/public/assets/gen/nas_tower_cut.png b/public/assets/gen/nas_tower_cut.png new file mode 100644 index 0000000..768db3e Binary files /dev/null and b/public/assets/gen/nas_tower_cut.png differ diff --git a/public/assets/gen/nvme_hoard.png b/public/assets/gen/nvme_hoard.png new file mode 100644 index 0000000..9fffee8 Binary files /dev/null and b/public/assets/gen/nvme_hoard.png differ diff --git a/public/assets/gen/nvme_hoard_cut.png b/public/assets/gen/nvme_hoard_cut.png new file mode 100644 index 0000000..c38c00d Binary files /dev/null and b/public/assets/gen/nvme_hoard_cut.png differ diff --git a/public/assets/gen/ssd_shoebox.png b/public/assets/gen/ssd_shoebox.png new file mode 100644 index 0000000..d09080d Binary files /dev/null and b/public/assets/gen/ssd_shoebox.png differ diff --git a/public/assets/gen/ssd_shoebox_cut.png b/public/assets/gen/ssd_shoebox_cut.png new file mode 100644 index 0000000..8711781 Binary files /dev/null and b/public/assets/gen/ssd_shoebox_cut.png differ diff --git a/src/main.ts b/src/main.ts index 93a5f57..98b1ee8 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, 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), }; } diff --git a/src/sim/balance.ts b/src/sim/balance.ts index c3dedb0..f1b07ee 100644 --- a/src/sim/balance.ts +++ b/src/sim/balance.ts @@ -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 }; diff --git a/src/sim/catalog.ts b/src/sim/catalog.ts index f798d68..69de43e 100644 --- a/src/sim/catalog.ts +++ b/src/sim/catalog.ts @@ -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])); diff --git a/src/sim/save.ts b/src/sim/save.ts index 3713666..8b14ee7 100644 --- a/src/sim/save.ts +++ b/src/sim/save.ts @@ -22,7 +22,16 @@ export function loadState(): SimState | null { delete (s as unknown as Record).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); diff --git a/src/sim/sim.ts b/src/sim/sim.ts index 9f0fafd..b8ddd38 100644 --- a/src/sim/sim.ts +++ b/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(); // 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) { diff --git a/src/sim/state.ts b/src/sim/state.ts index d7c3824..bc9ca8c 100644 --- a/src/sim/state.ts +++ b/src/sim/state.ts @@ -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, diff --git a/src/ui/hud.css b/src/ui/hud.css index 69b8afd..ad33f81 100644 --- a/src/ui/hud.css +++ b/src/ui/hud.css @@ -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; } diff --git a/src/ui/hud.ts b/src/ui/hud.ts index 5aaeba5..477a183 100644 --- a/src/ui/hud.ts +++ b/src/ui/hud.ts @@ -54,6 +54,7 @@ export class Hud {
VIEWS0
UPLINK0/0 Mbps
HOSTING$0/s
+
VRAM0GB
MODELSlopLM-1B
ALGO HEAT0%
@@ -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"; diff --git a/src/ui/panels.ts b/src/ui/panels.ts index 09351ec..398ac21 100644 --- a/src/ui/panels.ts +++ b/src/ui/panels.ts @@ -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 `
${def.name}
-
${Math.round(d.temp)}°C · ${Math.round(d.util * 100)}% util · perf ${(d.perf * 100).toFixed(0)}%${isCompute ? ` · ${def.tokRate} tok/s base` : ""}
+
${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` : ""}
- ${isCompute ? `

SOCKETS (${d.upgrades.length}/${SOCKET_SLOTS})

+ ${def.gpuSlots ? `

GPU SLOTS (${d.gpus.length}/${def.gpuSlots})

+ ${d.gpus.map((g, i) => { + const gd = GPU_DEF[g]; + return `
+ ${gd.name}${gd.tok} tok/s · ${gd.watts}W · ${gd.vram}GB +
`; + }).join("")} + ${d.gpus.length < def.gpuSlots + ? `

INSTALL CARD

` + GPUS + .filter((g) => (g.unlockAt ?? 0) <= s.earnedTotal && (!g.rackOnly || def.rackClass)) + .map((g) => ``).join("") + : ""}` : ""} + ${compute ? `

SOCKETS (${d.upgrades.length}/${SOCKET_SLOTS})

${d.upgrades.map((u) => `
${SOCKETS.find((x) => x.id === u)?.name}
`).join("")} ${d.upgrades.length < SOCKET_SLOTS ? SOCKETS.filter((u) => !d.upgrades.includes(u.id)).map((u) => diff --git a/tools/mb_add.py b/tools/mb_add.py index efbe1f1..453d9e9 100644 --- a/tools/mb_add.py +++ b/tools/mb_add.py @@ -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" diff --git a/tools/mb_manifest.json b/tools/mb_manifest.json index 433fc14..478308d 100644 --- a/tools/mb_manifest.json +++ b/tools/mb_manifest.json @@ -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" } } \ No newline at end of file