feat: nines return two cards to hand, unfrozen - #1378
Open
itsalaidbacklife wants to merge 1 commit into
Open
itsalaidbacklife wants to merge 1 commit into
itsalaidbacklife wants to merge 1 commit into
Conversation
Nines are one of the highest ranks in the deck but have the most niche, least impactful one-off. This buffs the effect: a nine one-off now returns TWO cards from the opponent's board to their controller's hand, and neither returned card is frozen, so both are immediately replayable. Ships as a beta for the Spades season, announced on the home page, with a community poll toward the end of 2026 deciding whether it stays. Rules: - A nine one-off requires exactly two distinct targets, any mix of point cards, royals/glasses, and top jacks. - Both targets resolve SIMULTANEOUSLY: each card goes to the hand of whoever controlled it before the nine resolved. So targeting a jack and the point card it is stealing sends both to the jack holder's hand. - Only the top jack of a stack is targetable. - Any queen now blocks a nine outright: a queen protects everything else, leaving itself the only legal target, so two legal targets are impossible. Enforced in the validator and pre-emptively in the move-choice UI. Twos keep their existing queen rules. Notable details: - isFrozen was never persisted; it was re-derived on unpack from "resolved is a 9 and this hand card equals targetCard". That derivation is removed, so nothing freezes anymore. The freeze plumbing is left dormant rather than deleted so a poll-driven revert stays a clean git revert. - Second-target state is carried in additive nullable columns (oneOffTargetTwo, oneOffTargetTwoType, targetCardTwo) rather than widening the existing singular fields, which twos and the seven flow also read. - Nine validation is extracted to a shared helper called from both the one-off and seven-one-off validators, which previously duplicated it. - The AI move generator now enumerates two-target pairs, and its jack bodies no longer omit targetType. - Fixes a pre-existing leak: the fizzle path never cleared the one-off target fields. It was masked because the next one-off overwrote them. Selected targets get a distinct treatment (pink border/tint plus a check badge) and the green "valid target" overlay is suppressed once a card is chosen, so selection does not read as disabled. Requires a manual migration before deploy, as the repo has no migration tooling and staging/production use migrate: 'safe': ALTER TABLE gamestaterow ADD COLUMN IF NOT EXISTS "oneOffTargetTwo" text; ALTER TABLE gamestaterow ADD COLUMN IF NOT EXISTS "oneOffTargetTwoType" text; ALTER TABLE gamestaterow ADD COLUMN IF NOT EXISTS "targetCardTwo" text; Rules version bumped to 2.0.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnmfJeHNxebpfcDzSN8PqV
Contributor
Author
|
Migration has been run |
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
Nines are one of the highest ranks in the deck but have the most niche, least impactful
one-off. This buffs the effect: a nine one-off now returns two cards from the
opponent's board to their controller's hand, and neither returned card is frozen, so
both are immediately replayable.
Ships as a beta for the Spades season, announced on the home page, with a community
poll toward the end of the year deciding whether it stays. Rules version bumped to
2.0.0.The new rule
royals/glasses eights, and top jacks.
controlled it before the nine resolved. So targeting a jack and the point card it is
stealing sends both to the jack holder's hand — the point card does not first revert to
its original owner.
has, leaving itself the only legal target, so two legal targets are impossible. Enforced
in the validator and pre-emptively in the move-choice card. Twos keep their existing
queen rules (blocked only by 2+).
The repo has no migration tooling and staging/production use
migrate: 'safe', so thesenullable columns must be added before this deploys:
All nullable, so legacy rows and in-flight games keep loading. Worth confirming the table
name against the deployed schema first.
Notable implementation details
isFrozenwas never persisted. It was re-derived on unpack from "resolved is a 9 andthis hand card equals
targetCard" (unpack-gamestate.js). Removing the freeze thereforeneeded that derivation deleted too, not just the
isFrozen: truewrites — otherwise the DBround-trip re-froze target #1. Once both are gone, nothing in the game can freeze a card.
The freeze plumbing is left dormant, not deleted. Since a poll may revert this, the
playedCard.isFrozenguards, the snowflake UI, and the related i18n stay in place so therevert is a clean
git revert. If the poll keeps the change, a follow-up should removethem — worth an issue at merge time.
Additive columns rather than widening existing fields.
oneOffTarget/oneOffTargetTypeare read by twos, the seven flow, the log, the socket payload, and thestore; changing their type would be a breaking read for every existing row.
Nine validation is now shared.
one-off/validate.jsandseven-one-off/validate.jspreviously duplicated the whole
case 2: case 9:block. Extracted toapi/helpers/game-states/validate-nine-targets.jsand called from both.AI updated.
get-move-bodies-for-move-type.jsnow enumerates two-target pairs fornines (skipping generation entirely when blocked), so the bot stays a real opponent for the
trial. Also fixes a pre-existing bug where its jack bodies omitted
targetType.Fixes a pre-existing leak. The fizzle path in
resolve/execute.jsnever cleared theone-off target fields. It was masked because the next one-off overwrote them; a new
assertion in
targeting_cleanup.spec.jsexposed it. Now all four target slots clear onfizzle.
Selection UX
Targets are picked on the board and sent with a Confirm button, styled after
BaseDialog's activator fab. Rank 2 still fires on the first click.Selected cards get a pink border/tint plus a check badge, and the green "valid target"
overlay is suppressed once a card is selected — otherwise the overlay renders on top of
the selected styling and the card reads as disabled rather than chosen. Clicking a
chosen target deselects it; a third click is ignored. Three-way read: plain (not
targetable), green (targetable), pink + badge (selected).
Please describe additional details for testing this change
Local results:
npm run lintnpm run test:unitone-offs/**(12 specs)cypress runhome.spec.jswith the new announcement liveWorth exercising by hand:
holder's hand.
"You can't play a Nine while your opponent has a Queen".
validation errors in the log.
🤖 Generated with Claude Code
https://claude.ai/code/session_01JnmfJeHNxebpfcDzSN8PqV