Three bug reports I could not reproduce, a spec that turned out to be the actual bug, and what the guess tree looks like now.
Kludoku's what-if tree is the feature I am proudest of and the one that has generated the most "this is broken" reports. This week I finally understood why: it was never broken in the way the reports said. It was built on a different idea of what a guess is from the one every player brings to it, including me when I play.
Three of them, over one day, all mine, all from real play:
None of them reproduced. I built the exact shapes described, in the game's own store, headless, with the real side strings and the real column presses, and every time the placements unwound correctly and the claims unwound correctly. I did it in a browser, as a premium account, at two levels of depth, on a phone viewport, with forced clicks and without. Clean, every time.
When a bug will not reproduce after that much effort, the description is wrong — not the reporter, the description. So I wrote down what I actually do, in a notation precise enough to argue with.
Four suspects: Hector, Maria, Julius, Peter. X(a, b, c) is person X at option
c, reached through option a of the first fork and option b
of the second. "Guess as second position" means: they are on a square, and I want to try another.
1. Place H(1), guess second position H(2)
2. Place J(2,1) in a column
3. Switch to H(1), J(2,1) disappears
4. Place J(1,1), guess J(1,2)
5. Place P(1,J2,1), guess P(1,J2,2)
6. Place M(1,J2,P2,1)
7. Switch to P(1,J2,1), M(1,J2,P2,1) disappears
8. Place M(1,J2,P1,1) in the same column as J(2,1)
9. Switch to H(2): everything else disappears, J(2,1) claims the column
10. Switch to H(1): the previous H1 tree reappears, up to M(1,J2,P1,1)
11. Switch to P(1,J2,P2): M(1,J2,P2,1) appears
Then I ran it, step by step, against the store. This is what the tree that shipped actually did:
| step | before | after |
|---|---|---|
| 3. Switch to H(1): J(2,1) disappears | pass | pass |
| 7. Switch to P(1,J2,1): M(1,J2,P2,1) disappears | pass | pass |
| 7. …and no mini portrait for M either | fail | pass |
| 9. Switch to H(2): M and P disappear, J(2,1) claims the column | pass | pass |
| 9. …and J(1,1) is gone too | fail | pass |
| 10. Switch to H(1): the H1 tree reappears | fail | pass |
| 11. Switch to P(1,J2,P2): M(1,J2,P2,1) appears | pass | pass |
Three failures. None of them was a placement leaking through a switch — the thing all three reports described. They were three places where the tree's model and mine disagreed, and once I could see the disagreement, every report made sense.
Here is the shape the tree had. You place Hector. You open a guess and put Hector somewhere else. The guess is a branch, forked from the board you were on, and that board — the committed board — is the base of every branch.
That explains step 9. Julius placed "under H(1)" was placed on the committed board, and the committed board is the floor of every branch, so Julius showed inside H(2). And it explains step 10: "switch to H(1)" meant "show the committed board", which is not where the work was — the work was in branches below it, which are correctly off the path from there. There was nothing to remember a leaf against, because the thing I called H(1) did not exist.
It also explains the reports. If you want two alternatives and you press + inside a branch, you get a deeper guess, never a sibling — there was no control that made one. And if you press + right after placing a suspect, nothing happens at all, because placing clears the selection and a guess needs an anchor. So the sequence a player gets when they want two options is: nothing, then a chain. A chain inherits. From the board, that is indistinguishable from "the placements did not disappear".
The fix is the diagram everybody already had in their head:
Step 7's mini portrait was a different fault with a longer history. The mini portraits are the tree's whole visible surface: the other side of every fork you are standing on, drawn on the square it would take. Months ago a player — me again — picked a portrait at depth two, dropped to the committed board, and found the branch just left had no portrait leading back to it. The fix put the whole tree on the board from everywhere: every branch, at any depth, one press away. It was even pinned by a test with that name.
And it was wrong, in the way that fixes for navigation problems often are: it solved getting back by making it impossible to see where you are. Standing on H(1), a guess made under H(2) — an option I had not taken — sat on my board as if it were mine. My own design note for the tree, written before any of it existed, says what should happen: "the tree swaps, my previously selected Henri becomes a mini head and the other choices disappear."
So the rule is now the one the code's own comment had always claimed and never enforced: a branch off your path is offered only if it was forked from a node on your path. Getting back to a deep branch is done by the thing that should have done it all along — selecting an option puts you back on the leaf you were last on beneath it.
Four changes, all in the store, none in the renderer — the board draws what the store says, and the store was the thing with the wrong idea.
A guess about a placed suspect opens a split. The kept twin carries the suspect where they were; the guess starts empty; both are children of where the player stands, and the player lands in the guess. A second guess about the same suspect from the same node finds the twin already there and becomes option 3 — H(1), H(2), H(3) are one fork, not two.
const at = placedEffective()[anchor];
const twin = at != null ? state.hypos.find(x => x.kept && (x.parent ?? null) === parent
&& x.anchor === anchor && x.placed[anchor] === at) : null;
const kept = at != null && !twin
? { id: id++, anchor, parent, kept: true, placed: { [anchor]: at }, order: [anchor] }
: null;
const guess = { id, anchor, parent, placed: {}, order: [] };
const hypos = [...state.hypos, ...(kept ? [kept] : []), guess];
Selecting an option lands where you left off — unless you are walking up. Re-entering a subtree from outside restores its last leaf. Choosing an ancestor of where you stand lands on that ancestor, or an intermediate node could never be looked at once anything was forked beneath it.
const landing = (id) => {
if (id == null) return null;
const cur = state.activeHypo;
if (cur != null && cur !== id && lineageOf(cur).some(b => b.id === id)) return id;
const r = state.hypoLast[id];
if (r == null || r === id || !branchById(r)) return id;
return lineageOf(r).some(b => b.id === id) ? r : id;
};
The other choices disappear. One line, and the line the comment above it had described for months.
if (b.parent != null && !onPath.has(b.parent)) continue;
The numerals are distances. The other side of your own fork has no number; the other option of the fork one level up reads −1; a deeper guess you made from here reads +1. The off-path portraits had been counting from one step too far back, so a fresh fork labelled its other option −1 and a deeper guess got no label at all — which is why the "+1"s and "−2"s I had designed for seemed to have vanished. They had not; they started one level late.
Two smaller things. The cap counts guesses, not nodes, because a kept twin is bookkeeping and a free account should not go from two ideas to one the day this shipped. And refuting a guess folds a twin nobody built on, while a twin with work under it stays.


The first probe I wrote to reproduce the column claim reported "no leak" from a tree that had never held a claim, because a column press that misses the strip by four pixels is silent, and so is one made with nobody selected, and so is one on a strip somebody already occupies. Three silent exits, one symptom. The store now records which exit each edge press took, so the next report of "it did nothing" is one look instead of a day.
The second: the video pipeline's verifier checked that the clip's file returned 200. It did — the single-page app's fallback returns 200 for anything, with three kilobytes of HTML. The embed sat there with no source and no error. A status is not a file; it asks for the content type now. Both are the same lesson the whole week was: measure the thing you are actually asking about, not the thing next to it.
Nothing visual ships here without a hostile review of screenshots — light and dark, desktop and phone — and this one came back with numbers rather than opinions. The two chips of a split read "1" and "1": placement counts, on chips the guide called option 1 and option 2. They now carry the option number, in the fork's own colour, and the colour stays on the chip at rest instead of appearing only on the lit one. On a phone the chips measured 30px tall — a rule meant to make them 34px on touch was inside a 30px box — and at two levels of depth the row was 410px wide on a 390px screen, with the + chip cut in half and the 👁 chip off the screen. The chips are 40px on touch now and the row wraps. The column-claim hatch measured 1.35:1 against a green floor in dark. It could not simply be made heavier — an older complaint, "it is too much", caps the band's ink under the checkerboard's and a test holds it there — so each stripe got a dark edge inside the same budget: on the same pixels the hatch now touches roughly twice the square (24–49% of it, from 4–31%) and its strongest tenth reads 1.9–2.1:1 where it read 1.3–1.6:1. That is a hatch you can see on every biome I tried; it is still not 3:1, and on a same-hue floor the typical stripe pixel stays under 2:1. The devlog itself was 664px wide on a 390px phone, because two 640px diagrams had no rule capping them, and the clip was re-shot on the phone layout: in a desktop frame its subject, a chip, was 8px tall at the size the guide embeds it on a phone.
One thing the review could not settle on its own: the −1 numeral on a mini portrait was 4.7px tall on a medium board on a phone, and a badge big enough to read, clamped inside a 36px square, landed on the face it belongs to. The call was that the face expands when you press it, so covering a little at rest is fine — and the badge now holds a floor of 8px, growing up to 1.8× on small squares, and on those squares it sits beside the head along the tile's edge rather than riding its corner. Measured after: 9px of digit on the phone, the face still showing; unchanged on the desktop, where 13 units was already 9px.
Published · More from the devlog