An interactive browser-based simulation that models strategic decision-making under uncertainty across finance, marketing, operations, and HR.
Pick an industry and a historical (or hypothetical) crisis, get a briefing built from PESTEL, Porter's Five Forces, and SWOT, then run four rounds of cross-functional decisions while a live scorecard, a counterfactual engine, and a procedurally generated situation-report ticker track what your choices actually did — and what the modeled-best alternative would have done instead.
Live demo: https://nnd1106.github.io/strat-lab/
You don't predict the crisis. You decide what happens next.
6 industries (Oil & Gas, Commercial Banking, Airlines, Consumer Retail, Automotive Manufacturing, Technology & SaaS) × 6 crisis scenarios (2008 GFC, Strait of Hormuz closure, 1929-style Depression, a pandemic shock, the 1973 oil embargo, a trade war) × 4 decision rounds × 4 functions (Marketing, Operations, HR, Finance), each with 4 industry-specific choices (37 distinct lever definitions in total: 13 reflavored across every industry, plus 24 signature moves exclusive to one industry each).
STRATLAB is five layers, each reading only from the layer below it:
- Industry / scenario data (
data_gen.py→data.js) — the single source of truth. Every industry's baseline scorecard and shock sensitivities, every scenario's shock channels and 4-round intensity curve, every lever's effects, and the real-world-scaled KPI baselines used for translation. No logic lives here, only content. - Framework briefing layer (
buildFrameworkContent,renderBriefing) — turns the industry+scenario pairing into a specific PESTEL / Porter's Five Forces / SWOT read, computed from which shock channels that industry is actually most exposed to, not a generic template. - Decision engine (
computeRoundOutcome,simulateRound) — applies the scenario shock plus your four chosen levers' effects (and last round's laggednext_roundeffects) to the six tracked metrics, with diminishing returns on repeated lever choices. This function is pure with respect to global state — it's the one place round math happens. - Impact / translation engine (
computeRealMetrics, KPI dashboard, Industry Benchmark, Risk Exposure gauge) — turns the six abstract metrics into industry-realistic figures (revenue in $B, EBITDA margin, headcount, Brand NPS, etc.) purely for display. Nothing here feeds back into the decision engine. - Scoring / counterfactual engine (
scoreOption,replaySimulation, Strategist DNA, the Counterfactual Lab) — every round, silently scores every unchosen option through the same decision engine (layer 3), not a second model, weighted toward protecting metrics closest to their floor. The Counterfactual Lab takes this further: it actually re-runscomputeRoundOutcomeforward through the rest of the game with one substituted choice, rather than estimating the effect.
Each round, two things happen to the six tracked metrics (Revenue, Market Share, Cash, Morale, Brand, CSAT):
- The scenario shock. The scenario pushes on up to six channels (demand, cost, credit, sentiment, supply chain, commodity) at an intensity set by that round's point on a 4-round curve. Each channel is scaled by the industry's sensitivity to it, mapped to metrics with a fixed weighting, and divided down so no single round can wipe the scorecard.
- Your four choices. Each chosen option's
effectsapply directly. If it isn't round 1, last round's four choices also apply theirnext_roundeffects now — the lag that makes some strategies pay off, or hurt, a beat later than you made them. Reusing the same lever option across rounds triggers diminishing returns (2nd use = 50% effects, 3rd+ = 25%, locked in at the moment of choice and reused for its lagged effect too), so repetition can't stack unlimited value.
Every round, the engine also scores every option you didn't pick, under identical conditions, weighted so metrics closer to their floor count for more — this is what powers the Risk Exposure gauge, the Strategist DNA dimensions, and the interactive Counterfactual Lab in the debrief, without requiring a second simulation model.
The KPI dashboard, Industry Benchmark, and radar/trend charts all read from the same six abstract metrics but display them differently: KPIs scale a per-industry real-world baseline by the metric's ratio to its own baseline; charts normalize every metric to a shared fixed display range so a bank's $20 cash baseline and an airline's $6 one are visually comparable.
Everything past this point re-presents data the engine already computes — nothing here is a new statistic, and nothing feeds back into the simulation.
- "Why?" explanations. Every metric delta with a clean, honest trace to one abstract metric (in the war room's Live Scorecard and Key Metrics panel, and the debrief's Company Trajectory) gets a "Why? ⓘ" toggle. It un-sums the same shock-channel and lever-effect math the engine already ran to name which lever(s) and which scenario channel moved that metric most, e.g. "Cash rose primarily from Raise Debt (+7), partially offset by the credit-channel shock from this scenario (−2)." Derived/damped KPI fields (EBITDA margin, Debt/Equity, headcount, attrition) intentionally don't get a toggle — explaining a compound figure via one lever's raw effect would misstate units, so those are left unexplained rather than fabricated.
- Decision Attribution. Recomputes, for every one of the 16 decisions, how far its score sat above or below the average of its other candidates that round (distinct from the Counterfactual Lab's chosen-vs-best gap) — surfaced as the single biggest positive and negative across the run.
- Strategic Trade-off Matrix. A scatter of all 16 choices vs. all 16 modeled-best alternatives: X = risk (variance of an option's own effect deltas across the 6 metrics — a concentrated big swing scores higher-risk than the same total spread evenly), Y = the counterfactual engine's composite score. The one-line takeaway compares cluster centroids, not a fixed narrative.
- Strategist DNA. 6 dimensions (Financial Discipline, Risk Management, Growth Orientation, Operational Thinking, People Strategy, Adaptability), each a formula over already-tracked data — Adaptability, for instance, is literally the average diminishing-returns discount actually applied across all 16 picks, so it directly rewards not repeating a lever into the penalty.
- Debrief narrative order. Restructured into 8 sections (Executive Verdict → Strategic Scorecard → Strategist DNA → Company Trajectory → Decision Attribution → Counterfactual Lab → Strategic Blind Spot → Lessons for Next Time) so it reads top-to-bottom as a story instead of a stack of panels. The Blind Spot and Lessons text are built the same way as everything else — a small rule-based template keyed off the actual DNA scores and Decision Attribution results, not per-scenario copy, so it holds up across all 36 industry×scenario combinations.
"Download Executive Report" (debrief screen) generates a full, designed
report client-side via jsPDF (cdnjs) — a full-bleed dark cover page (hero
photo + gradient, a metadata block, a Board Confidence score with a
per-round sparkline and a Board Verdict tag), a one-page Bottom
Line summary, a Table of Contents, and 15 numbered content sections on a
light background: Executive Verdict, KPI Bridge, Credit Rating & Outlook,
Strategic Scorecard, Company Trajectory, Decision Attribution (including
the full 16-decision table against the modeled-best alternative),
Strategic Patterns, Risk & Stakeholders, Your Path vs. Optimal Path (a
featured counterfactual re-simulated via replaySimulation() and
presented as an explicit causal chain — decision → immediate effect →
next-round effect → cumulative impact → alternative → what it would have
protected), Round-by-Round Summary, Strategic Blind Spot, Lessons for Next
Time, Final Key Metrics, the full situation log + alternative callouts,
and a Methodology & Glossary appendix. Every chart is embedded as a boxed,
captioned image (chart.toBase64Image()); every number — including every
v7 addition below — is recomputed from the same tracked state the
on-screen debrief uses, not invented for the PDF.
Board Confidence starts at 100, loses 10 points per round that scored CAUTION and 20 per round that scored DISTRESS (the same per-round Strategic Health tier the war-room badge already computes), floored at 0 each round so the cover sparkline never dips below zero. The qualitative read and the Board Verdict tag (CONTINUE / CONTINUE WITH CONDITIONS / REASSESS STRATEGY / URGENT INTERVENTION at fixed 80/60/40 score cutoffs) are both picked from that same score, not written per scenario.
Credit Rating bands final Cash Runway, Debt/Equity, and EBITDA Margin
against fixed thresholds (9 months, 1.0x, 15%): all three passing is
"A — Investment Grade," two is "BBB — Stable," one is "BB — Speculative,"
none is "B — Distressed." Outlook compares the Round 3 and Round 4 values
of the same two KPIs. KPI Bridge and Risk Register reuse the same
"named, fixed threshold" approach — see the report's own Methodology
appendix for every exact cutoff, and CREDIT_RATING_THRESHOLDS,
KPI_BRIDGE_ASSESSMENT_BAND, RISK_SEVERITY_MEDIUM/_HIGH in app.js
for the code. Note that jsPDF's standard fonts can't render emoji or most
Unicode arrows, so status indicators that read as 🟢/🟡/🔴 or ↑/↓/→ in the
brief are implemented as color-coded words instead (e.g. "Ahead" /
"Within Range" / "Behind"; "Worsening" / "Flat" / "Improving").
jsPDF ships only Helvetica/Times/Courier — there's no IBM Plex in a PDF without embedding font files as a new asset dependency, which the stack intentionally avoids. Helvetica and Courier stand in for Plex Sans and Plex Mono, keeping the "no new dependencies beyond what's already loaded" constraint intact.
One real bug worth noting: several sentence-builder functions call
escapeHtml() on lever labels like "HR & People" so their output is safe
to inject via innerHTML on screen (where the browser decodes &
back to & automatically). jsPDF has no such decoding step, so that same
text printed literally as &. Fixed with decodeHtmlEntities() — sets
innerHTML on a scratch element and reads textContent, which both strips
tags and decodes entities using the browser's own parser — wired into every
text-drawing helper in the PDF generator, not just the section where the
bug was first spotted.
- Deterministic, not probabilistic. Given the same choices in the same order, the engine always produces the same outcome — there is no randomness, no dice roll, no hidden variance. What you see in the Counterfactual Lab is an exact re-run, not a sampled estimate.
- Illustrative shock magnitudes, not calibrated forecasts. Every shock
channel value, sensitivity multiplier, and lever effect is directionally
grounded in what actually happened in the referenced historical episode,
but the specific numbers were chosen to make a 4-round game legible and
playable, not fit to real data. The same applies to the KPI dashboard's
real_metricsbaselines (revenue scale, margins, headcount) — they're directionally realistic for each industry's rough scale, not sourced from any specific company's filings. - No real company or financial data. Nothing in this simulator is
scraped, sourced, or calibrated from an actual company, market, or
dataset. Every number is either hand-set in
data_gen.pyor derived from those hand-set numbers by a documented formula. - A single composite score, not a full decision-theoretic model. The counterfactual scorer's "best alternative" is a simple weighted-sum composite across six metrics — useful for comparing options within one function in one round, not a claim about optimal strategy in general.
Nothing here should be read as a prediction, a risk model, or investment or business advice.
- Difficulty levels. A single fixed shock/lever balance was chosen deliberately so every playthrough of a given industry+scenario is comparable and the counterfactual scorer stays meaningful — variable difficulty would mean the "optimal path" means something different every game.
- Uncertainty ranges / probabilistic outcomes. Sampling shock or effect magnitudes from a distribution would make the game more "realistic" but would break the Counterfactual Lab's exact-replay guarantee, which depends on the engine being deterministic. Kept out on purpose, not missing by oversight.
Vanilla HTML/CSS/JS + Chart.js +
jsPDF (both via cdnjs). No framework,
no bundler, no backend. All content lives in data_gen.py and compiles to
data.js — a plain const STRATLAB_DATA = {...} global loaded before
app.js — so content is versioned independently of UI code and the
deployed app never makes a fetch/XHR call of any kind.
strat-lab/
data_gen.py # single source of truth for all content — edit this
data.js # generated output, committed (regenerated by CI on push)
index.html # app shell, 4 screens
style.css # war-room / decision-terminal design system
app.js # decision engine, impact engine, scoring/counterfactual engine, UI
assets/ # hero image
.github/workflows/deploy.yml
To regenerate data.js after editing data_gen.py:
python3 data_gen.py
GitHub Actions re-runs this on every push (in case data_gen.py changed)
and deploys the result to Pages — see .github/workflows/deploy.yml.
Built by Niranjan Desai.