Put a row of numbers at the bottom, write the sum of each adjacent pair on the row above, and repeat until you reach the apex. That puzzle is a number pyramid — also called a math pyramid, a calculation pyramid, or in Japanese 計算ピラミッド. It turns up on primary-school worksheets and in brain-training apps.
Making them by hand for a printed sheet is easy. Generating them endlessly inside an app is not, because a pyramid with holes punched in it at random will often have more than one solution, or no reachable one at all. This post covers the rules, how to generate puzzles that provably have a unique answer, and how to control difficulty with something better than "number of rows".
The rules
With [3, 1, 4, 2] on the bottom, you add your way up:
- Second row:
3+1=4,1+4=5,4+2=6 - Third row:
4+5=9,5+6=11 - Apex:
9+11=20
Which stacks up like this:
20 9 11 4 5 6 3 1 4 2
To turn it into a puzzle, you blank out some of the cells and ask for them back.
Solving it
What you do depends on where the holes are.
Both cells below are filled — just add. This is the easy case.
The cell above and one of the two below are filled — now it is subtraction, since above = left + right gives right = above − left. This is where the difficulty actually lives: a puzzle that is pure addition and one with three subtractions in it feel nothing alike, even at the same size.
Nothing around it is filled — that cell isn't determined on its own. You have to look at the pyramid as a system of equations.
The whole pyramid follows from the bottom row
One property matters more than any other: the contents of the pyramid are fully determined by its bottom row.
Write the bottom row as b[0], b[1], ..., b[n-1]. The cell r rows up and i from the left is:
cell(r, i) = Σ[k=0..r] C(r, k) · b[i + k]
where C(r, k) is a binomial coefficient. Pascal's triangle falls straight out of the construction — two rows up, for instance, a cell is 1·b[i] + 2·b[i+1] + 1·b[i+2].
So every cell is a linear form over the n unknowns in the bottom row. Designing a puzzle means choosing enough clues to recover those n unknowns.
The naive generator, and where it breaks
Start with the obvious approach:
type Pyramid = number[][]; // pyramid[0] is the bottom row function build(bottom: number[]): Pyramid { const rows: Pyramid = [bottom]; while (rows[rows.length - 1].length > 1) { const below = rows[rows.length - 1]; rows.push(below.slice(0, -1).map((v, i) => v + below[i + 1])); } return rows; }
Pick a random bottom row, stack it up, blank some cells. It looks fine, and then two things go wrong depending on which cells you blanked:
- Multiple solutions — not enough clues, so more than one bottom row fits
- Technically solvable, practically not — unique in the mathematical sense, but only reachable by solving simultaneous equations
The first is a real bug, and it surfaces as a user entering a correct answer and being told they are wrong.
Checking uniqueness
This is where the linear form pays off. For every revealed cell, build the length-n vector of its binomial coefficients. Stack those vectors into a matrix. If the rank is n, the bottom row is uniquely determined — and therefore so is the whole puzzle.
function binomial(n: number, k: number): number { let result = 1; for (let i = 0; i < k; i++) result = (result * (n - i)) / (i + 1); return Math.round(result); } /** The cell r rows up, i from the left, expressed over a bottom row of `width`. */ function coefficients(row: number, index: number, width: number): number[] { const v = new Array<number>(width).fill(0); for (let k = 0; k <= row; k++) v[index + k] = binomial(row, k); return v; } /** Matrix rank by Gaussian elimination. */ function rank(matrix: number[][]): number { const m = matrix.map((row) => [...row]); const cols = m[0]?.length ?? 0; let r = 0; for (let c = 0; c < cols && r < m.length; c++) { let pivot = -1; for (let i = r; i < m.length; i++) { if (Math.abs(m[i][c]) > 1e-9) { pivot = i; break; } } if (pivot === -1) continue; [m[r], m[pivot]] = [m[pivot], m[r]]; for (let i = 0; i < m.length; i++) { if (i === r || Math.abs(m[i][c]) < 1e-9) continue; const factor = m[i][c] / m[r][c]; for (let j = c; j < cols; j++) m[i][j] -= factor * m[r][j]; } r++; } return r; } /** Given the revealed (row, index) cells, is the puzzle uniquely solvable? */ function hasUniqueSolution(revealed: Array<[number, number]>, width: number): boolean { if (revealed.length < width) return false; const matrix = revealed.map(([row, index]) => coefficients(row, index, width)); return rank(matrix) === width; }
Binomial coefficients grow fast, so floating-point elimination starts getting unreliable somewhere around eight rows. For the 3–6 rows an app actually ships, this is fine; if you plan to go bigger, switch to exact rational or integer-preserving elimination (Bareiss, for instance).
applyBlanks is a one-liner that punches the holes:
type Puzzle = Array<Array<number | null>>; function applyBlanks(pyramid: Pyramid, blanks: Array<[number, number]>): Puzzle { const puzzle: Puzzle = pyramid.map((row) => [...row]); for (const [r, i] of blanks) puzzle[r][i] = null; return puzzle; }
Generation is then generate-and-reject:
function generate(width: number, blanks: number, maxBottom = 9): Puzzle { for (let attempt = 0; attempt < 200; attempt++) { const bottom = Array.from( { length: width }, () => 1 + Math.floor(Math.random() * maxBottom), ); const pyramid = build(bottom); const cells: Array<[number, number]> = []; pyramid.forEach((row, r) => row.forEach((_, i) => cells.push([r, i]))); // Shuffle, then blank the first `blanks` of them for (let i = cells.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [cells[i], cells[j]] = [cells[j], cells[i]]; } const revealed = cells.slice(blanks); if (hasUniqueSolution(revealed, width)) { return applyBlanks(pyramid, cells.slice(0, blanks)); } } throw new Error(`${blanks} blanks is not achievable at width ${width}`); }
Running out of attempts means that blank count is impossible in principle, not unlucky. Throwing rather than swallowing it keeps the mistake where it belongs — in the difficulty configuration.
"Unique" and "solvable in your head" are different things
Rank n says nothing about whether a person can get there. Blank the entire bottom row and reveal only the upper cells and the puzzle is still unique — but solving it means doing linear algebra by hand.
For a mental-arithmetic puzzle you want a stronger property: every blank can be filled one at a time, using only addition or subtraction.
/** Can every blank be resolved one cell at a time from what is already known? */ function isSolvableStepwise(known: boolean[][]): boolean { const state = known.map((row) => [...row]); let progress = true; while (progress) { progress = false; for (let r = 1; r < state.length; r++) { for (let i = 0; i < state[r].length; i++) { const above = state[r][i]; const left = state[r - 1][i]; const right = state[r - 1][i + 1]; const filled = [above, left, right].filter(Boolean).length; // Two of the three known means the third is one add or subtract away if (filled !== 2) continue; if (!above) { state[r][i] = true; progress = true; } else if (!left) { state[r - 1][i] = true; progress = true; } else if (!right) { state[r - 1][i + 1] = true; progress = true; } } } } return state.every((row) => row.every(Boolean)); }
Run this alongside hasUniqueSolution and you keep only puzzles that are both uniquely determined and reachable by a human procedure. Dropping this second check on purpose is a reasonable way to build an expert mode that does demand simultaneous equations.
Difficulty, measured
Row count is not the only lever, and it isn't the best one. In rough order of how much they actually matter:
Number of subtractions. How often the solver hits "above and one below are known". This is the single clearest difficulty signal. Pure addition versus three subtractions is a different puzzle at the same size.
Number of carries. 7 + 8 costs more than 3 + 4. Count the carrying operations across every pair in the finished pyramid and you have a usable metric.
How many values must be held at once. This is the working-memory load. Simulate the solving order and take the maximum number of values that are "still needed but not yet resolved" at any point. It tracks perceived difficulty far better than row count does.
Range of the bottom row. 1–9 versus 1–20 changes the digit count. Worth having, but raising it mostly adds tedium rather than difficulty, so it belongs low on the list.
Plenty of apps treat row count as the difficulty dial. In practice a four-row puzzle full of subtractions beats a five-row addition-only one comfortably.
The app
I ship this generator as Pyramid Mental Arithmetic — an iOS app and a browser version. Its difficulty tiers are built on the "values held at once" metric above rather than on row count.