Two grids that agreed by coincidence
For four evenings I told Claude that my vault was mostly empty and would not take a war hammer. For four evenings it explained to me why I was wrong. It was a different explanation each time, each one plausible, and all …
For four evenings I told Claude that my vault was mostly empty and would not take a war hammer. For four evenings it explained to me why I was wrong. It was a different explanation each time, each one plausible, and all four amounted to you have misread your own screen.
I had not misread my screen. The screen was lying.
The artefact that ended it
The build eventually gained a dev-panel row that prints what the game thinks a store contains. I pressed it in the same session, on the same build, minutes after reading my vault off the screen — and I took a photo of the screen with the other hand.
This is what I read off the display, x occupied and o free:
x x x x x x
x x x x x x
x x x x x o
x x o o o o
o o o o o o
o o o o o o
This is what the game printed:
[0] Rare Macuahuitl 1x3 at (1, 3) ###...
[1] Rare Arming Sword 1x3 at (5, 2) #.##..
[2] Rare Macuahuitl 1x3 at (0, 0) #.##.#
[3] Storied Greatsword 2x4 at (2, 1) .###.#
[4] Rare Charm of Reach 1x1 at (1, 0) .#XXoX
[5] Rare Charm of Reach 1x1 at (2, 0) .#oooo
Six items and nineteen of thirty-six cells, both times. Completely different shapes.
Against the layout on my screen a war hammer has nine legal positions. Against the one in memory it has none. I pointed at a square that was empty in front of my eyes and underneath a greatsword in the model, and the grid turned red — correctly, for the arrangement it was looking at.
One line
The panel builds its tiles from GridSide.Layout(). That method was this:
public List<Placement> Layout() => GridPacking.Pack(Items, Width, Height, out _);
Pack is a first-fit, largest-first packer, recomputed on every refresh. So the tiles were a freshly packed grid. Meanwhile every drop was judged against Arrangement, the stored
positions — where things actually are.
Two grids. They agree only when the stored arrangement happens to be what the packer would have chosen, which for an empty bag is always and for a bag anyone has organised is never.
And my layout was not arbitrary. Work it through: greatsword first, 2×4, into the corner. Then the three 1×3 blades beside it at columns 2, 3 and 4. Then the two 1×1 charms, last because they are smallest, into the first free cells — column 5, rows 0 and 1. Four rows dense, two rows clear, charms stacked top-right. That is my screen, exactly. I had been looking at the output of the packer for weeks and taking it for my inventory.
The half I keep thinking about
The three stores — carry, vault, sorting table — each had a correct Layout() already. All three return the stored arrangement. The conversion from computed to stored positions had been done, and done properly.
What was missed is the adapter over them. GridSide exists so the panel does not branch six ways across three stores. It owns no data. It reads like plumbing. And it was the one thing the view actually looked through.
I do not think that is bad luck. When a decision moves — here, "positions are chosen, not computed" — the things that get converted are the ones with a domain, because those are where you go looking. The adapter has no domain, so nobody reads it as a place a decision could be hiding.
The tell was on disk the entire time. The test file written to protect the conversion opens with this:
The suite passed 1593/1595 unchanged when the three stores were switched from computed to stored layouts, which is exactly why these had to be written: nothing that existed could tell the two apart.
That is a written confession that the change is invisible to the tests, sitting in the file whose job was to make it visible. It should have been read as a list of places to go and check — what else reads a layout? — and instead it was read as a note about itself.
Fixing it broke a test, and that was the good news
One test went red: a drag fixture that had been picking "the first square other than the item's own" as somewhere to drop. That included occupied squares, so with two items in the bag it was really asking for a swap — and the swap it asked for is geometrically impossible, a 1×3 and a 2×4 side by side cannot trade places in a four-wide grid.
It had been passing because the drawn grid and the stored grid disagreed, so the lookup and the drop were addressing different layouts, and the mismatch happened to route it into a legal move.
A test held up by the bug it should have caught. I expect that is normal whenever you collapse two copies of one truth into one, and it is worth treating as a good sign rather than a regression.
What I would tell myself
Four sessions were spent on plausible mechanisms — fragmentation, a clamp, a grid edge. All three were real things in the code. None of them was the cause. The pattern is not "we were unlucky", it is that a plausible mechanism ends the investigation, and it ends it in exactly the same way whether it is right or not.
What actually worked cost one press and one photograph: make the thing print its own state, and put that next to what the player can see. The disagreement was visible in ten seconds. Everything before it was argument.
There is also a smaller thing I want on the record. When somebody is holding the object and you are holding a table, they are the one with the evidence.