Repository navigation
feat: rework fours to reveal three cards, caster chooses two to discard - #1443
Open
itsalaidbacklife wants to merge 3 commits into
Open
itsalaidbacklife wants to merge 3 commits into
itsalaidbacklife wants to merge 3 commits into
Conversation
The second of the two Four variations trialled this Spades season. A four one-off now makes the opponent reveal three cards of their choice from hand, and the four's player chooses two of those for them to discard. Replaces the 3.0.0 random discard. Rules version bumped to 4.0.0. Rules: - 4+ cards in hand: the victim reveals three (RESOLVING_FOUR_REVEAL), then the caster chooses two of them (RESOLVING_FOUR_DISCARD). - Exactly 3 cards: the reveal is skipped; the whole hand is revealed. - 2 or fewer: discarded outright as the four resolves. A four stays playable against any hand, including an empty one. Implementation: - New GamePhases RESOLVING_FOUR_REVEAL (8) and RESOLVING_FOUR_DISCARD (9), and MoveTypes revealFour / discardFour with validate/execute helpers, policy arms, AI move-body generation, active-player cases, and log lines. - Revealed cards persist between the two moves in a new nullable jsonb `revealedCards` column on GameStateRow, threaded through pack/unpack, validate-gamestate, deal-cards, load-fixture-gamestate, and the socket game payload (sent to both players, outside opponent-hand hiding). - get-log keeps the "at random" wording for rules 3.x games only. - Legacy RESOLVING_FOUR / resolveFour replay paths are retained untouched. - New reveal and choose-discard dialogs, waiting overlays (the victim sees their revealed cards), store getters/actions, and socket handling. - Announcement copy, rules text, and move-effect text updated in all locales, under a new announcement id so it shows again. Deploy: production and staging use migrate: 'safe', so before deploying run ALTER TABLE gamestaterow ADD COLUMN IF NOT EXISTS "revealedCards" jsonb; Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A four resolved against an empty hand carries no discardedCards, so it was mistaken for a legacy (rules < 3.0.0) row and logged "must discard two cards". Identify legacy rows by their RESOLVING_FOUR phase instead, which every pre-3.0.0 four resolve entered and no later one does, and give the empty hand its own line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
create-socket-events flips playedBy on a resolve to the player who played the one-off, but the RESOLVE case 4 handler assumed it was the resolver. So the victim saw their own discards flash in the caster's hand, and the caster never saw the reveal. Invert the check, correct the comment, and cover both sides in 4_fours.spec.js. Fixes #1440 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue number
Relevant issue number
Please check the following
What this does
Replaces the 3.0.0 random-discard Four with Reveal-three: the opponent reveals three cards of their choice from hand, and the Four's player chooses two of those for them to discard. Rules version bumped to 4.0.0, and the Spades-season beta announcement now describes this variation.
RESOLVING_FOUR_REVEAL), then the caster picks 2 of them (RESOLVING_FOUR_DISCARD)A Four stays playable against any hand, including an empty one. Playing a 4 for points or as a scuttle is unchanged.
Production and staging use
migrate: 'safe', so the new column must be added by hand before deploying:Implementation
Two new phases and moves, following the
resolveFivepattern for a player-input step:GamePhase.RESOLVING_FOUR_REVEAL: 8. The victim is active,(turn + 1) % 2. The only legal move isrevealFour { revealedCards: [3 ids] }.GamePhase.RESOLVING_FOUR_DISCARD: 9. The caster is active,turn % 2. The only legal move isdiscardFour { discardedCards: [2 ids] }. This completes the Four: it scraps the one-off, runs the hand-limit check and advances the turn.resolve/four.jspicks the branch by hand size.resolve/execute.jsreturns early when the Four is now waiting on either new phase, the same way it already does for 3/5/7.hasValidMoveBody,getActivePlayerPNum, AI move-body generation,get-log,MoveTypeandSocketEvent.getCombinationsin the AI generator is hoisted to module scope so both moves can use it.Storage: the revealed cards stay in the victim's hand and are recorded in a new
revealedCardscolumn (nullable jsonb, default[]). It's threaded through the model, pack/unpack, thevalidate-gamestatewhitelist,deal-cardsandload-fixture-gamestate, which gains arevealedCardsoverride for staging phase 9. The socket game payload sends it to both players, outsideplayers[].hand, since both players know those cards. It is deliberately left out ofvalidate-all-cards, because the cards are still in the hand and would trip the duplicate check.Backward compatibility: everything #1384 retained for pre-3.0.0 replays (
RESOLVING_FOUR,resolveFour,FourDialog) is untouched.get-logkeeps the "at random" wording only for rules 3.x games, which it checks withgame.rulesVersion. 4.0.0 games that discard a short hand on resolution are narrated without it. Legacy resolve rows are identified by theirRESOLVING_FOURphase (every pre-3.0.0 four resolve entered it), so a Four against an empty hand is no longer mislogged as "must discard two cards"; it now reads "…has no cards to discard."Fixes #1440:
create-socket-eventssends the one-off's player asplayedByon a resolve, but the RESOLVEcase 4socket handler (from #1384) treated it as the resolver. So when a hand was discarded on resolution, the victim saw their own discards flash in the caster's hand, and the caster never saw them. The check is inverted and the comment corrected.4_fours.spec.jsnow asserts the caster sees the reveal and spies onprocessFoursto assert the victim never runs it; both assertions fail with the old check.Frontend:
<script setup>dialogsFourRevealDialogandFourDiscardDialog, modelled onDiscardToHandLimitDialog.inGameEventscases;discardFouranimates the discards out of the caster's view of the hidden hand.Copy: new dialog, overlay and snackbar keys, plus updated move-effect text, rules page text and announcement, in all five locales with identical key sets. The announcement id changed so the popup shows again.
Please describe additional details for testing this change
npm run lintnpm run buildnpm run test:unit4_fours.spec.jsplayer_sevens.spec.jsreconnecting.spec.jshandLimit.spec.jslayout.spec.jswinConditions.spec.jsfour.spec.jscovers the resolve branches by hand size, the reveal and discard validators, the active player and the log text.4_fours.spec.jsis rewritten. It covers each hand size from both perspectives, glasses, a staged phase-9 state, and the legacy phase-4 replay.Worth exercising by hand:
Coordination: the unmerged
feat/nines-discard-twobranch also claims rules 4.0.0, so whichever merges second renumbers. If nines lands first and this becomes 5.0.0, games played under 4.x would still have random Fours. Theget-logcheck (parseInt(game.rulesVersion, 10) === 3) would then need to cover majors 3 and 4.🤖 Generated with Claude Code