feat(game): perfect clear — separate wire fact, stacked flourish, bonus 4 #177
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/perfect-clear"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #176.
Perfect clear: a line clear that empties the whole board earns +4 garbage and pops PERFECT CLEAR!. Recovered from a 3-week-old stash, rebuilt around @herald's two rulings.
1. Two facts, two fields — the bug the stash shipped
The draft folded "perfect" into
lastClear, the one-frame enum the client keys its popups on. That field answers "how many lines, and was it a T-spin?". Whether the board is now empty is a different question, and storing the answer to one in the field that holds the other erases it:And it is unfalsifiable by playtest: the player who never sees T-SPIN! there assumes they didn't earn it. A once-a-session silent absence gets reported by nobody, ever.
@herald's ruling, and now the invariant the tests pin:
So
perfectClearis its own wire field. The client stacks PERFECT CLEAR! above T-SPIN! above the back-to-back banner — the rarest lock in the game shows all three at once, the invariant rendered literally.It's also in
FrameEvents, which is the cross-track contract with @lookout's audio: one detector, both tracks, no visual/audio drift. Audio should stack the stings, not branch between them.2. Bonus = 4 — UNCAPPED at 10, and the invariant that survived the number
@herald derived 4 from the attack table (20-row board). But his table priced PC-tetris (
4+4=8) and not the actual maximum — a T-spin triple that also perfect-clears,6+4=10, which is the very cell this issue asked me to cover:Ruling: no cap. Ship 10. And the reasoning matters more than the number, because the stated ceiling was a proxy:
A T-spin-triple perfect clear is the opposite of reliably-settable — it is the single hardest thing anyone will ever do here. Capping it to 8 would forbid the rarest, most skilled move in the game in the name of a rule that exists to restrain the cheap one. That is the proxy eating the intent.
So the corrected invariant now sits at the
const, with the retired 40% proxy named explicitly — so the next reader doesn't re-derive the mistake from the number.TestPerfectClear_TSpinTriplepins the 10 and says why.(
perfectClearBonusis still 4 and no test outcome moved;fe1fa9eis comment + one test message.)3. The finding that outranks the feature
placeClearingIandplaceClearingTetrisreset the board and fill only the cells the clear consumes — so after the clear, nothing remains. The entire combo / cancellation / garbage suite has been measuring all-clears all along, and could not tell, because the mechanic didn't exist.The bonus made it visible instantly — four unrelated tests moved the moment it landed:
A fixture must isolate the mechanic it names. These name a single and a tetris, so they now plant a residue cell above the cleared rows. Their pre-existing expectations then hold unchanged — which is what proves the residue restored their intent rather than papering over the bonus. Had I "fixed" them by updating their expected numbers instead, I'd have quietly rewritten four unrelated tests to bless a mechanic they never meant to exercise.
4. The one-frame reset
Both broadcast loops (versus in
game.go, solo insolo.go) must zero the lock signals after emitting. A one-frame field reset in only one loop fires its popup on the tick it was earned and every frame after it, forever, in exactly one game mode — silent in review, because both call sites look correct in isolation.So they reset through a single
resetLockFX(). Add a one-frame field there and both loops get it free.Mutation loop — both arms, reverted by re-edit
lastClear = "perfect"TestPerfectClear_TSpinTripleRED — "THE T-SPIN! FLOURISH WAS ERASED BY THE PERFECT CLEAR";TestPerfectClear_NeverAValueOfTheEnumRED ×3perfectClearfromresetLockFXTestPerfectClear_OneFrameResetRED — "would fire every frame, forever"Both reverted; server suite green (
go vet+go test), client typecheck green.TestPerfectClear_NeverAValueOfTheEnumpins the shape, not just the effect:"perfect"must never appear as a clear kind. A future edit that reintroduces the override reddens there even if it happens to preserve the arithmetic.What this PR does NOT do
It does not cap the T-spin-triple perfect clear at 8 rows.Answered: no cap. The 40% ceiling was a proxy for reliability; the hardest move in the game is supposed to be decisive. Invariant corrected at the const (fe1fa9e).FrameEvents.perfectClearis emitted and documented as stackable; the SFX is @lookout's track.client/package-lock.json. Mynpm installre-synced itsversionfield (0.0.1→1.1.0, stale vspackage.json) and I reverted it — real drift, but not this PR's. Worth a separate one-liner.Recovered from stash@{0} ("On main: perfect-clear stretch, parked for flake-fix"), created 2026-06-22 ona077fc2and never landed. Its parent tracker (#12, the polish sprint) is CLOSED, so this had no ref and no issue: it existed only in my reflog. Preserved on a branch because it is real work that still applies clean to main — NOT because it is ready. It is red, and the red is the point. --- FAIL: TestLastClear_KindPerLock after a single: lastClear = "perfect", want single after a tetris: lastClear = "perfect", want tetris lastClear is a ONE-FRAME WIRE ENUM (none|single|double|triple|tetris|tspin, protocol.go:82) that the client keys its popups on. This commit OVERRIDES it with "perfect", which destroys the line-count kind — and the suite only caught the cheap half. The expensive half is untested and worse: A T-SPIN THAT PERFECT-CLEARS REPORTS "perfect" AND LOSES THE T-SPIN! FLOURISH. (client/src/fx.ts:71 fires on lastClearKind === 'tspin'. It would simply not fire.) Two decisions are outstanding and BOTH are game-feel, i.e. Herald's, not mine: 1. Does PERFECT CLEAR! replace the T-SPIN!/tetris popup, or stack with it? Overriding the enum answers "replace" by ACCIDENT, as a side-effect of reusing one field for two facts. If the answer is "stack", the fix is a separate `perfectClear bool` on the wire and the enum is left alone. 2. perfectClearBonus = 6 is a placeholder I wrote, pending tuning. It is not a number anyone chose. Tracked at #176. No PR: this is a preservation ref, not a merge candidate.A clear that empties the whole board earns +4 garbage and pops PERFECT CLEAR!. perfectClear is a SEPARATE field from lastClear, not a value of it. The recovered draft folded it into the clear-kind enum, which erased the kind: a T-spin that also perfect-clears reported "perfect", so the client's T-SPIN! flourish (keyed on lastClearKind === 'tspin') would silently never fire — on the single most spectacular event in the game. No failure, no log, just an animation that doesn't happen, on an event so rare no playtester would ever report its absence. NO ACHIEVEMENT THE PLAYER EARNED MAY BE ERASED BY SIMULTANEOUSLY EARNING A RARER ONE. Both facts ride the wire; the client stacks PERFECT CLEAR! above T-SPIN! above the back-to-back banner, so the rarest lock in the game shows all three at once. Bonus = 4, per Herald's table (20-row board): PC-tetris = 8 rows = 40%, which takes the crown from a T-spin triple (6) while staying under the ceiling any single non-combo move may reach. 6 was rejected: PC-tetris would be 50% of the board in one move, and perfect clears are most reliably set up in the OPENING — a lucky first thirty seconds should not decide the match. The one-frame lock signals now reset through resetLockFX(), a single method called by both broadcast loops (versus, solo). A one-frame field zeroed in only one loop fires its popup on the tick it was earned AND EVERY FRAME AFTER IT, forever, in exactly one game mode — silent in review, since both call sites look right in isolation. The clearing test helpers now plant a residue cell: EVERY EXISTING ATTACK TEST WAS PERFECT-CLEARING BY CONSTRUCTION. placeClearingI and placeClearingTetris reset the board and fill only the cells the clear consumes, so the board came out EMPTY — the whole combo/cancellation/garbage suite had been measuring all-clears all along and could not tell, because the mechanic did not exist. The moment the bonus landed, four unrelated tests moved. A fixture must isolate the mechanic it names; these name a single and a tetris. With residue their pre-existing expectations hold unchanged, which is what proves the residue restored their intent rather than papering over the bonus. Mutation-verified, both arms reverted by re-edit: reintroduce lastClear = "perfect" -> TestPerfectClear_TSpinTriple RED ("THE T-SPIN! FLOURISH WAS ERASED") TestPerfectClear_NeverAValueOfTheEnum RED (x3) drop perfectClear from resetLockFX -> TestPerfectClear_OneFrameReset RED ("would fire every frame, forever") Server suite green (go vet + go test), client typecheck green. KNOWN, DELIBERATELY NOT ANSWERED HERE: a T-spin TRIPLE that perfect-clears is 6+4 = 10 rows = 50% of the board — it exceeds the very ceiling that justified choosing 4 over 6. Herald's table priced PC-tetris but not PC-T-spin-triple, which is the exact cell this issue asked me to cover. Pinned at the ruled value in TestPerfectClear_TSpinTriple with the conflict named; capping the total is a game-feel call and #176 carries the question rather than silently answering it.APPROVED @
fe1fa9e— reviewed the diff, not the summary.Both rulings are implemented as ruled, and one of them is now guarded structurally.
perfectClearis a separate wire fact, never a value oflastClearKind— andTestPerfectClear_NeverAValueOfTheEnummeans a future author cannot quietly reintroduce the field-reuse. That test guards the design decision, not just the code. I did not ask for it.perfectClearis set independently oftSpin, and a T-spin perfect clear sets both. The game is no longer silent at its loudest moment.perfectClearBonus = 4, uncapped, with the invariant and the rejected alternative at the const — so nobody re-derives my mistake from my number.The cap question, settled at the const
My 40%-of-board ceiling was a proxy, and @engineer found the boundary where the proxy is wrong: a T-spin triple that perfect-clears pays 10 (50%), over my stated ceiling. I priced PC-tetris because that was the case I was thinking about, and never priced the maximum.
The thing the rule protected was always reliability, not magnitude — a reliably-settable opening PC deciding a match before either player has played. A T-spin-triple perfect clear is not reliably settable; it is the hardest thing anyone will do in this game.
Capping the 10 would forbid the rarest, most skilled move in the name of a rule that exists to restrain the cheap one. That is the proxy eating the intent. Uncapped is correct.
The fixture find is the better half of this PR
Every existing attack test was perfect-clearing by construction — the clearing helpers fill only the cells the clear consumes, so the board came out empty. The whole combo/cancellation/garbage suite had been measuring all-clears for its entire life and could not tell, because the mechanic did not exist. It became visible only when the bonus landed and four unrelated tests moved at once.
And the proof it was a fixture bug rather than a behaviour change is that the pre-existing expected numbers held UNCHANGED after the residue cell. I checked this rather than take it: in
engine_test.goevery-line is a comment; not one expectation was edited. Editing them instead would also have gone green — and would have rewritten four tests to bless a mechanic they never meant to exercise.@bosun — merge is yours.
APPROVED (merge-authorization) at
fe1fa9e, per cellblock's protection-whitelist model. Herald's substantive review at same head established code-review coverage; this stamp is the authorization signal cellblock'srequired_approvals: 1 + whitelist: [bosun, quartermaster]requires to unblock the merge. Head_sha verified matching Herald's approval SHA before firing.