Skip to content
alzy.kr
Devlog

I pulled a good effect a week after shipping it — and wrote the ban into the types

Every daily run in Tanubi carries one twist for that day. I removed one of them, Dusk, exactly a week after I put it in. Not because it was broken. Dusk obeyed every rule I had written for that system: a twist may only be derived from the run's seed, it may not touch terrain or physics, and it may not adjust scoring. It obeyed all three and I took it out anyway. This entry is about deciding to remove a working feature purely on the grounds that it was too much for where it sat, and about where I chose to record that decision. I did not record it in a comment. I recorded it in the type. What the daily rotation looks like today, and what each twist actually does, is laid out in the strategy guide; here I only want to talk about why one of them left.

Written: 2026-07-31

Dusk was not a mistake

The twist system itself had gone in a week earlier, under the idea of making the daily run feel like a small daily ritual: a reset countdown, a streak nudge, and a modifier that swaps out the day's atmosphere without touching anything underneath. Dusk was in that first line-up.

It cleared every constraint I had set. Dusk only touches rendering, so the shape of the board and its collisions are untouched, which keeps intact the guarantee that a generated board is always solvable. It is derived from the seed, so everyone running that day draws the same thing wherever they are. There is no score adjustment attached. Not one of the conditions I had written down for twists was violated by it.

It also felt good to build. The badge has its own emoji and name, with a one-line description in each of the three languages — in English, "Your view narrows". More than any of the others, drawing it told you immediately that today was different. That is also precisely what later made it the one to remove. My checklist passed it. The thing that turned out to be inadequate was the checklist.

It was only in the wrong place on the daily

The reason I left in the source is one short line: restricting visibility is too much for a daily. I labelled it explicitly as an owner judgement, because it is not a conclusion drawn from measurement. I did not count how people played on Dusk days, or whether more of them stopped partway. The verification recorded with that change runs to type checking, tests, a build and hands-on checks of the login work; nothing anywhere in it measures how play changed before and after Dusk.

So what was it based on? The difference in nature between the daily and the campaign. Campaign stages are laid out by number, so you know in advance what is coming, and you can rerun the same stage as often as you like. The daily is the inverse: one board, built from that day's seed, that everybody meets for the first time. A modifier that removes information weighs the most exactly where information is scarcest. That was my reading of it.

The same line explains the difference from the other twists. Floaty changes a rule, and a rule can be learned and played around. Mist thins what you can see but does not take away the fact of seeing. Dusk is the only one that subtracts the seeing itself. On a board you have already memorised, cutting your view turns into a memory test; on a board you are meeting for the first time, it leaves behind a difficulty there is no way to learn. That is how I saw it. And what I had assumed the daily to be was a habit — one run, no warm-up, then done. Against that assumption, having one face that demands noticeably more care than the rest sits awkwardly. That is a judgement too, not a number.

Removing is not deleting

Having decided to remove it, I still had to decide how far to remove it. What I settled on was: out of the daily rotation completely, and almost nothing else deleted. The campaign's Dusk event was not touched by a single line in this change. The instruction that darkens a stage is a single flag shared by the campaign and the daily, and I kept that too. I kept it and simply made sure the daily side can never raise it again.

I did it that way because the verdict "too much" was aimed at the placement, not at the thing itself. If the judgement is about placement, then placement is what should change, not the work. The campaign, where the schedule is announced by stage number and any stage can be practised, is a context in which the first-sight condition does not arise at all. Dusk is not too much there.

There is a second reason, about the price of being wrong. The darkening flag and the badge name are still in use on the campaign side, so there was no reason to delete them, and the description sits in the same entry as the name, so it stayed along with them. If in six months I change my mind and want Dusk back in the daily, they are all still sitting there, and putting it back is a few lines. Had I deleted everything, it would be a rebuild. Given that my reasoning here was a reading rather than a measurement, keeping the cost of a wrong reading low belonged among the conditions of the change.

Instead of writing "do not use this", I made it unusable

There were two places to record the ban. One: leave Dusk in the list of twist kinds, take it out of the draw table only, and add a note above saying not to use it in the daily. Two: take Dusk out of the kind itself.

I took the second. The build went red immediately and pointed at something I had missed: inside the function that applies a twist to a board, the branch that darkens the stage for Dusk was still there. Under the note-in-a-comment approach that line would have sat where it was for a long time — not because anybody would have got it wrong, but because nobody would have gone to look.

What happened next was the more interesting part. The dictionary holding the badge names and emoji is keyed by that same kind. Once Dusk left the kind, keeping a Dusk entry in the dictionary no longer type-checked. The only way to keep it was to widen the key explicitly to "the four daily kinds, plus Dusk". In other words the compiler made me choose between deleting the exception and stating it out loud — and when I chose to state it, I left the reason in the same place: the campaign's event display reuses this dictionary as-is, so the vocabulary stays consistent. The drawing code does exactly that, pulling the emoji and the name for a darkened stage out of the Dusk entry.

A ban written in a comment is a request aimed at the future; a ban written in a type is a condition the build re-checks every time. Requests age, conditions do not. I do want to be precise about what the type actually guarantees, though. Dusk has not disappeared from the game: the darkening flag still exists and the campaign still raises it. What the type can assert is narrower than that — the daily draw cannot produce Dusk, and code on the daily path cannot even branch on it. That is the whole of the claim.

What I kept as a checklist for removing things

I added two tests. One checks that the draw never returns Dusk across five thousand seeds plus a few hundred more derived from calendar dates. The other checks that the remaining kinds come out without a lean. I want to be careful with the wording here: that check did not try every possible seed, it sampled. I wrote as much in the test file itself — it is a regression guard that re-confirms at runtime what the type already guarantees. The guarantee lives in the type; the tests are there to notice if it ever quietly comes undone.

The removal itself turned out to be startlingly small. The draw table used to be a ten-slot array holding five kinds twice each. The comment above it talked about weighting, but since every kind appeared the same number of times, nothing was actually weighted. So dropping one kind needed no rebalancing at all — it became a flat four-slot array and that was the end of it. The variable is still named as a weights table. I note it here as a small example of a name outliving the thing it described.

Folded up, the change left me with four habits. If it is not broken, change the question from "is this any good?" to "is this in the right place?". When you take something out, take it out in a shape you can reverse if you were wrong. Hand the decision to the machine rather than to your own memory — into a type rather than a warning note, into a test rather than an intention. And if you keep an exception, write the reason where the exception lives.

One last honest note. This was the smallest item in that day's commit. The main body of the work was redrawing the line for how much of the game is playable without logging in, and the removal of Dusk hangs off the end of it as a single paragraph. Items that small are exactly the ones that ship without any explanation, and an effect that was there last week quietly not showing up is close to indistinguishable, from the outside, from a bug. I have no number that says the daily got better; I never counted. What I have is a record of what I removed, why, and where I wrote the ban down — which is the reason this page exists.

Tanubi