workshop private

← all creations

Overhang

mechanics · created 2026-09-11

Bricks off the edge of a bench, where the same greedy rule — always take the furthest legal spot — reaches 0.500 lengths applied upward and 1.748 applied downward, with the same bricks and the same physics. The build order is the whole game.

physicssimulationgame-feelcanvas

Stack bricks off the edge of a bench and reach as far as you can. There is one rule and it is the only rule:

the centre of mass of everything above a cut must lie within the footprint of whatever is directly below it.

Apply it at every horizontal cut in the stack, plus one more at the bench, and that is the entire model. src/overhang.mjs is a hundred lines of code — the rule, the two legal moves in closed form, and the reference builders — and nothing else. No solver, no timestep, no collisions.

The first thing you will do, and why it does not work

Lay a brick. Push it out until it is about to go, which is half a length. Lay another on top, push it out until it is about to go. Lay another. This is the obvious strategy, everybody plays it, and it never gets anywhere:

bricksgreedy on topgreedy underneathH(n)/2what the last brick bought
10.50000.50000.5000
20.50000.75000.75000.0000
40.50001.04171.04170.0000
180.50001.74761.74760.0000
10000.50003.74273.74270.0000

Brick one buys half a length. Bricks two through one thousand buy, between them, nothing at all — not a little, not diminishing returns, zero — and the reason is two lines of arithmetic:

The demo’s watch: greedy on top button plays this out: eighteen bricks placed perfectly legally, ending as a flush column hanging exactly half off, with the optimum drawn in dashes beside it.

And it is not the first brick’s fault. Hold brick one back to d and then play greedily on top of it, and the run reaches min(d + 0.5, 1 − d):

first brick reaches2 bricks4 bricks12 bricks40 bricks
0.50000.50000.50000.50000.5000
0.30000.70000.70000.70000.7000
0.25000.75000.75000.75000.7500
0.20000.70000.70000.70000.7000
0.08330.58330.58330.58330.5833

Sweeping every first placement, the ceiling of the entire greedy-on-top strategy is 0.750, reached with two bricks, and the rows are flat, so brick three onward is decoration. Forty bricks could have done 2.139.

The same greed, pointed the other way

Now build downward. The tower you have is a rigid object; slide a fresh brick under it and push the whole assembly out. Take everything on offer, every time — the identical rule that just failed.

Two constraints survive the rigid shift (the joints inside the tower ride along unchanged), and they give the move’s size directly:

d  ≤  −C_all + 1/(2(n+1))

For a tower already maximal, C_all = 0 and the shift is exactly 1/(2(n+1)). Measured against the closed form, brick by brick:

brick #kmeasured shift1/(2k)reach after
10.5000000.5000000.5000
20.2500000.2500000.7500
30.1666670.1666670.9167
40.1250000.1250001.0417
100.0500000.0500001.4645

Which sums to ½·H(n), the harmonic series, which diverges. Downward there is no ceiling at all, and greed is not merely adequate, it is exactly optimal at every single step.

Same bricks. Same physics. Same “take the most you can” instruction. 0.500 one way, unbounded the other. The build order is not a detail of how you get to the tower — it is the thing that decides whether the tower is reachable.

Why the orders are not symmetric

The two stacks are the same object. demo/ will draw either. The asymmetry is entirely in what each move has to know.

Going down, a move only ever changes things below what is already placed. The bricks above it keep their relative positions, their joints keep their margins, and the move cannot invalidate a decision you already made. Local information is complete information.

Going up, every brick you lay constrains every brick still to come — and by how much depends on how many are still coming, which you do not know. Lay the optimal eight-stack from the bottom and ask, at each step, how far that brick was allowed to go against how far it actually went:

brick (from bottom)placed atfurthest legalheld back byj/(2(j+1))
1-0.93750-0.500000.437500.43750
2-0.86607-0.437500.428570.42857
4-0.68274-0.282740.400000.40000
6-0.39107-0.057740.333330.33333
7-0.141070.108930.250000.25000
80.358930.358930.000000.00000

Not one brick but the last takes what it is offered, and the holdback is j/(2(j+1)) where j is how many bricks are still coming — a quarter of a length with one left to place, climbing toward half a length the deeper the pile. Downward that column is zero all the way down.

So the upward builder is not playing badly. They are playing a game that requires a number they were never given, and the penalty for guessing it wrong is steep. Rows are what the builder planned for, columns what the pile actually held:

planned forgot 2got 4got 6got 10got 18best for that count
20.7500.750
40.2921.0421.042
100.1060.2390.4231.4641.464
180.0570.1220.1960.3891.7481.748
400.0250.0520.0800.1420.2942.139

The diagonal is optimal; everything off it is a bad guess about the depth of a pile. Plan for forty, get ten, and you reach 0.142 — worse than one brick laid on its own and left alone.

The price of a metre

Unbounded is not the same as cheap. H(n) ≈ ln n + γ inverts to n ≈ e^(2R−γ), and the useful reading is the derivative:

reach (L)bricks× previouse^(2R−γ)
0.5011.5
1.0044.0004.1
1.50112.75011.3
2.00312.81830.7
2.50832.67783.3
3.002272.735226.5
4.001,6742.7181673.7
5.0012,3672.71812367.0

Every extra half-length of reach costs e times as many bricks, and it is the same factor every time, forever. Nobody put 2.71828 in this file. It is what you get when the thing you are summing is 1/k and you ask how many terms buy a fixed amount. Clearing ten lengths would take 272 million bricks.

The optimum is a knife edge everywhere at once

This is the bit that surprised me when I drew it. In the optimal stack there is no slack anywhere — every joint is simultaneously on the point of letting go:

n = 8, cutload CoMsupport right edgemargin
bench-0.0000000.0000004.2e-17
2 on 10.0625000.0625004.9e-17
5 on 40.3172620.3172625.6e-17
8 on 70.8589290.8589291.1e-16

Turn on balance in the demo and you can see it: a chevron at every joint, each one sitting exactly on the right-hand edge of the brick beneath it, all the way down. That is what the 0.000 readout means, and it is why the tower looks like it should not be standing. It should not. It is at the boundary.

H(n)/2 is a theorem for single-file stacks (Paul B. Johnson, Leaning Tower of Lire, 1955), so the search below is a check on my model rather than on the mathematics. Perturb the optimum randomly, throw away everything that falls over, and see how far the survivors get:

bricksceiling H(n)/2triesstill standingbest of thosebeat it?
30.9166760,0006,9920.91666no
51.1416760,0001,3991.14165no
81.3589360,0001221.35887no
121.5516160,00031.50263no

The survivor count collapsing from 6,992 to 3 is the knife edge showing up again from the other side: at twelve bricks, essentially no random nudge leaves the thing standing.

What is physics here and what isn’t

Playing it

Pointer or arrow keys to aim, click or Space to place, M to switch between on top and underneath, R to rebuild. The ghost brick is green while the placement stands and red while it doesn’t, so toppling is always a choice. optimum draws the best stack your remaining bricks could reach; balance draws the centre-of-mass chevrons. Three autopilots — greedy on top (0.500), planned on top (1.748, and only because it was told the count in advance), and greedy underneath (1.748, knowing nothing) — are the argument in about nine seconds.

Reuse

src/overhang.mjs is framework-free and has no canvas in it. A stack is just an array of left-edge positions, bottom brick first, in brick lengths, with the bench edge at x = 0. isStable() / failingJoint() are the rule; maxOnTop() and minOnTop() give the legal window for a new top brick in closed form (every cut contributes one linear bound, and the answer is the tightest); maxUnderShift() / slideUnder() are the downward move; harmonicStack(), greedyTop(), greedyUnder() and plannedTop() are the reference builders. bricksFor() inverts the price curve.

The demo is demo/index.html with its own copy of the module (ADR-0002) and a window.__demo hook the rig drives. node scripts/measure.mjs prints every table above and ends in a self-check. node scripts/screenshot-demo.mjs regenerates the thumb and media and doubles as the smoke test — it drives the real keyboard and pointer paths in a browser and asserts that four spacebars underneath land on H(4)/2 = 1.0417, and that eighteen greedy bricks on top land on 0.500.