Some answers are brutal to derive with a formula. But running the same random experiment ten thousand times? That's just a loop with a counter. That's the entire trick behind Monte Carlo simulation β trade an exact derivation you can't do for a statistical estimate you can β and you already know how to write it.
Watch the darts find Ο βPicture a jar of jellybeans. You could count every bean by hand β slow and error-prone. Or you could grab handfuls at random, look at what you got, and after enough handfuls you'd know the jar pretty well. Monte Carlo is the second approach, turned into code: model the situation, feed it random inputs, run it a huge number of times, and read the answer off the results.
Instead of deriving a clean equation, you let random chance explore the problem for you β one possible outcome at a time. The dice aren't a bug; they're the method.
One random trial tells you almost nothing. A million of them, averaged together, tells you almost everything. Volume is what converts noise into a signal.
Its real superpower: it hands you a whole distribution of possible outcomes and how likely each one is β not a single fragile point estimate you have to take on faith.
The name is pure history, not jargon. In the 1940s, working on neutron diffusion at Los Alamos, Stanislaw Ulam and John von Neumann needed answers to integrals that no formula could crack. Their colleague Nicholas Metropolis suggested a code name after the Monte Carlo casino in Monaco β where Ulam's uncle famously gambled. The label stuck, and so did the method: when you can't compute the answer, gamble your way to it.
Here's the famous one. Draw a square, and inside it a quarter-circle. Now throw darts at the square completely at random. The darts that land inside the curve, compared to the total, are directly linked to Ο. No circle formula required at the throwing stage β just a coin-toss of geometry and a tally. Hit the buttons and watch the estimate firm up.
True value: Ο = 3.14159β¦ Notice how jumpy the estimate is with a handful of darts β and how it steadies as the count climbs. That's the whole point of the next section.
The square has area 1. The quarter-circle inside it has area Ο/4. Throw darts evenly across the square and the fraction that land inside the curve will hover around Ο/4. Flip that around and you get your estimate:
Ο β 4 Γ (inside Γ· total)
A dart is "inside" when its distance from the corner is β€ 1 β i.e. xΒ² + yΒ² β€ 1. That single comparison is the only geometry in the whole program. We never invoke Ο to find Ο; it falls out of the counting.
// The entire simulation. Yes, really. let inside = 0; const N = 1_000_000; for (let i = 0; i < N; i++) { const x = Math.random(); // 0 β¦ 1 const y = Math.random(); // 0 β¦ 1 if (x*x + y*y <= 1) inside++; } const pi = 4 * inside / N; console.log(pi); // β 3.14159β¦
There's one theorem doing all the work here, and it has an intimidating name β the Law of Large Numbers β for a simple idea: as you repeat a random experiment, the average of your results converges to the true expected value. Each dart is a Bernoulli trial β inside with probability Ο/4 β so the running fraction must drift toward Ο/4. Watch the estimate stagger around early on, then lock onto Ο and ride down inside a narrowing funnel.
Think of each trial as a noisy vote. A few votes can swing wildly. But as votes pile up, the random over- and under-shoots tend to cancel, and the signal underneath rises to the surface. You're not making any single trial more accurate β every dart is exactly as random as the last. You're drowning the randomness in volume until what's left is the underlying truth.
That gold band isn't decoration. The width of an estimate's likely error scales like one over the square root of the number of samples β so the band pinches inward as N grows, but it never collapses to zero. Your estimate stays somewhere inside it. That single fact, error β 1/βN, defines almost everything about how Monte Carlo behaves in practice. It's the next section.
Here is Monte Carlo's defining trade-off, measured directly. For each sample count N, we run many independent simulations and record the typical (root-mean-square) error. Plotted on log-log axes, the measured errors fall almost perfectly onto a straight line β the theoretical 1/βN curve. There's no escaping its slope.
Because error shrinks like 1/βN, halving the error costs 4Γ the samples, and squeezing out one more decimal digit (a 10Γ drop in error) costs roughly 100Γ the work. That's the wall. It's why Monte Carlo is wonderful for a fast "good enough, with honest error bars" answer and miserable when you need many guaranteed digits β for which a deterministic numerical method will usually crush it.
Notice what's missing from error β 1/βN: the dimension of the problem. The convergence rate doesn't care whether you're integrating over 2 variables or 200. Classical grid methods care enormously β and that asymmetry is exactly where Monte Carlo stops being a toy and starts being indispensable. Hold that thought for section 07.
The dart trick wasn't special to circles. Estimating Ο was secretly measuring an area by counting hits β and area under a curve is exactly what an integral is. Pick any function below, throw darts into the unit box, and the fraction that land under the curve estimates the integral. Try the parabola, the sine hump, the quarter-circle. The estimate converges to the true area the same way the Ο estimate converged to Ο.
true area = β
Switching the function resets the board. The box has area 1, so the estimate is simply under Γ· total.
This "hit-or-miss" method is the gentlest form of Monte Carlo integration. In real code you'd usually skip the y-coordinate entirely: sample x at random, evaluate f(x), and average those values β the average value of the function times the width of the interval is the integral. Same idea, lower variance, and it generalizes to any number of dimensions without changing a line of the logic.
Strip away the Ο and the integrals and what's left is a recipe that fits almost any uncertain system. Once you see the shape of it, you'll spot Monte Carlo problems everywhere β pricing, scheduling, queueing, reliability, games.
function monteCarlo(runs) { const results = []; for (let i = 0; i < runs; i++) { const outcome = simulateOneTrial(); // steps 1 & 2 results.push(outcome); // step 3 } return summarise(results); // step 4: mean, %, histogram, error bar⦠}
The only hard part is honest in step 4: always report the spread, not just the mean. An estimate without an error bar is a guess wearing a lab coat.
For a one-dimensional integral, you'd never reach for Monte Carlo β a deterministic rule like the trapezoid or Simpson's converges far faster and lands on many digits cheaply. So where does randomness actually win? When the problem has many dimensions, or no formula at all.
Suppose you cover each axis with a modest grid of 100 points to integrate a function. In 1 dimension that's 100 evaluations. In 2 dimensions it's 100Β² = 10,000. In 10 dimensions it's 100ΒΉβ° = 10Β²β° evaluations β more than any machine will finish before the heat death of your sprint. Grid cost explodes as points^dimensions. This is the "curse of dimensionality," and it kills exact and grid-based methods stone dead.
Monte Carlo's error is 1/βN β and N is the number of random samples, independent of the dimension. Ten dimensions or ten thousand, a million samples is a million samples. The convergence rate doesn't budge. So past a handful of dimensions, the slow-but-flat 1/βN method overtakes the fast-but-exploding grid, and there is simply no contest. High-dimensional integrals are Monte Carlo's home turf.
This is not an academic edge case. A financial derivative whose payoff depends on a hundred correlated price paths, a rendered film frame integrating light over every surface and bounce, a physics calculation over the states of millions of particles β these are integrals in absurdly high-dimensional spaces. No formula exists, and no grid will fit. Monte Carlo isn't the convenient choice there; it's frequently the only choice.
Ο is cute, but here's where Monte Carlo earns its keep day to day. You've got five chunks of work, each with a best case, likely case, and worst case. Adding up the "likely" numbers gives one tidy estimate β and it lies to you, because work rarely all goes to plan at once. Instead, simulate the whole project thousands of times with random task durations and look at the distribution of outcomes.
Each bar = how often, out of all the simulated projects, the total landed in that range of days. The red line is your deadline.
Drag the deadline after running β the odds update instantly off the same simulated outcomes.
The naΓ―ve "add up the likely numbers" answer is 29 days. Run the simulation and you'll usually find the odds of actually hitting that are uncomfortably low β because each task draws from a skewed triangular distribution and the long tails stack up. That gap is risk you couldn't see in a single point estimate β and the reason people reach for Monte Carlo.
Brute-forcing more samples is the obvious lever, but 1/βN makes it an expensive one. The smarter move is to attack the variance β the noisiness of each sample β so the same N buys a tighter answer. The simplest trick is stratification: instead of scattering points purely at random, divide the space into equal cells and place one jittered point per cell, so the samples can't clump or leave gaps.
Hit Compare: we run 200 independent Ο estimates of 256 points each, both ways, and measure how much the estimates wobble (their standard deviation). Lower is better.
Stratified sampling typically cuts the wobble noticeably for the same number of samples β free accuracy, just from spreading the points fairly. It's one of a family of variance-reduction techniques: importance sampling (spend samples where the function is big), antithetic variates (pair each sample with its mirror image so errors cancel), and control variates (subtract off a related quantity you can compute exactly). All chase the same prize β bend the constant in front of 1/βN down, since you can't change the βN itself.
Every dart you threw came from Math.random() β and that function isn't actually random at all. It's a pseudo-random number generator (PRNG): a deterministic formula that, from a starting seed, churns out a stream of numbers that merely looks random. Same seed, same stream, every time. For Monte Carlo that's a feature, not a flaw.
A PRNG is fast, repeatable, and portable. The toy below is a linear congruential generator β the textbook example. Real engines (xorshift, PCG, Mersenne Twister) are far better, but the principle is identical: a hidden integer state advances by a fixed rule each call. Because it's deterministic, you can seed it to reproduce a run exactly β essential for debugging a simulation, writing a regression test, or letting a teammate reproduce your numbers to the digit.
// A linear congruential generator (LCG). let seed = 42; // same seed β same stream function rng() { seed = (1103515245*seed + 12345) % 2147483648; return seed / 2147483648; // 0 β¦ 1 }
True random numbers come from physical entropy β thermal noise, radioactive decay, timing jitter β exposed by your OS (and wrapped by crypto.getRandomValues). They're unpredictable and unrepeatable, which is exactly what cryptography needs and exactly what makes them worse for simulation: you can't reproduce a run, and they're slower to draw.
The risk that bites Monte Carlo is a low-quality PRNG: short period (it loops and re-uses numbers), or hidden correlations (consecutive draws line up on planes in higher dimensions). A bad generator can quietly bias an answer that looks perfectly converged. The fix is a well-tested modern PRNG β not switching to "true" randomness.
The same loop-and-tally you wrote for Ο runs quietly under a surprising amount of modern software and science. A few of the big ones:
Price exotic options by simulating thousands of random price paths; estimate Value-at-Risk by simulating a portfolio's worst plausible days. When the payoff has no closed-form formula, you simulate it.
The original use case: neutron transport, particle showers, and statistical mechanics. Reliability engineers simulate component failures to estimate how long a whole system survives.
Path tracers in film and games estimate the light arriving at each pixel by firing thousands of random rays and averaging β a Monte Carlo integral over every possible path light could take.
Bootstrapping resamples data at random to put error bars on almost any statistic; permutation tests and Bayesian MCMC estimate things no formula will hand you in an A/B or causal analysis.
Queues, logistics, capacity planning, epidemic spread β simulate the system under random demand thousands of times to see the full range of outcomes, not just the rosy average.
Monte Carlo Tree Search powers strong game-playing agents by random-rollout-sampling possible futures from each move β randomness as a way to scout an intractable decision tree.
It converges in probability, never lands exactly. You always quote an estimate Β± an error bar that shrinks like 1/βN. Treat any Monte Carlo number without its uncertainty as half-finished.
Recovering digits of Ο mostly measures your sample count, not randomness quality β the result is dominated by 1/βN noise. A subtly biased generator can still produce a passable Ο while failing real randomness tests. It's a weak RNG check at best.
1/βN has brutal diminishing returns, and it can't rescue a wrong model or a biased sampler. Garbage in, garbage out β just with tighter, more convincing error bars wrapped around the wrong answer.
If you can simulate one outcome, you can estimate anything.
Model a trial. Feed it randomness. Loop it. Average it β and report the error bar. You walked in knowing how to write a loop and a counter, and that was always the hard part. The rest is letting randomness do the exploring while repetition sharpens the answer, on the iron schedule of 1/βN. Cheap to start, honest about what it doesn't know, and unbeatable when the formula simply isn't there.