# 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: ```js 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** — `lvl` being 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 print `cell.lvl+1` (see `railFormula`), but every cost and growth call passes the raw `lvl`. If you add a formula, keep `g(0)` and `fx(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: ```js 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`, where `left`/`up` are the neighbor values. A wall of generators to the left and above makes an adder explode. - **`sum`** — walks rows `0..r-1` of its own column, sums owned values, banks `colSum * g * flow`. - **`rand`** — `v += g * Math.random() * flow`. - **`pivot`** — double-loops every owned cell in columns `0..c-1`, sums them, banks `leftSum * 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: ```js for(let c=0;c0 ? 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 → 0` and 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 `` 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: ```js for(let i=0;i **DEV NOTE** — the `fillJustEnded` flag and its `setTimeout(…,50)` reset exist because the document-level `click` handler also runs on `mouseup`, 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 `Sub`s 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: ```js let hasOut=false; for(let r=0;r0)) 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: ```js 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: ```js 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 `add` and `pivot` read 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 `gm` touch a compounding exponent.** The `prev` branch keeps `expo` global-multiplier-independent on purpose. Fold any new multiplier in *linearly*, the way `gm` already is, or the first big `recalc` overflows past `CELL_MAX`. - **`lvl` is 0-indexed; the UI shows `lvl+1`.** New formulas must define `g(0)`/`fx(0)` as the first purchasable tier, and every cost call must pass raw `lvl`, 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 change `OUT_COL`, change the guard, the seed, `computeNetWorth`, and `vbaAutoPivot` together. - **`CELL_MAX` must stay below `Number.MAX_VALUE` with headroom for `SUM(H1:H20)`.** Raise it and an 8-hour offline `prev` run can reach `Infinity` and break `fmt`. 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-derive `fillCost` and re-test the `fillJustEnded` click-swallow.