Illegal states, unrepresentable
An architecture review found both duel pages still carrying the same ninety-line derivation chain by hand. The fix collapses sixteen flat props into one discriminated model.
An architecture review looking specifically at the duel and PvP hot spots found that the panel’s ownership rule had never actually stopped being duplicated. Both /pve and /pvp still carried their own roughly ninety-line chain deriving ownership, the display word, the collision index, and the chip content, each copy carrying the same bug-fix comments referencing the same old issue numbers, because sharing the small ownership function hadn’t gone far enough to share everything downstream of it.
The fix moved the whole chain, not just the first step, into a single function that returns one discriminated model instead of handing DuelWordPanel sixteen separate flat props to reconcile on its own:
stateDiagram-v2
accTitle: WordPanelModel state transitions
accDescr: The discriminated WordPanelModel replaces sixteen flat boolean and string props with four kinds. Player input transitions to player animating on word submission, opponent draft to opponent animating on move animation, and states cycle back to player input. Each kind carries only its necessary fields, making impossible states unrepresentable.
[*] --> PlayerInput
PlayerInput --> PlayerAnimating: Word submitted
PlayerAnimating --> OpponentDraft: Animation complete
OpponentDraft --> OpponentAnimating: Move animation begins
OpponentAnimating --> PlayerInput: Animation complete
note right of PlayerInput
Player's word input state
Fields: displayWord, collisionIndex
end note
note right of PlayerAnimating
Player's word animation
Fields: displayWord, collisionIndex
end note
note right of OpponentDraft
Opponent's turn pending
Fields: displayWord
end note
note right of OpponentAnimating
Opponent's move animation
Fields: displayWord, collisionIndex
end note
class PlayerInput,PlayerAnimating,OpponentDraft,OpponentAnimating accentexport function selectWordPanelViewModel(inputs: WordPanelInputs): WordPanelModel
WordPanelModel is a union of exactly four kinds, player-input, player-animating, opponent-draft, opponent-animating, each carrying only the fields that kind actually needs. Sixteen flat props could combine into states that never made sense, a draft that was somehow also the input, an opponent panel marked used when only the player’s own word ever gets marked that way, and nothing in the type system said so. Four kinds instead of sixteen booleans-and-strings means those combinations are no longer just untested, they’re impossible to construct. One input fell out entirely along the way: whether the player’s last word had crashed used to be its own field both pages had to set correctly; it’s now derived from the collision step index the panel already tracks, which is one less thing for two call sites to keep in sync with each other.
Nothing about what a player sees changed. The two pages still render bit-for-bit the same PvE and PvP behavior, since this was a seam moving, not new behavior arriving; the screen-reader announcement regions stayed exactly where they were, in each page rather than the shared component, since PvE and PvP genuinely announce different things for reasons specific to each mode. “Word panel” earned its own entry in the project’s shared terminology alongside the change, so the next person reading the code or a future issue about it reaches for that name rather than inventing a new one for the same idea.