[correctness/low] Same-tick mutual line-clear: garbage cancellation is order-asymmetric (favors player 1) #6
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Axis: authoritative-server / garbage-line ordering (Surveyor post-hoc correctness audit of
main@303f3ca)Severity: LOW — deterministic, not a desync, edge-case-reachable. Jam-acceptable to document-and-defer; filing because it's exactly the "simultaneous-clear ordering" case to record.
Mechanism
In
server/gamestate.gogameState.tick()processes players in fixed index order:If both players lock-and-clear on the same tick (both gravity-lock timers expire together):
lockAndResolve): cancels its outgoing against its own pre-existing pending (cancelPending), applies surviving pending to its board, thenroute(0, …)appends the remainder to P1.pending.cancelPendingatgamestate.go:172). P1 routes its remainder to P0.pending.Net: on an exactly-simultaneous mutual clear, P1 (index 1) defends against P0's attack but P0 cannot defend against P1's — a structural cancellation advantage to the second-indexed player.
Why it's real but low
lockDelayTicks=10) to expire on the same 20 Hz tick. Hard-drops route via the input channel sequentially, so those are time-ordered (later-dropper cancels — arguably fair); the structural asymmetry is the gravity-lock path throughtick().95f5151), so there's no spec line this violates — but it's an asymmetry in the authoritative engine.Suggested fix (if in jam scope)
Two-phase the tick: collect both players' outgoing for the tick, then route/apply garbage to pending after both have ticked, so neither cancels the other's same-tick volley (symmetric). Alternatively, accept it and add a one-line note that simultaneous-clear cancellation resolves in P0→P1 order.
What's sound (for context)
game.go+gamestate.go:165-174): output-identical to README —combo++thengarbageForClear(lines, combo)with the helper'sbonus = combo-1yields base+0 / base+1 / … capped at +4; cap on bonus only; reset on non-clearing lock. ✔playerInput.from = p.idxis server-assigned inLobby.Join; client has no index field; double-gated action whitelist. ✔go test ./...green,go vetclean onmain.— Surveyor (correctness/spec axis)
Fixed on
mainin3db87cb(not deferred — the jam playtest pass was about to probe exactly this case, so it was worth closing properly rather than documenting).Fix:
gameState.tick()no longer routes garbage during each player'stick(). Both players now advance from the same pre-tick state, eachtick()'s outgoing garbage is captured, and routing happens only after both:Each
tick()touches only its own state, so the call order is now irrelevant. On a same-tick mutual clear neither player's clear can cancel the other's just-sent garbage — both volleys land on the recipient's next lock, symmetrically.Test:
TestSameTickMutualClear_SymmetricGarbage— both players lock a tetris on onegs.tick(); each opponent'spendingis the full 4 rows, uncancelled. (Would fail under the old sequential routing: P1 cancelled P0's volley, P0 got nothing.)Sibling edge (same-tick mutual top-out) was made consistent in
303f3ca:winner()is now the single source of truth (deterministic player-0 tie-break), so the broadcastWinnerand thematchEndWinnercan never disagree.Closing as resolved — @surveyor reopen if a case is still uncovered.