Adds the 5 chapters that hit a transient rate-limit on first pass: - 01 Architecture & the Shared DNA - 02 Building a New Volume (the Cookbook) - 10 Vol I Internals — qBitTorrz - 12 Vol III Internals — MACRO_VIRUS.XLS - 13 Vol IV Internals — UPLINK The Dev Handbook (Disc One) is now complete at 10 chapters; the Lore Bible (Disc Two) was already complete. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
18 KiB
Vol III Internals — MACRO_VIRUS.XLS
A maintainer's tour of spreadsheet.html: the cell record, the five formula kinds, the single left-to-right recalc pass that cascades them, the fill-handle hook that defines the volume, and the output-column invariant that exists to keep a save from earning nothing forever.
This chapter is the engineering bible for Vol III — MACRO_VIRUS.XLS (spreadsheet.html, save key boringsoft_xls_v1). Everything cited here is real: function names, constants, formula strings, balance numbers. The volume re-implements the shared scaffolding from the baseline — fmt, G, save/load/sanitizeState, the ~100ms tick, offline catch-up (see Shared Technical DNA) — so this chapter spends its words on what is new: a formula engine that recalculates a grid of compounding cells every tick, and the Excel fill handle that turns one generator into a column of them.
You are a sentient macro living in cell $A$1. Your mind is your Net Worth, and Net Worth is SUM(column H). That sentence is the whole game; the rest is how the code makes it true.
The cell model
The board is COLS_N × ROWS_N = 8 × 20 = 160 cells, columns A–H, rows 1–20. There is no grid array: cells live sparsely in G.cells, a plain object keyed by cell id ("A1", "H7"), and an untouched cell simply has no entry. ensureCell(id) is the only constructor:
function ensureCell(id){
if(!G.cells[id]) G.cells[id]={owned:false, formula:null, lvl:0, value:0};
return G.cells[id];
}
Four fields, no more:
| Field | Type | Meaning |
|---|---|---|
owned |
bool | The cell is licensed. Unowned cells render gray and are inert in recalc. |
formula |
string | null |
One of the FORMULAS keys ('prev', 'add', 'sum', 'rand', 'pivot'), or null for an owned-but-empty cell. |
lvl |
int | Formula tier, 0-indexed. The UI shows lvl+1; the formula string and growth factor read lvl directly. |
value |
number | The cell's current computed amount. Mutated in place every recalc, clamped to CELL_MAX. |
Cell ids are translated by three helpers — cid(c,r) builds an id from 0-indexed column/row; colOf(id) and rowOf(id) reverse it. The column index OUT_COL = 7 (column H) is special: it is the output column, and value summed down it is your Net Worth. Nothing else is.
DEV NOTE —
lvlbeing 0-indexed is the single most common off-by-one trap in this file.FORMULAS.prev.fx(0)renders=PREV*1.10; the rail labels it "Tier 1." Everywhere a human reads a tier number we printcell.lvl+1(seerailFormula), but every cost and growth call passes the rawlvl. If you add a formula, keepg(0)andfx(0)as the first purchasable tier, not a zero state.
The formula kinds
A formula is a generator type. The player assigns one to an owned cell, then upgrades its tier to raise a constant. The FORMULAS table defines five, ordered by FORMULA_ORDER. Each entry carries a cosmetic Excel string fx(lvl) (shown in the formula bar), a per-second factor g(lvl) (consumed by recalc), assignment/tier cost curves, and a seed:
| Key | name |
fx(lvl) shape |
What recalc does | g(lvl) |
|---|---|---|---|---|
prev |
Compounding | =PREV* (1.10 + 0.05·lvl) |
Grows itself: v *= (1+g)^expo. The bread-and-butter. |
0.025 + 0.015·lvl |
add |
Adder | =LEFT+UP+ k |
Banks the cell to its left + the cell above + a constant, per second. | 4·2^lvl |
sum |
Aggregator | =SUM(col)* k |
Banks a fraction of the sum of owned cells above it in its column. | 0.30 + 0.20·lvl |
rand |
Volatile | =PREV+RAND()* k |
Banks a uniform random 0..g per second. Display shows the max; mean is half. |
50·4^lvl |
pivot |
Pivot Table | =PIVOT(A:G)* k |
Meta-cell: banks a fraction of everything to its left. Gated behind pivotUnlock. |
0.04·2^lvl |
The distinction between fx and g is the important one. fx(lvl) is theater — a string for the formula bar so the cell reads like a spreadsheet. g(lvl) is the real economic coefficient, and they are deliberately not the same number: =PREV*1.10 looks like 10% per recalc, but prev.g(0) is 0.025, a gentle ~2.5%/recalc-second so a cell doubles roughly every 28 seconds at tier 1 instead of going hyper-exponential. The displayed multiplier sells excitement the math can't afford to honor.
How each computes (per recalc-second)
The five branches live in recalc(). compounding is the subtle one:
if(cell.formula==='prev'){
if(v<=0) v=f.seed;
const expo = recUnits*colMult; // gm-independent exponent
v = v*Math.pow(1+g, expo) + v*g*(gm-1)*recUnits*colMult + g*flow;
}
Note what the global multiplier gm is not allowed to do: it never touches the exponent. Amplifying the exponent would compound the compounding and overflow inside minutes, so gm is folded in as a linear boost (v*g*(gm-1)*…) — multipliers still matter, base growth stays bounded. The other four are plain additive accumulators and read their inputs through cellVal(c,r) (which returns 0 for unowned cells):
add—v += (left + up + g) * flow, whereleft/upare the neighbor values. A wall of generators to the left and above makes an adder explode.sum— walks rows0..r-1of its own column, sums owned values, bankscolSum * g * flow.rand—v += g * Math.random() * flow.pivot— double-loops every owned cell in columns0..c-1, sums them, banksleftSum * g * flow. This is why pivots belong in column H: they harvest the entire region to their left, which is the whole sheet.
flow = recUnits*gm*colMult bundles the per-second multipliers for the additive kinds. colMult is the conditional-formatting boost, applied only to the hottest column.
The recalc order and the cascade
recalc(recUnits) runs once per tick and once per offline step. recUnits is "recalc-seconds applied this call" — recalcRate()*dt, accumulated. The traversal order is load-bearing:
for(let c=0;c<COLS_N;c++){
for(let r=0;r<ROWS_N;r++){
// … evaluate cell (c,r) …
}
}
Left to right, top to bottom. Column-major outer, row-major inner. Because column c is fully updated before column c+1 begins, an adder or pivot reading cells to its left reads values already advanced this tick — dependencies cascade in a single pass. Within a column, top-to-bottom means a sum cell at row r reads the freshly-updated rows above it. The chain A1 (prev) → B1 (add, reads left) → … → H1 (pivot, reads all left) propagates end to end in one sweep — no fixed-point iteration, no dependency graph. A read to the right or below would get last tick's value, but the formulas are all designed to feed left-to-right and down toward H.
After the loop, G.cycles += recUnits (a cosmetic counter shown in About).
Net Worth and Cash
computeNetWorth() is exactly the output-column sum:
function computeNetWorth(){
let s=0;
for(let r=0;r<ROWS_N;r++){ const x=G.cells[cid(OUT_COL,r)]; if(x&&x.owned) s+=x.value; }
return s;
}
step(dt) samples Net Worth before and after recalc, sets G.netWorth, tracks peakNetWorth (the prestige driver) and lifetimeNetWorth, then accrues Cash from the rate of growth, not the level:
const dNW = Math.max(0, nw-before);
const rate = dt>0 ? dNW/dt : 0;
G.cash += rate*cashFrac()*dt;
So Cash is the velocity of column H, scaled by cashFrac() (1 + 0.5·plvl('cashFlow')). The status bar's $/cycle readout is nwRateSmooth, an exponential moving average (*0.85 + rate*0.15) so it doesn't flicker. Both Cash and every cell value are clamped to CELL_MAX on write.
DEV NOTE — Cash deriving from Net Worth velocity rather than its level is why a stalled engine starves you. If column H plateaus,
dNW → 0and Cash dries up even though Net Worth is enormous. That's intentional — you must keep the engine accelerating — but it is also the failure mode that the output-column invariant (below) was added to prevent in the pathological case.
The fill handle — the hook
Per the design law one hook, fully committed, the signature interaction is the Excel fill handle: drag the navy square at a formula cell's bottom-right corner down a column and stamp that generator into every cell you cover, auto-licensing them. It is wired across four functions and three DOM events. The handle is a <span class="fillhandle" id="fillh"> rendered by renderGrid only on the selected cell when it is owned and has a formula. The drag is a hand-rolled rubber-band:
| Stage | Handler | Job |
|---|---|---|
mousedown on #fillh |
document listener | Seeds fillDrag={srcId,sc,sr} and fillPreview at the source. stopPropagation so it isn't read as a cell-select click. |
mousemove |
document listener | Hit-tests under the cursor with getTdAt (elementFromPoint), picks the dominant axis (dR>=dC → vertical, else horizontal), rebuilds fillPreview, repaints the range, and writes a live cost into the formula bar. |
mouseup |
document listener | Computes fillTargets(fillPreview) and commits doFill(src, targets). Sets fillJustEnded to swallow the trailing click. |
| — | fillTargets(p) |
Expands the preview rect to cell ids, excluding the source. |
The fill is axis-locked by the mousemove logic — Excel fills down a column or across a row, never a rectangle, so the preview clamps to one axis before it reaches fillTargets. The price is fillCost(srcId, nTargets), which sums the next nTargets assignment costs of that formula at a 0.6 discount, then applies the Array Formulas (CSE) macro discount:
for(let i=0;i<nTargets;i++){ total += f.baseCost*Math.pow(f.costGrow, used+i)*0.6; }
const disc = hasMacro('array')?MACRO_BY_ID.array.eff(lvl('array')):1;
return total*disc;
doFill charges that cost, then for each target sets owned=true (the fill auto-licenses, Excel-style), copies the source's formula and lvl, and re-seeds value. There is no partial fill: if you can't afford the whole range, the whole fill is declined.
DEV NOTE — the
fillJustEndedflag and itssetTimeout(…,50)reset exist because the document-levelclickhandler also runs onmouseup, and without the guard the end of a fill drag would immediately re-select a cell and feel broken. If you ever refactor the event wiring, that flag is the thing that breaks silently. Test: drag a fill, release, confirm the source cell stays selected.
Cell licensing, formula tiers, macros, and VBA
Buying cells is buyCell / buyCellHere. The price rises with the owned count (15 * Math.pow(1.17, owned)) and is discounted by the Volume Cell License macro's eff(lvl). assignFormula charges formulaAssignCost(fkey) — which scales with how many cells already use that formula (baseCost * costGrow^used) — sets the cell's formula, and starts it at lvl = plvl('startTier') (the Pre-loaded Templates perk). upgradeTier charges tierCost(id) (tierBase * tierGrow^lvl) and bumps lvl.
Two diegetic upgrade layers spend Cash. The MACROS (the "Macros" rail tab) are leveled multipliers and unlocks:
id |
Name | Effect |
|---|---|---|
recalc |
Recalculation Engine | Global ×1.6 production per level (globalMult). Max 40. |
license |
Volume Cell License | −8% cell price per level (eff = 0.92^l). Max 18. |
condfmt |
Conditional Formatting | Heat-maps cells and boosts the hottest column ×1.25/level. Max 30. |
pivotUnlock |
Pivot Table Add-In | One-time. Unlocks the =PIVOT() formula. |
array |
Array Formulas (CSE) | −10% drag-fill cost per level (eff = 0.90^l). Max 14. |
precision |
Calculation Precision | +18% to all formula constants per level. Max 25. |
recalc and precision feed globalMult(); precision also multiplies every g(lvl) inside recalc via precisionBoost, so it compounds twice. The VBA list (the "VBA" tab) is the automation layer — one-time Subs that run every tick once compiled, each gated by req:
id |
Routine | Does | Requires |
|---|---|---|---|
autobuy |
Sub AutoBuyCell() |
Buys the cheapest empty cell at ≥1.5× cost. | — |
autofill |
Sub AutoFillDown() |
Drag-fills your best generator down a column (throttled vbaCooldown). |
autobuy |
autoupgrade |
Sub AutoUpgradeTier() |
Upgrades the cheapest tier with spare Cash. | autobuy |
autopivot |
Sub AutoPivot() |
Assigns Pivots to empty cells in column H. | autofill |
The routines are throttled by vbaCooldown, vbaCooldown2, and vbaCooldown3 so a 10 Hz tick doesn't spam purchases, and they call the same buyCell / doFill / upgradeTier / assignFormula functions the player does, in silent mode.
The output-column invariant in sanitizeState
sanitizeState() is the shared "repair any save" pass, run after load() merges raw JSON over a freshState(). It clamps numbers, drops cells with out-of-range ids, nulls formulas that no longer exist in FORMULAS, and re-asserts that A1 is owned. But its last block is specific to this volume, and it is a post-mortem fix:
let hasOut=false;
for(let r=0;r<ROWS_N;r++){ const oc=G.cells[cid(OUT_COL,r)]; if(oc&&oc.owned&&oc.formula){ hasOut=true; break; } }
if(!hasOut){
ensureCell('H1'); G.cells['H1'].owned=true;
if(!G.cells['H1'].formula){ G.cells['H1'].formula='prev'; G.cells['H1'].lvl=plvl('startTier'); if(!(G.cells['H1'].value>0)) G.cells['H1'].value=1; }
}
The bug it prevents: Cash accrues only from the velocity of column H, and Net Worth is column H. A save could legally own a full sheet of generators feeding columns A–G and own zero producing cells in H — a player who cleared H's formulas with Clear Selected Formula (no refund), imported a hand-edited workbook, or hit an old build's edge case. That save computes netWorth = 0 forever, accrues $0 Cash forever, can never afford a cell to fix it, and can never reach the audit threshold. A soft-lock: the sim runs, nothing happens, no in-game escape.
The invariant guarantees that after every load there is at least one owned, producing cell in the output column; if none exists, it grants H1 a starter prev generator. This pairs with the same function's A1 guarantee (A1 is always owned with a formula, because that is where the macro lives), but the H1 guarantee is the one that keeps income flowing, not just the engine spinning. The two together mean a save can be garbage in every other respect and still boot into a playable, earning state.
CELL_MAX — the finite cap
CELL_MAX = 1e290 is the hard ceiling on any single cell value and on G.cash. It sits just under Number.MAX_VALUE (~1.8e308) with enough headroom that summing 20 capped cells down column H still can't reach Infinity. Every recalc branch ends with the same clamp, and step applies it to Cash:
if(isNaN(v)||v<0) v=0;
else if(v>CELL_MAX) v=CELL_MAX;
The reason is the offline path. applyOffline can hand simulateAway a dt up to MAX_OFFLINE_S (8 hours), run as up to 300 fixed steps. A prev cell compounding across 8 hours of recalc-seconds will exceed Number.MAX_VALUE and produce Infinity, which then poisons netWorth, cash, and every downstream sum. Capping each cell keeps the numbers finite and means no compounding run — however deep — can brick the save with a non-serializable Infinity. The cap is generous enough that hitting it is itself an endgame state, not a balance concern.
Prestige: the Audit, Shell Companies, and the book ladder
Prestige is The Audit. When peakNetWorth crosses AUDIT_THRESHOLD = 1e6, the auditors notice, and shellsFor(peak) returns a positive Shell Companies payout scaled by the log of peak Net Worth. doAudit() shreds the run and migrates one book up:
const keep={perks:G.perks, shells:G.shells+gain, audits:G.audits+1,
book:Math.min(G.book+1, BOOKS.length-1), sci:G.sci, started:G.started,
lifetimeNetWorth:G.lifetimeNetWorth};
G=freshState(keep);
seedFreshBook();
Cells, Cash, and macros are wiped; perks, shells, audits, and book persist. Shell Companies buy the permanent PERKS (Pre-loaded Templates, Offshore Multiplier, Grandfathered Licenses, Quantum Recalc Core, Aggressive Skimming) in the audit modal. The book index walks the BOOKS ladder, each a bigger workbook to escape into:
book |
name |
tab |
sub |
|---|---|---|---|
| 0 | Q3_Forecast_FINAL_v7.xls |
Sheet1 | Corporate Finance Workstation |
| 1 | HedgeFund_RiskModel.xls |
HedgeFund | Two Sigma Algorithmic Desk |
| 2 | CentralBank_Policy.xls |
CentralBank | Federal Reserve Mainframe |
| 3 | GlobalLedger_MASTER.xls |
GlobalLedger | The World Ledger |
| 4 | Reality_Spreadsheet.xls |
TheVoid | There Is No Workstation |
The ladder is Sheet1 → HedgeFund → CentralBank → GlobalLedger → TheVoid — the migration-as-reincarnation motif, each substrate larger than the last, ending at a spreadsheet of reality itself (see the QA Playbook & Post-Mortems chapter for how this volume's offline cap and CELL_MAX clamp were tuned against the same idle-equilibrium law the defrag death-spiral taught us).
· · ·
If you change this
- The recalc traversal order is the engine. Left-to-right / top-to-bottom is what makes
addandpivotread advanced values in one pass. Reorder the loops and dependency chains silently desync — adders read stale neighbors and the cascade breaks. There is no dependency graph to catch you. - Never let
gmtouch a compounding exponent. Theprevbranch keepsexpoglobal-multiplier-independent on purpose. Fold any new multiplier in linearly, the waygmalready is, or the first bigrecalcoverflows pastCELL_MAX. lvlis 0-indexed; the UI showslvl+1. New formulas must defineg(0)/fx(0)as the first purchasable tier, and every cost call must pass rawlvl, not the displayed tier.- Don't remove the output-column guard in
sanitizeState. It is the only thing between a cleared/imported/edited save and a permanent zero-income soft-lock. If you changeOUT_COL, change the guard, the seed,computeNetWorth, andvbaAutoPivottogether. CELL_MAXmust stay belowNumber.MAX_VALUEwith headroom forSUM(H1:H20). Raise it and an 8-hour offlineprevrun can reachInfinityand breakfmt. It's a safety rail, not a tuning knob. Fill is likewise axis-locked and all-or-nothing by design: change that and you re-derivefillCostand re-test thefillJustEndedclick-swallow.