diff --git a/api/helpers/game-states/ai/get-move-bodies-for-move-type.js b/api/helpers/game-states/ai/get-move-bodies-for-move-type.js
index 50598698b..04b572dc4 100644
--- a/api/helpers/game-states/ai/get-move-bodies-for-move-type.js
+++ b/api/helpers/game-states/ai/get-move-bodies-for-move-type.js
@@ -85,12 +85,35 @@ module.exports = {
const twosAndNines = playerHand.filter((card) => [ 2, 9 ].includes(card.rank));
for (let twoOrNine of twosAndNines) {
for (let potentialTarget of opponentFaceCards) {
- res.push({ moveType, playedBy, cardId: twoOrNine.id, targetId: potentialTarget.id, targetType: 'faceCard' });
+ res.push({
+ moveType,
+ playedBy,
+ cardId: twoOrNine.id,
+ targetId: potentialTarget.id,
+ targetType: TargetType.faceCard,
+ });
}
for (let pointCard of opponentPoints) {
+ // Only nines can target the point card itself
+ if (twoOrNine.rank === 9) {
+ res.push({
+ moveType,
+ playedBy,
+ cardId: twoOrNine.id,
+ targetId: pointCard.id,
+ targetType: TargetType.point,
+ });
+ }
+
if (pointCard.attachments.length) {
- res.push({ moveType, playedBy, cardId: twoOrNine.id, targetId: pointCard.attachments.at(-1).id });
+ res.push({
+ moveType,
+ playedBy,
+ cardId: twoOrNine.id,
+ targetId: pointCard.attachments.at(-1).id,
+ targetType: TargetType.jack,
+ });
}
}
}
@@ -224,6 +247,17 @@ module.exports = {
}
for (let opponentPoint of opponentPoints) {
+ // Only nines can target the point card itself
+ if (targetedOneOff.rank === 9) {
+ res.push({
+ moveType,
+ playedBy,
+ cardId: targetedOneOff.id,
+ targetId: opponentPoint.id,
+ targetType: TargetType.point,
+ });
+ }
+
if (opponentPoint.attachments.length) {
res.push({
moveType,
diff --git a/api/helpers/game-states/get-log.js b/api/helpers/game-states/get-log.js
index 38d6ec6bd..5bd5134a9 100644
--- a/api/helpers/game-states/get-log.js
+++ b/api/helpers/game-states/get-log.js
@@ -108,7 +108,7 @@ module.exports = {
)} and ${getFullCardName(deck[1])}.`;
case 9:
- return `The ${resolvedCardName} one-off resolves, returning the ${targetCardName} to ${player}'s hand. It cannot be played next turn.`;
+ return `The ${resolvedCardName} one-off resolves, putting the ${targetCardName} on top of the deck.`;
}
break;
diff --git a/api/helpers/game-states/is-top-jack.js b/api/helpers/game-states/is-top-jack.js
new file mode 100644
index 000000000..dcfe29940
--- /dev/null
+++ b/api/helpers/game-states/is-top-jack.js
@@ -0,0 +1,30 @@
+module.exports = {
+ friendlyName: 'Is top jack',
+
+ description: 'Determines whether the specified jack is the top of the stack attached to the point card it is stealing',
+
+ extendedDescription:
+ 'Only the top jack of a stack may be targeted. Each jack\'s owner is derived from its position in the stack (see convert-card-to-str.js), so removing a buried jack silently reassigns every jack above it -- and with it, control of the point card.',
+
+ inputs: {
+ targetId: {
+ type: 'string',
+ description: 'String ID of the jack to check',
+ required: true,
+ example: 'JS', // Jack of Spades
+ },
+ /**
+ * @param { Player } player - the player whose points should contain the jack's host
+ * @param { Card[] } player.points - the player's point cards -- can contain attachments
+ */
+ player: {
+ type: 'ref',
+ descriptions: 'Player object from the current game state',
+ required: true,
+ },
+ },
+ sync: true,
+ fn: ({ targetId, player }, exits) => {
+ return exits.success(player.points.some(({ attachments }) => attachments.at(-1)?.id === targetId));
+ },
+};
diff --git a/api/helpers/game-states/moves/one-off/validate.js b/api/helpers/game-states/moves/one-off/validate.js
index a7dd5a129..e1cb41517 100644
--- a/api/helpers/game-states/moves/one-off/validate.js
+++ b/api/helpers/game-states/moves/one-off/validate.js
@@ -86,6 +86,16 @@ module.exports = {
throw new BadRequestError('Twos can only target royals or glasses');
}
+ // Only the top attachment can be targeted. Each jack's owner is derived from its
+ // position in the stack, so removing a buried one silently reassigns the jacks above
+ // it -- and with them, control of the point card.
+ if (
+ requestedMove.targetType === 'jack' &&
+ !sails.helpers.gameStates.isTopJack(requestedMove.targetId, opponent)
+ ) {
+ throw new BadRequestError('game.snackbar.oneOffs.onlyTopJack');
+ }
+
const queenCount = opponent.faceCards.filter((faceCard) => faceCard.rank === 12).length;
// Legal if not blocked by opponent's queen(s)
diff --git a/api/helpers/game-states/moves/resolve/execute.js b/api/helpers/game-states/moves/resolve/execute.js
index 92d7b4643..728b0eedb 100644
--- a/api/helpers/game-states/moves/resolve/execute.js
+++ b/api/helpers/game-states/moves/resolve/execute.js
@@ -61,6 +61,9 @@ module.exports = {
result.moveType = MoveType.FIZZLE;
result.scrap.push(result.oneOff);
result.oneOff = null;
+ // Nothing is pending anymore, so the target slots have to clear too
+ result.oneOffTarget = null;
+ result.oneOffTargetType = null;
result.phase = playerMustDiscard ? GamePhase.DISCARDING_TO_HAND_LIMIT : GamePhase.MAIN;
result.turn = playerMustDiscard ? result.turn : result.turn + 1;
return exits.success(result);
diff --git a/api/helpers/game-states/moves/resolve/nine.js b/api/helpers/game-states/moves/resolve/nine.js
index fd5d90412..f0296e12f 100644
--- a/api/helpers/game-states/moves/resolve/nine.js
+++ b/api/helpers/game-states/moves/resolve/nine.js
@@ -3,12 +3,12 @@ const TargetType = require('../../../../../utils/TargetType.json');
module.exports = {
friendlyName: 'Resolve Nine One-Off',
- description: 'Returns new GameState resulting from resolving an nine, which returns target card to opponents hand',
+ description: 'Returns new GameState resulting from resolving a nine, which puts the target card on top of the deck',
inputs: {
currentState: {
type: 'ref',
- description: 'The latest GameState before the nine returns target card to opponents hand',
+ description: 'The latest GameState before the nine puts the target card on top of the deck',
required: true,
},
playedBy: {
@@ -23,36 +23,47 @@ module.exports = {
// the user who played the one off is the opposite of the one who is resolving it.
const opponent = playedBy ? result.p1 : result.p0;
const player = playedBy ? result.p0 : result.p1;
-
+
+ // Cards go onto the deck face down, and the deck is drawn from the front
+ const topDeck = (card) => result.deck.unshift({ ...card, attachments: [] });
+
if ([ TargetType.point, TargetType.faceCard ].includes(result.oneOffTargetType)) {
const targetIndex = opponent[`${result.oneOffTargetType}s`].findIndex(({ id }) => id === result.oneOffTarget.id);
+ // Sanity check: targets can't disappear mid-resolution, but no-op rather than splice(-1)
+ if (targetIndex < 0) {
+ return exits.success(result);
+ }
const [ targetCard ] = opponent[`${result.oneOffTargetType}s`].splice(targetIndex, 1);
-
- opponent.hand.push({ ...targetCard, isFrozen: true, attachments: [] });
-
+
// scrap all attachments
result.scrap.push(...targetCard.attachments);
+ topDeck(targetCard);
+
return exits.success(result);
- }
-
+ }
+
// TargetType = jack
// Find card that the jack is attached to
const targetIndex = opponent.points.findIndex(({ attachments }) =>
attachments.some(({ id }) => id === result.oneOffTarget.id));
+ if (targetIndex < 0) {
+ return exits.success(result);
+ }
const [ targetCard ] = opponent.points.splice(targetIndex, 1);
-
- // remove last attachment
- targetCard.attachments.pop();
+
+ // Both one-off validators reject any jack target that isn't the top of its stack,
+ // so the targeted jack is the last attachment
+ const jack = targetCard.attachments.pop();
// Card the jack was stealing goes back to other player
player.points.push(
{
...targetCard,
});
-
- // Push jack back to opponents hand
- opponent.hand.push({ ...result.oneOffTarget, isFrozen: true, attachments: [] });
+
+ // Put the jack on top of the deck
+ topDeck(jack);
return exits.success(result);
},
};
diff --git a/api/helpers/game-states/moves/seven-one-off/validate.js b/api/helpers/game-states/moves/seven-one-off/validate.js
index fef0899b9..219b3006e 100644
--- a/api/helpers/game-states/moves/seven-one-off/validate.js
+++ b/api/helpers/game-states/moves/seven-one-off/validate.js
@@ -76,6 +76,14 @@ module.exports = {
throw new BadRequestError('Twos can only target royals or glasses');
}
+ // Only the top attachment can be targeted
+ if (
+ requestedMove.targetType === 'jack' &&
+ !sails.helpers.gameStates.isTopJack(requestedMove.targetId, opponent)
+ ) {
+ throw new BadRequestError('game.snackbar.oneOffs.onlyTopJack');
+ }
+
const queenCount = opponent.faceCards.filter((faceCard) => faceCard.rank === 12).length;
// Legal if not blocked by opponent's queen(s)
diff --git a/api/helpers/game-states/unpack-gamestate.js b/api/helpers/game-states/unpack-gamestate.js
index ec4288a0a..f92d5ea21 100644
--- a/api/helpers/game-states/unpack-gamestate.js
+++ b/api/helpers/game-states/unpack-gamestate.js
@@ -39,14 +39,7 @@ module.exports = {
if (value) {
const { convertStrToCard } = sails.helpers.gameStates;
convertedData[attribute] = Array.isArray(value)
- ? value.map((cardStr) => {
- // Is the card frozen
- const freezeCard =
- !!gameStateRow.resolved &&
- gameStateRow.resolved[0] === '9' &&
- gameStateRow.targetCard === cardStr.substring(0, 3);
- return convertStrToCard(cardStr, freezeCard);
- })
+ ? value.map((cardStr) => convertStrToCard(cardStr))
: convertStrToCard(value);
}
});
diff --git a/docs/game-rules.md b/docs/game-rules.md
index 948195e4d..ca77309b3 100644
--- a/docs/game-rules.md
+++ b/docs/game-rules.md
@@ -48,7 +48,7 @@ On your turn you must make exactly one of the following actions below.
- **Five**: Discard one card then draw up to three cards from the **Deck** up to the 8 card hand limit.
- **Six**: Scrap all **Royals and Glasses Eights**, including your own.
- **Seven**: Reveal the top two cards from the **Deck** and choose one of them to play immediately. Play this card however you would like. The other card is placed back on top of the deck.
-- **Nine**: Return an opponent's card on the field to their hand. It can't be played next turn.
+- **Nine**: Put an opponent's card on the field on top of the deck. Only the top Jack of a stack may be targeted.
## Royals & Glasses Eights
diff --git a/docs/game-state-api.md b/docs/game-state-api.md
index 2705c15ed..26cb0efe5 100644
--- a/docs/game-state-api.md
+++ b/docs/game-state-api.md
@@ -155,7 +155,7 @@ Represents a single card, in its object format.
* id: `String` (same format as the gameStateRow cards e.g. ‘AS’ for Ace of Spades)
* suit: `0 | 1 | 2 | 3`
* rank: `[1- 13]`
-* isFrozen: `Boolean` \- whether the card is currently frozen due to a 9 and can’t be played this turn
+* isFrozen: `Boolean` \- whether the card is currently frozen and can’t be played this turn. No effect currently freezes a card; retained pending the outcome of the topdeck-nines beta
## Player
diff --git a/src/plugins/sockets/inGameEvents.js b/src/plugins/sockets/inGameEvents.js
index e77eee648..52f126805 100644
--- a/src/plugins/sockets/inGameEvents.js
+++ b/src/plugins/sockets/inGameEvents.js
@@ -62,10 +62,15 @@ export async function handleInGameEvents(evData, newRoute = null) {
gameStore.updateGame(evData.game);
break;
case SocketEvent.RESOLVE:
- if (evData.game.lastEvent?.oneOff?.rank === 7) {
- await gameStore.processSevens(evData.game);
- } else {
- gameStore.updateGame(evData.game);
+ switch (evData.game.lastEvent?.oneOff?.rank) {
+ case 7:
+ await gameStore.processSevens(evData.game);
+ break;
+ case 9:
+ await gameStore.processNines(evData.targetCard, evData.game);
+ break;
+ default:
+ gameStore.updateGame(evData.game);
}
break;
case SocketEvent.SCUTTLE:
diff --git a/src/routes/game/GameView.vue b/src/routes/game/GameView.vue
index 879029e4e..f9d4ae598 100644
--- a/src/routes/game/GameView.vue
+++ b/src/routes/game/GameView.vue
@@ -222,35 +222,35 @@
class="field-point-container"
>
-
+
-
+
-
+
-
+
@@ -466,8 +466,6 @@ export default {
targeting: false,
targetingMoveName: null,
targetingMoveDisplayName: null,
- targetType: null,
- nineTargetIndex: null,
showFourDialog: false,
topCardIsSelected: false,
secondCardIsSelected: false,
@@ -544,7 +542,18 @@ export default {
///////////////////////////
// Transition Directions //
///////////////////////////
+ /**
+ * True for the one list a topdecked card is leaving from. Every other list keeps its
+ * usual direction, so a topdecked jack can head for the deck while the point card it was
+ * stealing goes back to its owner in the same render.
+ */
+ topdeckZone() {
+ return this.gameStore.topdeckedCardZone;
+ },
playerPointsTransition() {
+ if (this.topdeckZone === 'playerPoints') {
+ return this.toDeckTransition('player', 'points');
+ }
switch (this.gameStore.lastEventChange) {
case 'resolve':
// Different one-offs cause points to move in different directions
@@ -553,18 +562,10 @@ export default {
case 2:
case 6:
return Transitions.SLIDE_UP;
- // For nines, transition direction depends on target type
+ // Nines put the target on the deck; the only point card entering here is a
+ // jacked card whose control reverts when its jack is topdecked
case 9:
- switch (this.gameStore.lastEventTargetType) {
- // Nine on jack causes points to swap control
- case 'jack':
- return Transitions.SLIDE_UP;
- // Everything else expect cards to move back to hand
- case 'point':
- case 'faceCard':
- default:
- return Transitions.SLIDE_DOWN;
- }
+ return Transitions.SLIDE_UP;
default:
return Transitions.SLIDE_DOWN_LEFT;
}
@@ -578,15 +579,9 @@ export default {
}
},
playerFaceCardsTransition() {
- // If a face card is bounced by a nine, slide down to player hand
- if (
- this.gameStore.lastEventChange === 'resolve' &&
- this.gameStore.lastEventOneOffRank === 9 &&
- this.gameStore.lastEventTargetType === 'faceCard'
- ) {
- return Transitions.SLIDE_DOWN;
+ if (this.topdeckZone === 'playerFaceCards') {
+ return this.toDeckTransition('player', 'faceCards');
}
-
// Playing from the deck
if (this.gameStore.lastEventChange === 'sevenFaceCard') {
return this.$vuetify.display.xs ? Transitions.SLIDE_DOWN : Transitions.ENTER_FROM_UPPER_LEFT;
@@ -596,6 +591,9 @@ export default {
return Transitions.SLIDE_DOWN_LEFT;
},
opponentPointsTransition() {
+ if (this.topdeckZone === 'opponentPoints') {
+ return this.toDeckTransition('opponent', 'points');
+ }
switch (this.gameStore.lastEventChange) {
// Jacks cause point cards to switch control (from/towards player)
case 'jack':
@@ -610,16 +608,10 @@ export default {
case 2:
case 6:
return Transitions.SLIDE_DOWN;
- // Nine transitions depend on the target type
+ // Nines move the target off the board entirely (to the deck), or hand a
+ // jacked point card back down to the player
case 9:
- switch (this.gameStore.lastEventTargetType) {
- // Nine on a jack switches point card control
- case 'jack':
- return Transitions.SLIDE_DOWN;
- // Everything else returns cards to hand
- default:
- return Transitions.SLIDE_UP;
- }
+ return Transitions.SLIDE_DOWN;
default:
return Transitions.SLIDE_UP_DOWN;
}
@@ -629,13 +621,8 @@ export default {
}
},
opponentFaceCardsTransition() {
- // If a face card is bounced by a nine, slide up to opponent's hand
- if (
- this.gameStore.lastEventChange === 'resolve' &&
- this.gameStore.lastEventOneOffRank === 9 &&
- this.gameStore.lastEventTargetType === 'faceCard'
- ) {
- return Transitions.SLIDE_UP;
+ if (this.topdeckZone === 'opponentFaceCards') {
+ return this.toDeckTransition('opponent', 'faceCards');
}
// Playing from the deck
if (this.gameStore.lastEventChange === 'sevenFaceCard') {
@@ -645,6 +632,20 @@ export default {
// Otherwise in from opponent hand, out towards scrap
return Transitions.SLIDE_UP_DOWN;
},
+ /**
+ * Jacks ride along with the point row they sit on, except when the jack itself is the
+ * card being topdecked -- then it leaves for the deck while its point card goes home.
+ */
+ playerJacksTransition() {
+ return this.topdeckZone === 'playerJacks'
+ ? this.toDeckTransition('player', 'points')
+ : this.playerPointsTransition;
+ },
+ opponentJacksTransition() {
+ return this.topdeckZone === 'opponentJacks'
+ ? this.toDeckTransition('opponent', 'points')
+ : this.opponentPointsTransition;
+ },
//////////////////
// Interactions //
//////////////////
@@ -712,18 +713,6 @@ export default {
return [];
}
},
- nineTarget() {
- switch (this.targetType) {
- case 'point':
- return this.nineTargetIndex !== null ? this.gameStore.opponent.points[this.nineTargetIndex] : null;
- case 'faceCard':
- return this.nineTargetIndex !== null
- ? this.gameStore.opponent.faceCards[this.nineTargetIndex]
- : null;
- default:
- return null;
- }
- },
// Sevens
playingFromDeck() {
return this.gameStore.playingFromDeck;
@@ -783,12 +772,33 @@ export default {
this.snackbarStore.alert(this.t(messageKey));
this.clearSelection();
},
+ /**
+ * Which way "toward the deck" is, for a card leaving the field during a nine's topdeck.
+ * At xs the deck sits below the field; above xs it sits to the upper left, so a card on
+ * the player's half travels up and left while one on the opponent's half goes straight
+ * across. Jacks follow their own point row.
+ */
+ toDeckTransition(side, area) {
+ if (this.$vuetify.display.xs) {
+ return area === 'points' ? Transitions.TO_DECK_DOWN : Transitions.TO_DECK_DOWN_LEFT;
+ }
+ return side === 'player' ? Transitions.TO_DECK_UP_LEFT : Transitions.TO_DECK_LEFT;
+ },
+ isTopdeckedCard(card) {
+ return card.id === (this.gameStore.topdeckedCard?.id ?? null);
+ },
+ /**
+ * Withholds suit and rank from the card a nine is topdecking, which is all GameCard needs
+ * to render its back (see its isBack check) and flips it via the existing CARD_FLIP
+ * transition. Data attributes keep using the real card so selectors stay stable.
+ */
+ faceDownWhenTopdecked(card) {
+ return this.isTopdeckedCard(card) ? { suit: undefined, rank: undefined } : card;
+ },
showCustomSnackbarMessage(messageKey) {
this.snackbarStore.alert(this.t(messageKey), 'base-dark');
},
clearOverlays() {
- this.nineTargetIndex = null;
- this.targetType = null;
this.targeting = false;
},
clearSelection() {
diff --git a/src/routes/home/components/announcementDialog/data/announcementData.js b/src/routes/home/components/announcementDialog/data/announcementData.js
index c3cbd96f6..a5ddcae4b 100644
--- a/src/routes/home/components/announcementDialog/data/announcementData.js
+++ b/src/routes/home/components/announcementDialog/data/announcementData.js
@@ -1,22 +1,28 @@
// import { Card } from '../../../../../../tests/e2e/fixtures/cards';
export const announcementData = {
- id: 'diamonds2026LiveAnnouncement',
+ id: 'topdeckNines2026Announcement',
activatorText: 'announcement.activatorText',
title: 'announcement.title',
- displayCards: [],
- imgSrc: '/img/announcement/diamonds_2026_cuttle_season_championship.svg',
- imgHref: 'https://challonge.com/diamonds_2026_cuttle_season_championship',
- startTime: '2026-08-30T12:00:00-04:00',
- endTime: '2026-08-30T17:00:00-04:00',
+ displayCards: [
+ { suit: 3, rank: 9 },
+ { suit: 2, rank: 9 },
+ ],
+ startTime: '2026-09-16',
+ endTime: '2027-01-05',
announcementText: [
{
heading: 'announcement.heading',
+ paragraph: [ { text: 'announcement.paragraph' } ],
+ },
+ {
+ heading: 'announcement.heading2',
paragraph: [
- { text: 'announcement.paragraph' },
- { text: 'announcement.twitchLink', url: 'https://www.twitch.tv/cuttle_cards' },
- { text: 'announcement.linkSeparator' },
- { text: 'announcement.youtubeLink', url: 'https://www.youtube.com/live/mMYULGUXrfs' },
+ { text: 'announcement.paragraph2' },
+ {
+ text: 'announcement.patreonLink',
+ url: 'https://www.patreon.com/cuttle/posts/help-refine-4-9-169762967',
+ },
],
},
],
diff --git a/src/sass/transitions.scss b/src/sass/transitions.scss
index a3ad3ca87..d6b063f0a 100644
--- a/src/sass/transitions.scss
+++ b/src/sass/transitions.scss
@@ -138,3 +138,38 @@
from { rotate: 8deg; }
to { rotate: 0deg; }
}
+
+// Nine one-off: the targeted card flips face down (CARD_FLIP, via GameCard's isBack)
+// and then travels off toward the deck. Which way "toward the deck" is depends on the
+// breakpoint and which half of the field the card sits on -- see toDeckTransition() in
+// GameView.
+//
+// Only `-leave-active` is positioned absolutely, so the cards left behind reflow into the
+// gap; `-move` has to keep its place in the flow to slide there. That difference is the
+// reason the two cannot share one rule.
+$to-deck-offsets: (
+ // xs: the deck sits below the point rows
+ 'down': translateY(96px),
+ // xs: and below/left of the face card rows
+ 'down-left': translate(-96px, 96px),
+ // above xs: the deck is up and to the left of the player's half
+ 'up-left': translate(-96px, -96px),
+ // above xs: and straight left of the opponent's half
+ 'left': translateX(-96px),
+);
+
+@each $direction, $offset in $to-deck-offsets {
+ .to-deck-#{$direction}-leave-active {
+ position: absolute;
+ transition: opacity var(--duration-slow), transform var(--duration-slow);
+ }
+
+ .to-deck-#{$direction}-move {
+ transition: transform var(--duration-slow);
+ }
+
+ .to-deck-#{$direction}-leave-to {
+ opacity: 0;
+ transform: $offset;
+ }
+}
diff --git a/src/stores/game.js b/src/stores/game.js
index 1a6ef097c..06c3b820a 100644
--- a/src/stores/game.js
+++ b/src/stores/game.js
@@ -104,7 +104,13 @@ export const useGameStore = defineStore('game', () => {
// Last Event
const lastEventChange = ref(null);
const lastEventOneOffRank = ref(null);
- const lastEventTargetType = ref(null);
+ // Set only while a nine's topdeck animation is in flight. Drives both the face-down flip
+ // (GameCard renders its back when suit/rank are withheld) and the toward-the-deck exit.
+ const topdeckedCard = ref(null);
+ // Which list the topdecked card sat in, captured before the server state lands so the row
+ // it leaves from animates toward the deck while the others keep their usual direction:
+ // 'playerPoints' | 'playerFaceCards' | 'playerJacks' | 'opponent...' equivalents.
+ const topdeckedCardZone = ref(null);
const lastEventPlayedBy = ref(null);
// GameOver
const gameIsOver = ref(false);
@@ -249,7 +255,6 @@ export const useGameStore = defineStore('game', () => {
function updateGame(newGame) {
lastEventChange.value = newGame.lastEvent?.change ?? null;
lastEventOneOffRank.value = newGame.lastEvent?.oneOff?.rank ?? null;
- lastEventTargetType.value = newGame.lastEvent?.oneOffTargetType ?? null;
const lastEventPlayer = newGame.lastEvent?.pNum;
lastEventPlayerChoosing.value =
typeof lastEventPlayer === 'number' && myPNum.value !== null ? lastEventPlayer === myPNum.value : null;
@@ -310,7 +315,8 @@ export const useGameStore = defineStore('game', () => {
lastEventPlayerChoosing.value = false;
lastEventChange.value = null;
lastEventOneOffRank.value = null;
- lastEventTargetType.value = null;
+ topdeckedCard.value = null;
+ topdeckedCardZone.value = null;
lastEventPlayedBy.value = null;
lastEventThreeTarget.value = null;
gameIsOver.value = false;
@@ -374,6 +380,95 @@ export const useGameStore = defineStore('game', () => {
await sleep(1000);
updateGame(game);
}
+ /**
+ * Finds which list a card currently sits in, so the row it leaves from can animate toward
+ * the deck while every other row keeps its usual direction.
+ */
+ function findFieldZone(cardId) {
+ const sides = [ [ 'player', player.value ], [ 'opponent', opponent.value ] ];
+ for (const [ side, fieldPlayer ] of sides) {
+ if (!fieldPlayer) {
+ continue;
+ }
+ if (fieldPlayer.faceCards?.some(({ id }) => id === cardId)) {
+ return `${side}FaceCards`;
+ }
+ if (fieldPlayer.points?.some(({ id }) => id === cardId)) {
+ return `${side}Points`;
+ }
+ if (fieldPlayer.points?.some(({ attachments }) => attachments?.some(({ id }) => id === cardId))) {
+ return `${side}Jacks`;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Removes a card from either player's points or face cards, including jacks attached to a
+ * point card. Lets a card play its exit before the server state is applied.
+ */
+ function removeCardFromField(cardId) {
+ players.value.forEach((fieldPlayer) => {
+ if (!fieldPlayer) {
+ return;
+ }
+ const faceCardIndex = fieldPlayer.faceCards?.findIndex(({ id }) => id === cardId) ?? -1;
+ if (faceCardIndex > -1) {
+ fieldPlayer.faceCards.splice(faceCardIndex, 1);
+ }
+
+ const pointIndex = fieldPlayer.points?.findIndex(({ id }) => id === cardId) ?? -1;
+ if (pointIndex > -1) {
+ fieldPlayer.points.splice(pointIndex, 1);
+ return;
+ }
+
+ fieldPlayer.points?.forEach((pointCard) => {
+ const jackIndex = pointCard.attachments?.findIndex(({ id }) => id === cardId) ?? -1;
+ if (jackIndex > -1) {
+ pointCard.attachments.splice(jackIndex, 1);
+ }
+ });
+ });
+ }
+
+ /**
+ * Animates a nine putting its target on top of the deck, in three stages.
+ *
+ * The exit and the knock-on move could share a render -- a topdecked jack leaves through
+ * the jacks list while its point card goes home through the points list, and those are
+ * separate TransitionGroups, so the directions do not compete. They are kept apart on
+ * purpose: giving the card its own beat to reach the deck before the point card comes
+ * home reads better than both moving at once.
+ */
+ async function processNines(targetCard, game) {
+ if (!targetCard || !player.value) {
+ updateGame(game);
+ return;
+ }
+
+ // Drop out of the countering phase up front, the way processThrees and processFives do.
+ // The authoritative state does not land until stage 3, so without this the "waiting for
+ // opponent to counter" scrim covers the whole animation.
+ phase.value = GamePhase.MAIN;
+
+ // Stage 1: the target flips face down where it sits. Matches CARD_FLIP's --duration-slow
+ // so the flip finishes before the card starts moving.
+ topdeckedCardZone.value = findFieldZone(targetCard.id);
+ topdeckedCard.value = targetCard;
+ await sleep(1000);
+
+ // Stage 2: the card leaves for the deck on its own, and the deck grows to match
+ removeCardFromField(targetCard.id);
+ deck.value.unshift({ isHidden: true });
+ await sleep(1000);
+
+ // Stage 3: the server state lands, bringing a topdecked jack's point card home
+ topdeckedCard.value = null;
+ topdeckedCardZone.value = null;
+ updateGame(game);
+ }
+
async function processThrees(chosenCard, game) {
phase.value = GamePhase.MAIN;
const three = game.resolved;
@@ -737,7 +832,8 @@ export const useGameStore = defineStore('game', () => {
lastEventThreeTarget,
lastEventChange,
lastEventOneOffRank,
- lastEventTargetType,
+ topdeckedCard,
+ topdeckedCardZone,
lastEventPlayedBy,
gameIsOver,
winnerPNum,
@@ -791,6 +887,7 @@ export const useGameStore = defineStore('game', () => {
setGameOver,
setRematch,
processScuttle,
+ processNines,
processThrees,
processFours,
processFives,
diff --git a/src/translations/de.json b/src/translations/de.json
index 4303b53f3..2b8915763 100644
--- a/src/translations/de.json
+++ b/src/translations/de.json
@@ -241,7 +241,7 @@
"6": "Alle Könige und Gläser Acht abwerfen",
"7": "Spiele eine der obersten zwei Karten des Decks und lege die andere zurück (beide sind aufgedeckt)",
"8": "Dein Gegner spielt mit offener Hand (seine Karten sind für dich sichtbar)",
- "9": "Gib die Zielkarte zurück in die Hand des Besitzers. Sie kann sie im nächsten Zug nicht spielen",
+ "9": "Lege die Zielkarte oben auf den Nachziehstapel",
"10": "Kein Effekt",
"11": "Spiele auf eine Ziel-Punktkarte, um sie zu stehlen",
"12": "Deine anderen Karten können nur durch Abwerfaktionen angegriffen werden",
@@ -362,7 +362,8 @@
"wrongPhase": "Du kannst jetzt nicht vom Deck spielen",
"mustBeDoubleJacks": "Du kannst keine Karten von einer Sieben abwerfen, es sei denn, beide Karten sind Buben",
"jacksMustBeUnplayable": "Du kannst nicht abwerfen, weil die Buben gespielt werden können"
- }
+ },
+ "onlyTopJack": "Du kannst nur den obersten Buben eines Stapels als Ziel wählen"
}
}
},
@@ -461,7 +462,7 @@
"seven": "Sieben",
"sevenDescription": "Zeige die obersten zwei Karten des Stapels beiden Spielern und wähle eine, die du nach Belieben spielst.",
"nine": "Neun",
- "nineDescription": "Gib eine Karte vom Spielfeld deines Gegners auf dessen Hand zurück. Sie darf in der nächsten Runde nicht gespielt werden.",
+ "nineDescription": "Lege eine Karte vom Spielfeld deines Gegners oben auf den Nachziehstapel.",
"ten": "Zehn",
"tenDescription": "Kein Effekt."
},
@@ -490,7 +491,7 @@
"rule2": "Mische zwei Joker in das Deck. Joker zählen als Royals mit dem Effekt 'Stehle eine Royal-Karte'. Ähnlich wie Buben können sie eine Royal von einem Spieler zu einem anderen transferieren.",
"rule3": "Das Handkartenlimit ist auf 7 (statt 8) reduziert.",
"rule4": "Buben können gespielt werden, um eine Punktekarte von einem beliebigen Spieler zu einem anderen zu übertragen. Du kannst daher Punkte von dir selbst (oder anderen) 'stehlen', um sie deinem Partner zu geben und möglicherweise mit deren Punkten in deinem Zug zu gewinnen (ihr Ziel wird weiterhin durch die Anzahl ihrer eigenen Könige auf dem Spielfeld bestimmt).",
- "rule5": "Neuner können für einen alternativen ähnlichen Effekt gespielt werden. Immer wenn ein Gegner eine Royal spielt, kannst du sofort einen Neuner als Antwort spielen, um diese Karte sofort auf die Hand des Besitzers zurückzugeben und ihren Effekt zu verhindern. Dies kann außerhalb des Zuges erfolgen, kostet keinen Zug, verhindert jedoch nicht, dass der Besitzer die Karte in seinem nächsten Zug erneut spielt. Alternativ kannst du weiterhin einen Neuner für seinen ursprünglichen Einmal-Effekt in deinem Zug spielen (dein Zug wird dafür genutzt), um eine Karte auf die Hand ihres Besitzers zurückzugeben und zu verhindern, dass sie in der nächsten Runde gespielt wird."
+ "rule5": "Neuner können für einen alternativen ähnlichen Effekt gespielt werden. Immer wenn ein Gegner eine Royal spielt, kannst du sofort einen Neuner als Antwort spielen, um diese Karte sofort auf die Hand des Besitzers zurückzugeben und ihren Effekt zu verhindern. Dies kann außerhalb des Zuges erfolgen, kostet keinen Zug, verhindert jedoch nicht, dass der Besitzer die Karte in seinem nächsten Zug erneut spielt. Alternativ kannst du weiterhin einen Neuner für seinen ursprünglichen Einmal-Effekt in deinem Zug spielen (dein Zug wird dafür genutzt), um eine Karte vom Spielfeld deines Gegners oben auf den Nachziehstapel zu legen."
},
"faq": {
"title": "FAQ",
@@ -586,13 +587,13 @@
}
},
"announcement": {
- "activatorText": "Jetzt live: Diamonds 2026 Saisonmeisterschaft!",
- "title": "Diamonds 2026 Meisterschaft",
- "heading": "Jetzt live: Cuttle-Saisonmeisterschaft",
- "paragraph": "Die Diamonds 2026 Cuttle-Saisonmeisterschaft läuft gerade live! Die 8 besten Spielerinnen und Spieler der vergangenen Ranglistensaison kämpfen im Doppel-K.-o.-Modus um den Titel der Diamonds 2026 Cuttle-Saisonmeisterschaft. Verfolge alles live auf",
- "twitchLink": "Twitch",
- "linkSeparator": "oder",
- "youtubeLink": "YouTube"
+ "activatorText": "Neuner werden gestärkt!",
+ "title": "Regel-Beta: Neuner legen obenauf",
+ "heading": "Neuner legen Karten jetzt oben auf den Nachziehstapel",
+ "paragraph": "Ein Neuner ist eine der höchsten Karten im Spiel, doch sein Einmal-Effekt war schon immer der speziellste. Statt eine Karte auf die Hand ihres Besitzers zurückzugeben, legt ein Neuner jetzt eine Karte vom Spielfeld deines Gegners oben auf den Nachziehstapel — eine Punktekarte, eine Royal oder den obersten Buben eines Stapels. Ihr nächster Zug bringt ihnen die eigene Karte zurück, der Neuner kostet sie also einen ganzen Zug. Nichts wird mehr eingefroren, und Damen schützen weiterhin wie bisher.",
+ "heading2": "Die erste von vier Testregeln der Pik-Saison",
+ "paragraph2": "Das ist eine Beta und die erste von vier Regelvarianten, die wir in der Pik-Saison testen — zwei Verbesserungen für Neuner und zwei für Vierer. Gegen Ende des Jahres stellen wir sie in einer Community-Umfrage zur Abstimmung. Den vollständigen Hintergrund zu allen vier findest du unter",
+ "patreonLink": "Die 4 und die 9 verbessern"
},
"profile": {
"myGames": "Meine Spiele",
diff --git a/src/translations/en.json b/src/translations/en.json
index bca7781dc..ee2cb49a0 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -241,7 +241,7 @@
"6": "Scrap all Royals and Glasses eights",
"7": "Play one of the top two cards of the deck and put the other back (both are revealed)",
"8": "Your opponent plays with an open hand (their cards are revealed to you)",
- "9": "Return target card to its controller's hand. They can't play it next turn",
+ "9": "Put target card on top of the deck",
"10": "No effect",
"11": "Play on top of target point card to steal it",
"12": "Your other cards may only be targeted by scuttles",
@@ -363,7 +363,8 @@
"wrongPhase": "You can't play from the deck right now",
"mustBeDoubleJacks": "You can't discard from a seven unless both cards are jacks",
"jacksMustBeUnplayable": "You can't discard because the jacks can be played"
- }
+ },
+ "onlyTopJack": "You can only target the top Jack of a stack"
}
}
},
@@ -462,7 +463,7 @@
"seven": "Seven",
"sevenDescription": "Reveal the top two cards of the deck to both players and choose one to play however you choose.",
"nine": "Nine",
- "nineDescription": "Return a card from your opponent's field to their hand. They cannot play it next turn.",
+ "nineDescription": "Put a card from your opponent's field on top of the deck.",
"ten": "Ten",
"tenDescription": "No effect."
},
@@ -491,7 +492,7 @@
"rule2": "Shuffle two Jokers into the deck. Jokers count as Royals with the effect \"Steal target Royal\". Similar to Jacks, they may transfer control of a Royal from any player to any other player.",
"rule3": "The hand limit is reduced to 7 (from 8).",
"rule4": "Jacks may be played to transfer control of any point card to any other player. You may therefore \"steal\" points from yourself (or anyone else) in order to give them to your partner, potentially winning on your turn with their points (their goal is still determined by the count of their own Kings on the field).",
- "rule5": "Nines can be played for an alternative, similar effect. Whenever an opponent plays a Royal, you may immediately play a Nine in response to return that card to its owner's hand immediately, preventing its effect from taking place. Doing so may be done out of turn and does not take your turn, but does not prevent the target's owner from playing the card again on their next turn. Alternatively, you may still play a Nine for its original one-off effect on your turn (taking your turn to do so) to return a card to its controller's hand and prevent them from playing that card on their next turn."
+ "rule5": "Nines can be played for an alternative, similar effect. Whenever an opponent plays a Royal, you may immediately play a Nine in response to return that card to its owner's hand immediately, preventing its effect from taking place. Doing so may be done out of turn and does not take your turn, but does not prevent the target's owner from playing the card again on their next turn. Alternatively, you may still play a Nine for its original one-off effect on your turn (taking your turn to do so) to put a card from your opponent's field on top of the deck."
},
"faq": {
"title": "FAQ",
@@ -587,13 +588,13 @@
}
},
"announcement": {
- "activatorText": "Live now: Diamonds 2026 Season Championship!",
- "title": "Diamonds 2026 Championship",
- "heading": "Live Now: Cuttle Season Championship",
- "paragraph": "The Diamonds 2026 Cuttle Season Championship is live right now! The top 8 players from the past season of ranked play are competing in a double elimination showdown for the title of Diamonds 2026 Cuttle Season Champion. Watch it live on",
- "twitchLink": "Twitch",
- "linkSeparator": "or",
- "youtubeLink": "YouTube"
+ "activatorText": "Nines are getting a buff!",
+ "title": "Rules Beta: Topdeck Nines",
+ "heading": "Nines now put cards on top of the deck",
+ "paragraph": "A Nine is one of the highest cards in the deck, but its one-off has always been the most niche. Now, instead of returning a card to its controller's hand, a Nine one-off puts a card from your opponent's board on top of the deck — a point card, a Royal, or the top Jack of a stack. Their next draw is their own card coming back, so the Nine costs them a whole turn. Nothing is frozen any more, and Queens still protect as before.",
+ "heading2": "The first of four Spades season trials",
+ "paragraph2": "This is a beta, and the first of four rules variations we're trialling over the Spades season — two Nine buffs and two Four buffs. We'll put them to a community poll toward the end of the year to decide what stays. For the full context and reasoning behind all four, see",
+ "patreonLink": "Help refine the 4 and the 9"
},
"profile": {
"myGames": "My Games",
diff --git a/src/translations/es.json b/src/translations/es.json
index 569d679d2..2718ed9bf 100644
--- a/src/translations/es.json
+++ b/src/translations/es.json
@@ -241,7 +241,7 @@
"6": "Descarta todas las Figuras y Gafas Ocho en juego",
"7": "Revela las dos primeras cartas del mazo de robo y elige una de ellas para jugarla como quieras",
"8": "Puedes ver las cartas de la mano de tu rival (hasta que esta carta se descarte)",
- "9": "Devuelve la carta objetivo a la mano de su controlador. No puede volver a jugarla en su próximo turno.",
+ "9": "Pon la carta objetivo encima del mazo",
"10": "Sin efecto",
"11": "Juega sobre una carta de puntos objetivo para robarla",
"12": "Tus otras cartas no pueden ser objetivo de habilidades Únicas (pero sí pueden ser hundidas por cartas de puntos más altas)",
@@ -362,7 +362,8 @@
"wrongPhase": "No puedes jugar desde el mazo en este momento",
"mustBeDoubleJacks": "No puedes descartar desde un siete a menos que ambas cartas sean jotas",
"jacksMustBeUnplayable": "No puedes descartar porque las jotas se pueden jugar"
- }
+ },
+ "onlyTopJack": "Solo puedes seleccionar la Jota superior de una pila"
}
}
},
@@ -461,7 +462,7 @@
"seven": "Siete",
"sevenDescription": "Revela las dos primeras cartas del mazo a ambos jugadores y elige una para jugar como desees.",
"nine": "Nueve",
- "nineDescription": "Devuelve la carta objetivo a la mano de su controlador. No puede volver a jugarla en su próximo turno",
+ "nineDescription": "Pon una carta del campo de tu rival encima del mazo.",
"ten": "Diez",
"tenDescription": "Sin efecto."
},
@@ -490,7 +491,7 @@
"rule2": "Baraja dos comodines (Jokers) en el mazo. Los comodines cuentan como Figuras con el efecto \"Roba una Figura objetivo\", similar a la Sota. Al igual que las Sotas, pueden transferir el control de una Figura de cualquier jugador a cualquier otro jugador.",
"rule3": "El límite de cartas en la mano se reduce a 7 (de 8).",
"rule4": "Se pueden jugar Sotas para transferir el control de cualquier carta de puntos a cualquier otro jugador. Por lo tanto, puedes \"robarte\" puntos a ti mismo (o a cualquier otra persona) para dárselos a tu compañero, potencialmente ganando en tu turno con sus puntos (su objetivo aún está determinado por la cantidad de Reyes en su propio campo).",
- "rule5": "Se pueden jugar Nueves para obtener un efecto alternativo similar: siempre que un rival juegue una Figura, puedes jugar inmediatamente un Nueve en respuesta para devolver esa carta a la mano de su propietario inmediatamente, evitando que se produzca su efecto. Hacerlo puede hacerse fuera de turno y no gasta tu turno, pero no impide que el propietario del objetivo vuelva a jugar la carta en su siguiente turno. Alternativamente, aún puedes jugar un Nueve para obtener su efecto único original en tu turno (gastando tu turno para hacerlo), para devolver una carta a la mano de su controlador y evitar que juegue esa carta en su próximo turno."
+ "rule5": "Se pueden jugar Nueves para obtener un efecto alternativo similar: siempre que un rival juegue una Figura, puedes jugar inmediatamente un Nueve en respuesta para devolver esa carta a la mano de su propietario inmediatamente, evitando que se produzca su efecto. Hacerlo puede hacerse fuera de turno y no gasta tu turno, pero no impide que el propietario del objetivo vuelva a jugar la carta en su siguiente turno. Alternativamente, aún puedes jugar un Nueve para obtener su efecto único original en tu turno (gastando tu turno para hacerlo), para poner una carta del campo de tu rival encima del mazo."
},
"faq": {
"title": "FAQ",
@@ -586,13 +587,13 @@
}
},
"announcement": {
- "activatorText": "¡En directo ahora: Campeonato de la temporada Diamonds 2026!",
- "title": "Campeonato Diamonds 2026",
- "heading": "En directo ahora: Campeonato de temporada de Cuttle",
- "paragraph": "¡El Campeonato de la Temporada Diamonds 2026 de Cuttle está en directo ahora mismo! Los 8 mejores jugadores de la última temporada de partidas clasificatorias se enfrentan en un torneo de doble eliminación por el título de Campeón de la Temporada Diamonds 2026 de Cuttle. Míralo en directo en",
- "twitchLink": "Twitch",
- "linkSeparator": "o",
- "youtubeLink": "YouTube"
+ "activatorText": "¡Los Nueves reciben una mejora!",
+ "title": "Beta de reglas: Nueves al mazo",
+ "heading": "Los Nueves ahora ponen cartas encima del mazo",
+ "paragraph": "El Nueve es una de las cartas más altas de la baraja, pero su efecto único siempre ha sido el más específico. Ahora, en lugar de devolver una carta a la mano de su controlador, un Nueve pone una carta del campo de tu rival encima del mazo: una carta de puntos, una Figura o la Jota superior de una pila. Su siguiente robo será su propia carta, así que el Nueve les cuesta un turno entero. Ya no se congela nada, y las Damas siguen protegiendo igual que antes.",
+ "heading2": "La primera de cuatro pruebas de la temporada de Picas",
+ "paragraph2": "Esto es una beta, y la primera de cuatro variantes de reglas que probaremos durante la temporada de Picas: dos mejoras para el Nueve y dos para el Cuatro. Las someteremos a una votación de la comunidad hacia final de año para decidir qué se queda. Para conocer el contexto completo y el razonamiento de las cuatro, consulta",
+ "patreonLink": "Ayuda a refinar el 4 y el 9"
},
"profile": {
"myGames": "Mis juegos",
diff --git a/src/translations/fr.json b/src/translations/fr.json
index 6dfd8057d..8d4bd0d5c 100644
--- a/src/translations/fr.json
+++ b/src/translations/fr.json
@@ -241,7 +241,7 @@
"6": "Élimine toutes les figures et les huit en lunettes.",
"7": "Dévoilez deux cartes de la pioche et jouez en une.",
"8": "Révèle la main de votre adversaire.",
- "9": "L'adversaire reprend en main la carte ciblée qui est gelée pour un tour.",
+ "9": "Place la carte ciblée sur le dessus de la pioche",
"10": "Aucune tactique.",
"11": "Cible et vole une carte à points.",
"12": "Protège vos cartes à points des attaques tactiques",
@@ -362,7 +362,8 @@
"wrongPhase": "Vous ne pouvez pas jouer depuis le paquet pour le moment",
"mustBeDoubleJacks": "Vous ne pouvez pas défausser depuis un sept sauf si les deux cartes sont des valets",
"jacksMustBeUnplayable": "Vous ne pouvez pas défausser parce que les valets peuvent être joués"
- }
+ },
+ "onlyTopJack": "Vous ne pouvez cibler que le Valet du dessus d'une pile"
}
}
},
@@ -461,7 +462,7 @@
"seven": "Sept",
"sevenDescription": "Révélez les deux premières cartes du deck aux deux joueurs et choisissez-en une à jouer comme vous le souhaitez.",
"nine": "Neuf",
- "nineDescription": "Ciblez une carte active de votre adversaire pour la renvoyer dans sa main. Il pne pourra pas la jouer à son prochain tour.",
+ "nineDescription": "Placez une carte du terrain de votre adversaire sur le dessus de la pioche.",
"ten": "Dix",
"tenDescription": "Aucun effet."
},
@@ -490,7 +491,7 @@
"rule2": "Se joue avec un paquet de 54 cartes comprenant les deux jokers qui ont la capacité de transférer un carte Figure d'un joueur vers n'importe quel joueur.",
"rule3": "Votre main est limitée à 7 cartes (au lien de 8).",
"rule4": "Les valets peuvent être joués pour transférer n'importe quelle carte à points à n'importe quel autre joueur. Par conséquent, vous pouvez 'voler' des points à vous-même (ou à n'importe qui d'autre) pour les donner à votre partenaire, gagnant potentiellement à votre tour avec leurs points (leur objectif est toujours déterminé par le nombre de rois sur le terrain).",
- "rule5": "Le Neuf a une capacité supplémentaire. Il peut être joué pour contrer une carte figure jouée par un adversaire. Ainsi joué, le Neuf n'empêche pas cet adversaire de rejouer la même carte à son prochain tour. Si vous jouez un Neuf à votre tour (son effet habituel est appliqué. L'adversaire reprend en main la carte ciblée et n'a pas le droit de la jouer à son prochain tour."
+ "rule5": "Le Neuf a une capacité supplémentaire. Il peut être joué pour contrer une carte figure jouée par un adversaire. Ainsi joué, le Neuf n'empêche pas cet adversaire de rejouer la même carte à son prochain tour. Si vous jouez un Neuf à votre tour, son effet habituel est appliqué : la carte ciblée est placée sur le dessus de la pioche."
},
"faq": {
"title": "FAQ",
@@ -586,13 +587,13 @@
}
},
"announcement": {
- "activatorText": "En direct maintenant : Championnat de saison Diamonds 2026 !",
- "title": "Championnat Diamonds 2026",
- "heading": "En direct maintenant : Championnat de saison Cuttle",
- "paragraph": "Le Championnat de saison Diamonds 2026 de Cuttle est en direct en ce moment ! Les 8 meilleurs joueurs de la dernière saison classée s'affrontent en double élimination pour le titre de Champion de la saison Diamonds 2026 de Cuttle. À suivre en direct sur",
- "twitchLink": "Twitch",
- "linkSeparator": "ou",
- "youtubeLink": "YouTube"
+ "activatorText": "Les Neufs sont renforcés !",
+ "title": "Bêta des règles : Neufs sur la pioche",
+ "heading": "Les Neufs placent désormais les cartes sur la pioche",
+ "paragraph": "Le Neuf est l'une des plus fortes cartes du jeu, mais son effet unique a toujours été le plus limité. Désormais, au lieu de renvoyer une carte dans la main de son contrôleur, un Neuf place une carte du terrain de votre adversaire sur le dessus de la pioche : une carte de points, une figure ou le Valet du dessus d'une pile. Sa prochaine pioche lui rendra sa propre carte, le Neuf lui coûte donc un tour entier. Plus rien n'est gelé, et les Dames protègent toujours comme avant.",
+ "heading2": "La première de quatre expérimentations de la saison des Piques",
+ "paragraph2": "Il s'agit d'une bêta, et de la première de quatre variantes de règles que nous testerons pendant la saison des Piques : deux améliorations du Neuf et deux du Quatre. Nous les soumettrons à un vote de la communauté vers la fin de l'année pour décider de ce qui reste. Pour le contexte complet et le raisonnement derrière les quatre, voir",
+ "patreonLink": "Aidez-nous à affiner le 4 et le 9"
},
"profile": {
"myGames": "Mes jeux",
diff --git a/src/translations/ukr.json b/src/translations/ukr.json
index 3d3548a2d..23cbecfe5 100644
--- a/src/translations/ukr.json
+++ b/src/translations/ukr.json
@@ -241,7 +241,7 @@
"6": "Знищити всі королівські карти та восьмірки окулярів",
"7": "Зіграти одну з перших двох карт з колоди та повернути іншу назад (обидві відкриті)",
"8": "Ваш суперник грає з відкритою рукою (його карти відкриті для вас)",
- "9": "Повернути вибрану карту в руку її власника. Вони не можуть зіграти її наступного ходу",
+ "9": "Покласти вибрану карту зверху колоди",
"10": "Без ефекту",
"11": "Зіграти поверх карти з очками, щоб вкрасти її",
"12": "Ваші інші карти можуть бути вибрані лише для знищення",
@@ -362,7 +362,8 @@
"wrongPhase": "Ви не можете грати з колоди зараз",
"mustBeDoubleJacks": "Не можна відкидати з сімки, якщо обидві картки не є валетами",
"jacksMustBeUnplayable": "Не можна відкидати, тому що валети можуть бути зіграні"
- }
+ },
+ "onlyTopJack": "Ви можете вибрати лише верхнього Валета у стосі"
}
}
},
@@ -461,7 +462,7 @@
"seven": "Сім",
"sevenDescription": "розкрийте та покажіть усім дві верхні карти з колоди, а потім оберіть одну та зіграйте нею тим способом, який ви оберете.",
"nine": "Дев'ять",
- "nineDescription": "поверніть будь-яку карту з поля вашого суперника йому назад у руку. Він не може зіграти нею наступного ходу.",
+ "nineDescription": "покладіть будь-яку карту з поля вашого суперника зверху колоди.",
"ten": "Десять",
"tenDescription": "не має ефектів."
},
@@ -490,7 +491,7 @@
"rule2": "До колоди додають двоє джокерів. Джокери рахуються як королі з ефектом \"Викрасти цільового короля\". Як і валети, вони можуть передавати контроль над королем від одного гравця до іншого.",
"rule3": "Ліміт карт у руці зменшено до 7 (з 8).",
"rule4": "Валети можна грати, щоб передати контроль над будь-якою карткою балів іншому гравцеві. Тому ви можете \"вкрасти\" бали у себе (або будь-кого) та передати їх своєму партнеру, потенційно вигравши свій хід його балами (їхня мета все одно залежить від кількості їх власних королів на полі).",
- "rule5": "Дев'ятки можна зіграти для альтернативного подібного ефекту. Коли противник грає короля, ви можете негайно зіграти дев'ятку у відповідь, щоб повернути цю картку власнику до його руки, запобігаючи її ефекту. Це можна зробити поза чергою, і це не забирає ваш хід, але не перешкоджає власнику картки зіграти її на наступному ходу. Як альтернатива, ви все одно можете зіграти дев'ятку для її оригінального ефекту один раз на вашому ходу (використовуючи свій хід для цього), щоб повернути картку в руку її власнику та запобігти її зігранню на наступному ходу."
+ "rule5": "Дев'ятки можна зіграти для альтернативного подібного ефекту. Коли противник грає короля, ви можете негайно зіграти дев'ятку у відповідь, щоб повернути цю картку власнику до його руки, запобігаючи її ефекту. Це можна зробити поза чергою, і це не забирає ваш хід, але не перешкоджає власнику картки зіграти її на наступному ходу. Як альтернатива, ви все одно можете зіграти дев'ятку для її оригінального ефекту один раз на вашому ходу (використовуючи свій хід для цього), щоб покласти картку з поля вашого суперника зверху колоди."
},
"faq": {
"title": "FAQ",
@@ -586,13 +587,13 @@
}
},
"announcement": {
- "activatorText": "Уже в ефірі: чемпіонат сезону Diamonds 2026!",
- "title": "Чемпіонат Diamonds 2026",
- "heading": "Уже в ефірі: чемпіонат сезону Cuttle",
- "paragraph": "Чемпіонат сезону Diamonds 2026 у Cuttle просто зараз в ефірі! 8 найкращих гравців минулого рейтингового сезону змагаються за системою подвійного вибування за титул чемпіона сезону Diamonds 2026 у Cuttle. Дивіться наживо на",
- "twitchLink": "Twitch",
- "linkSeparator": "або",
- "youtubeLink": "YouTube"
+ "activatorText": "Дев'ятки стають сильнішими!",
+ "title": "Бета правил: дев'ятки кладуть на колоду",
+ "heading": "Дев'ятки тепер кладуть карти зверху колоди",
+ "paragraph": "Дев'ятка — одна з найстарших карт у колоді, але її одноразовий ефект завжди був найвужчим. Тепер замість повернення карти в руку власника дев'ятка кладе карту з поля вашого суперника зверху колоди — карту очок, фігуру або верхнього Валета у стосі. Наступним ходом вони візьмуть свою ж карту, тож дев'ятка коштує їм цілого ходу. Більше нічого не заморожується, а Дами захищають як і раніше.",
+ "heading2": "Перша з чотирьох проб сезону Пік",
+ "paragraph2": "Це бета-версія і перша з чотирьох варіацій правил, які ми тестуємо впродовж сезону Пік — два посилення дев'ятки та два четвірки. Наприкінці року проведемо опитування спільноти, щоб вирішити, що залишити. Повний контекст і обґрунтування всіх чотирьох див. тут:",
+ "patreonLink": "Допоможіть удосконалити 4 та 9"
},
"profile": {
"myGames": "Мої ігри",
diff --git a/tests/e2e/specs/in-game/handLimit.spec.js b/tests/e2e/specs/in-game/handLimit.spec.js
index 5c0781148..7253422b2 100644
--- a/tests/e2e/specs/in-game/handLimit.spec.js
+++ b/tests/e2e/specs/in-game/handLimit.spec.js
@@ -410,77 +410,19 @@ describe('Hand Limit — Discard to Hand Limit Phase', () => {
});
});
- describe('Nines and Hand Limit', () => {
+ describe('Passing and Hand Limit', () => {
describe('P1 perspective', () => {
beforeEach(() => {
cy.setupGameAsP1();
});
- it('Skips discarding when opponent nines player at the hand limit and player then plays points', () => {
+ it('Forces player to discard down to hand limit after passing while over the limit with an empty deck', () => {
+ // The over-limit hand is seeded directly: the hand limit is only enforced at the end of
+ // the active player's turn, and no effect hands a card to the inactive player.
cy.loadGameFixture(1, {
- p0Hand: [ Card.NINE_OF_SPADES ],
- p0Points: [],
- p0FaceCards: [],
- p1Hand: [
- Card.ACE_OF_CLUBS,
- Card.THREE_OF_CLUBS,
- Card.FOUR_OF_CLUBS,
- Card.FIVE_OF_CLUBS,
- Card.SIX_OF_CLUBS,
- Card.SEVEN_OF_CLUBS,
- Card.EIGHT_OF_CLUBS,
- Card.NINE_OF_CLUBS,
- ],
- p1Points: [ Card.TEN_OF_CLUBS ],
- p1FaceCards: [],
- topCard: Card.TWO_OF_HEARTS,
- });
-
- // Opponent (P0) plays nine targeting player's (P1's) point card
- cy.playTargetedOneOffOpponent(Card.NINE_OF_SPADES, Card.TEN_OF_CLUBS, 'point');
- cy.get('#cannot-counter-dialog').should('be.visible')
- .get('[data-cy=cannot-counter-resolve]')
- .click();
-
- // Player now has 9 cards (8 in hand + TEN_OF_CLUBS returned) — no immediate discard
- cy.get('[data-player-hand-card]').should('have.length', 9);
- cy.get('#discard-to-hand-limit-dialog').should('not.exist');
-
- // Player plays NINE_OF_CLUBS for points — hand drops to 8, no discard triggered
- cy.get('[data-player-hand-card=9-0]').click();
- cy.get('[data-move-choice=points]').click();
-
- cy.get('[data-player-hand-card]').should('have.length', 8);
- cy.get('#discard-to-hand-limit-dialog').should('not.exist');
-
- // Turn indicator shows it is now the opponent's turn
- cy.get('#turn-indicator').contains('OPPONENT\'S TURN');
-
- assertGameState(1, {
p0Hand: [],
p0Points: [],
p0FaceCards: [],
- p1Hand: [
- Card.ACE_OF_CLUBS,
- Card.THREE_OF_CLUBS,
- Card.FOUR_OF_CLUBS,
- Card.FIVE_OF_CLUBS,
- Card.SIX_OF_CLUBS,
- Card.SEVEN_OF_CLUBS,
- Card.EIGHT_OF_CLUBS,
- Card.TEN_OF_CLUBS,
- ],
- p1Points: [ Card.NINE_OF_CLUBS ],
- p1FaceCards: [],
- scrap: [ Card.NINE_OF_SPADES ],
- });
- });
-
- it('Forces player to discard down to hand limit after opponent nines and player draws', () => {
- cy.loadGameFixture(1, {
- p0Hand: [ Card.NINE_OF_SPADES ],
- p0Points: [],
- p0FaceCards: [],
p1Hand: [
Card.ACE_OF_CLUBS,
Card.THREE_OF_CLUBS,
@@ -490,91 +432,18 @@ describe('Hand Limit — Discard to Hand Limit Phase', () => {
Card.SEVEN_OF_CLUBS,
Card.EIGHT_OF_CLUBS,
Card.NINE_OF_CLUBS,
- ],
- p1Points: [ Card.TEN_OF_CLUBS ],
- p1FaceCards: [],
- topCard: Card.TWO_OF_HEARTS,
- });
-
- // Opponent (P0) plays nine targeting player's (P1's) point card
- cy.playTargetedOneOffOpponent(Card.NINE_OF_SPADES, Card.TEN_OF_CLUBS, 'point');
- cy.get('#cannot-counter-dialog').should('be.visible')
- .get('[data-cy=cannot-counter-resolve]')
- .click();
-
- // Player now has 9 cards (8 in hand + TEN_OF_CLUBS returned) — no immediate discard
- cy.get('[data-player-hand-card]').should('have.length', 9);
- cy.get('#discard-to-hand-limit-dialog').should('not.exist');
-
- // Player draws, bringing hand to 10 cards
- cy.get('#deck').click();
- cy.get('[data-player-hand-card]').should('have.length', 10);
-
- // Discard-to-hand-limit dialog appears — player must discard 2 cards
- cy.get('#discard-to-hand-limit-dialog').should('be.visible');
- cy.get('[data-discard-hand-limit-card=1-0]').click();
- cy.get('[data-discard-hand-limit-card=9-0]').click();
- cy.get('[data-cy=submit-discard-to-hand-limit-dialog]').click();
-
- cy.get('[data-player-hand-card]').should('have.length', 8);
- cy.get('#discard-to-hand-limit-dialog').should('not.exist');
-
- assertGameState(1, {
- p0Hand: [],
- p0Points: [],
- p0FaceCards: [],
- p1Hand: [
- Card.THREE_OF_CLUBS,
- Card.FOUR_OF_CLUBS,
- Card.FIVE_OF_CLUBS,
- Card.SIX_OF_CLUBS,
- Card.SEVEN_OF_CLUBS,
- Card.EIGHT_OF_CLUBS,
Card.TEN_OF_CLUBS,
- Card.TWO_OF_HEARTS,
],
p1Points: [],
p1FaceCards: [],
- scrap: [ Card.NINE_OF_SPADES, Card.ACE_OF_CLUBS, Card.NINE_OF_CLUBS ],
- });
- });
- });
- });
-
- describe('Passing and Hand Limit', () => {
- describe('P1 perspective', () => {
- beforeEach(() => {
- cy.setupGameAsP1();
- });
-
- it('Forces player to discard down to hand limit after passing while over the limit with an empty deck', () => {
- cy.loadGameFixture(1, {
- p0Hand: [ Card.NINE_OF_SPADES ],
- p0Points: [],
- p0FaceCards: [],
- p1Hand: [
- Card.ACE_OF_CLUBS,
- Card.THREE_OF_CLUBS,
- Card.FOUR_OF_CLUBS,
- Card.FIVE_OF_CLUBS,
- Card.SIX_OF_CLUBS,
- Card.SEVEN_OF_CLUBS,
- Card.EIGHT_OF_CLUBS,
- Card.NINE_OF_CLUBS,
- ],
- p1Points: [ Card.TEN_OF_CLUBS ],
- p1FaceCards: [],
// Empty deck so the player can pass (rather than draw) on their turn
deck: [],
});
- // Opponent (P0) plays nine targeting player's (P1's) point card, returning it to hand
- cy.playTargetedOneOffOpponent(Card.NINE_OF_SPADES, Card.TEN_OF_CLUBS, 'point');
- cy.get('#cannot-counter-dialog').should('be.visible')
- .get('[data-cy=cannot-counter-resolve]')
- .click();
+ // Opponent passes, handing the turn to the player who is over the limit
+ cy.passOpponent();
- // Player now has 9 cards (8 in hand + TEN_OF_CLUBS returned) — no immediate discard
+ // Player has 9 cards and has not been asked to discard yet
cy.get('[data-player-hand-card]').should('have.length', 9);
cy.get('#discard-to-hand-limit-dialog').should('not.exist');
@@ -616,10 +485,11 @@ describe('Hand Limit — Discard to Hand Limit Phase', () => {
p1FaceCards: [],
deck: [],
scrap: [
- Card.NINE_OF_SPADES,
+ // Discarded down to the hand limit
Card.ACE_OF_CLUBS,
// Cards put into scrap by loadGameFixture (empty deck sends all unused cards to scrap)
Card.TWO_OF_CLUBS,
+ Card.NINE_OF_SPADES,
Card.JACK_OF_CLUBS,
Card.QUEEN_OF_CLUBS,
Card.KING_OF_CLUBS,
diff --git a/tests/e2e/specs/in-game/one-offs/7_sevens/opponent_sevens.spec.js b/tests/e2e/specs/in-game/one-offs/7_sevens/opponent_sevens.spec.js
index 83f066d12..ab2bd6902 100644
--- a/tests/e2e/specs/in-game/one-offs/7_sevens/opponent_sevens.spec.js
+++ b/tests/e2e/specs/in-game/one-offs/7_sevens/opponent_sevens.spec.js
@@ -1,6 +1,5 @@
-import { assertGameState, assertSnackbar } from '../../../../support/helpers';
+import { assertGameState } from '../../../../support/helpers';
import { Card } from '../../../../fixtures/cards';
-import { SnackBarError } from '../../../../fixtures/snackbarError';
describe('Opponent playing SEVENS', () => {
beforeEach(() => {
@@ -504,6 +503,20 @@ describe('Opponent playing SEVENS', () => {
cy.get('[data-cy=cannot-counter-resolve]').should('be.visible')
.click();
+ // The queen went to the top of the deck, not back to the player's hand
+ assertGameState(1, {
+ p0Hand: [],
+ p0Points: [],
+ p0FaceCards: [],
+ p1Hand: [],
+ p1Points: [],
+ p1FaceCards: [ Card.KING_OF_HEARTS ],
+ scrap: [ Card.NINE_OF_DIAMONDS, Card.SEVEN_OF_CLUBS ],
+ });
+
+ // Player draws their queen straight back off the top
+ cy.get('#deck').click();
+
assertGameState(1, {
p0Hand: [],
p0Points: [],
@@ -559,44 +572,36 @@ describe('Opponent playing SEVENS', () => {
cy.get('[data-cy=cannot-counter-resolve]').should('be.visible')
.click();
+ // The jack went to the top of the deck, and the ace it was stealing reverts to its owner
assertGameState(1, {
p0Hand: [ Card.TEN_OF_DIAMONDS ],
p0Points: [ Card.ACE_OF_CLUBS ],
p0FaceCards: [],
- p1Hand: [ Card.JACK_OF_CLUBS ],
+ p1Hand: [],
p1Points: [],
p1FaceCards: [ Card.KING_OF_HEARTS ],
scrap: [ Card.NINE_OF_CLUBS, Card.SEVEN_OF_CLUBS ],
});
- // player plays the returned jack immediately
- cy.get('[data-player-hand-card=11-0]').click();
- // Frozen move choice cards should be disabled and display frozen text.
- cy.get('[data-move-choice=jack]')
- .should('have.class', 'v-card--disabled')
- .contains('This card is frozen')
- .click({ force: true }); // Break out into separate test case
- cy.get('[data-opponent-point-card=1-0]').click();
- assertSnackbar(SnackBarError.FROZEN_CARD);
- cy.log('Successfully prevented player from playing the jack while it is frozen');
-
- // Player draws
+ // Player draws, getting their own jack back rather than the fixture's top card
cy.get('#deck').click();
assertGameState(1, {
p0Hand: [ Card.TEN_OF_DIAMONDS ],
p0Points: [ Card.ACE_OF_CLUBS ],
p0FaceCards: [],
- p1Hand: [ Card.JACK_OF_CLUBS, Card.FOUR_OF_CLUBS ],
+ p1Hand: [ Card.JACK_OF_CLUBS ],
p1Points: [],
p1FaceCards: [ Card.KING_OF_HEARTS ],
scrap: [ Card.NINE_OF_CLUBS, Card.SEVEN_OF_CLUBS ],
});
+ // The jack is playable right away; nothing is frozen any more
+ cy.get('[data-player-hand-card=11-0]').should('not.have.class', 'frozen');
cy.playPointsOpponent(Card.TEN_OF_DIAMONDS);
- cy.get('[data-player-hand-card]').should('have.length', 2);
+ cy.get('[data-player-hand-card]').should('have.length', 1);
- // Player plays the returned jack
+ // Player replays the jack they just drew
cy.get('[data-player-hand-card=11-0]').click();
cy.get('[data-move-choice=jack]').click();
cy.get('[data-opponent-point-card=1-0]').click();
@@ -605,7 +610,7 @@ describe('Opponent playing SEVENS', () => {
p0Hand: [],
p0Points: [ Card.TEN_OF_DIAMONDS ],
p0FaceCards: [],
- p1Hand: [ Card.FOUR_OF_CLUBS ],
+ p1Hand: [],
p1Points: [ Card.ACE_OF_CLUBS ],
p1FaceCards: [ Card.KING_OF_HEARTS ],
scrap: [ Card.NINE_OF_CLUBS, Card.SEVEN_OF_CLUBS ],
diff --git a/tests/e2e/specs/in-game/one-offs/7_sevens/player_sevens.spec.js b/tests/e2e/specs/in-game/one-offs/7_sevens/player_sevens.spec.js
index 2a7e02940..5ac01dc4f 100644
--- a/tests/e2e/specs/in-game/one-offs/7_sevens/player_sevens.spec.js
+++ b/tests/e2e/specs/in-game/one-offs/7_sevens/player_sevens.spec.js
@@ -847,11 +847,12 @@ describe('Playing SEVENS', () => {
cy.resolveOpponent();
cy.get('#waiting-for-opponent-counter-scrim').should('not.exist');
+ // The queen went to the top of the deck, not back to the opponent's hand
assertGameState(0, {
p0Hand: [],
p0Points: [],
p0FaceCards: [],
- p1Hand: [ Card.QUEEN_OF_CLUBS ],
+ p1Hand: [],
p1Points: [],
p1FaceCards: [ Card.KING_OF_HEARTS ],
scrap: [ Card.NINE_OF_DIAMONDS, Card.SEVEN_OF_CLUBS ],
@@ -902,11 +903,12 @@ describe('Playing SEVENS', () => {
cy.resolveOpponent();
cy.get('#waiting-for-opponent-counter-scrim').should('not.exist');
+ // The jack went to the top of the deck, and the ace it was stealing reverts to its owner
assertGameState(0, {
p0Hand: [],
p0Points: [ Card.ACE_OF_CLUBS ],
p0FaceCards: [],
- p1Hand: [ Card.JACK_OF_CLUBS ],
+ p1Hand: [],
p1Points: [],
p1FaceCards: [ Card.KING_OF_HEARTS ],
scrap: [ Card.NINE_OF_DIAMONDS, Card.SEVEN_OF_CLUBS ],
diff --git a/tests/e2e/specs/in-game/one-offs/9_nines.spec.js b/tests/e2e/specs/in-game/one-offs/9_nines.spec.js
index c6d098b0f..5f124836a 100644
--- a/tests/e2e/specs/in-game/one-offs/9_nines.spec.js
+++ b/tests/e2e/specs/in-game/one-offs/9_nines.spec.js
@@ -2,21 +2,10 @@ import { assertGameState, assertSnackbar } from '../../../support/helpers';
import { Card } from '../../../fixtures/cards';
import { SnackBarError } from '../../../fixtures/snackbarError';
-function assertCardIsFrozen(card) {
- const cardSelector = `[data-player-hand-card=${card.rank}-${card.suit}]`;
- cy.get(cardSelector)
- .should('have.class', 'frozen')
- .click();
-
-
- // Card overlay should have the frozen state shown visually
- cy.get(`[data-player-overlay-card=${card.rank}-${card.suit}]`).should('have.class', 'frozen');
- // Frozen move choice cards should be disabled and display frozen text.
- cy.get('[data-move-choice=points]')
- .should('have.class', 'v-card--disabled')
- .contains('This card is frozen')
- .click({ force: true }); // Break out into separate test case
- assertSnackbar(SnackBarError.FROZEN_CARD);
+function assertDeckCount(count) {
+ cy.get('#deck .c-deck-count').invoke('text')
+ .invoke('trim')
+ .should('eq', `(${count})`);
}
describe('Playing NINES', () => {
@@ -52,7 +41,7 @@ describe('Playing NINES', () => {
});
}); // End 9 scuttle
- it('Plays a nine as ONE-OFF on lower point card to return it to owners hand', () => {
+ it('Plays a nine as ONE-OFF on a lower point card to put it on top of the deck', () => {
cy.loadGameFixture(0, {
p0Hand: [ Card.NINE_OF_SPADES, Card.NINE_OF_HEARTS ],
p0Points: [ Card.TEN_OF_HEARTS ],
@@ -60,29 +49,54 @@ describe('Playing NINES', () => {
p1Hand: [ Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS ],
p1Points: [ Card.ACE_OF_DIAMONDS ],
p1FaceCards: [],
+ topCard: Card.TWO_OF_CLUBS,
});
+ assertDeckCount(46);
// Player plays nine
cy.get('[data-player-hand-card=9-3]').click(); // nine of spades
- cy.get('[data-move-choice=targetedOneOff').click();
+ cy.get('[data-move-choice=targetedOneOff]').click();
cy.get('[data-opponent-point-card=1-1]').click(); // ace of diamonds
// Wait for opponent to resolve
cy.get('#waiting-for-opponent-counter-scrim').should('be.visible');
cy.resolveOpponent();
+ // The ace is gone from the board, and it did NOT go to anyone's hand
assertGameState(0, {
p0Hand: [ Card.NINE_OF_HEARTS ],
p0Points: [ Card.TEN_OF_HEARTS ],
p0FaceCards: [],
- p1Hand: [ Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS, Card.ACE_OF_DIAMONDS ],
+ p1Hand: [ Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS ],
p1Points: [],
p1FaceCards: [],
scrap: [ Card.NINE_OF_SPADES ],
+ deckLength: 47,
+ });
+
+ // The opponent's next draw is the card they just lost, proving it was on top
+ cy.drawCardOpponent();
+ assertDeckCount(46);
+
+ // Player draws the card that was underneath it
+ cy.get('#deck').click();
+ assertDeckCount(45);
+
+ // Opponent replays the ace, so it was never frozen
+ cy.playPointsOpponent(Card.ACE_OF_DIAMONDS);
+
+ assertGameState(0, {
+ p0Hand: [ Card.NINE_OF_HEARTS, Card.TWO_OF_CLUBS ],
+ p0Points: [ Card.TEN_OF_HEARTS ],
+ p0FaceCards: [],
+ p1Hand: [ Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS ],
+ p1Points: [ Card.ACE_OF_DIAMONDS ],
+ p1FaceCards: [],
+ scrap: [ Card.NINE_OF_SPADES ],
});
}); // End 9 one-off low point card
- it('Plays a nine as ONE-OFF on a higher point card to return it to owners hand', () => {
+ it('Plays a nine as ONE-OFF on a higher point card to put it on top of the deck', () => {
cy.loadGameFixture(0, {
p0Hand: [ Card.NINE_OF_CLUBS, Card.NINE_OF_HEARTS ],
p0Points: [ Card.TEN_OF_HEARTS ],
@@ -91,6 +105,7 @@ describe('Playing NINES', () => {
p1Points: [ Card.NINE_OF_SPADES ],
p1FaceCards: [],
});
+ assertDeckCount(46);
// Attempt illegal scuttle
cy.get('[data-player-hand-card=9-0]').click();
@@ -117,14 +132,15 @@ describe('Playing NINES', () => {
p0Hand: [ Card.NINE_OF_CLUBS ],
p0Points: [ Card.TEN_OF_HEARTS ],
p0FaceCards: [],
- p1Hand: [ Card.NINE_OF_SPADES, Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS ],
+ p1Hand: [ Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS ],
p1Points: [],
p1FaceCards: [],
scrap: [ Card.NINE_OF_HEARTS ],
+ deckLength: 47,
});
}); // End 9 one-off high-point card
- it('Plays a nine as a ONE-OFF to return a face card to its owners hand', () => {
+ it('Plays a nine as a ONE-OFF to put a face card on top of the deck', () => {
cy.loadGameFixture(0, {
p0Hand: [ Card.NINE_OF_SPADES, Card.NINE_OF_HEARTS ],
p0Points: [ Card.TEN_OF_HEARTS ],
@@ -133,6 +149,7 @@ describe('Playing NINES', () => {
p1Points: [],
p1FaceCards: [ Card.KING_OF_DIAMONDS ],
});
+ assertDeckCount(46);
// Player plays nine
cy.get('[data-player-hand-card=9-3]').click(); // nine of spades
@@ -148,14 +165,15 @@ describe('Playing NINES', () => {
p0Hand: [ Card.NINE_OF_HEARTS ],
p0Points: [ Card.TEN_OF_HEARTS ],
p0FaceCards: [],
- p1Hand: [ Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS, Card.KING_OF_DIAMONDS ],
+ p1Hand: [ Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS ],
p1Points: [],
p1FaceCards: [],
scrap: [ Card.NINE_OF_SPADES ],
+ deckLength: 47,
});
}); // End 9 on face card
- it('Plays a 9 on a jack to steal back point card', () => {
+ it('Plays a 9 on a jack to steal back point card, topdecking the jack', () => {
cy.loadGameFixture(0, {
p0Hand: [ Card.ACE_OF_SPADES, Card.NINE_OF_CLUBS ],
p0Points: [ Card.TEN_OF_SPADES ],
@@ -163,7 +181,9 @@ describe('Playing NINES', () => {
p1Hand: [ Card.JACK_OF_CLUBS ],
p1Points: [],
p1FaceCards: [],
+ topCard: Card.TWO_OF_CLUBS,
});
+ assertDeckCount(48);
// Player plays Ace of Spades
cy.get('[data-player-hand-card=1-3]').click();
@@ -190,7 +210,7 @@ describe('Playing NINES', () => {
p1FaceCards: [],
});
- // Player plays NINE to destroy jack
+ // Player plays NINE to topdeck the jack
cy.get('[data-player-hand-card=9-0]').click();
cy.get('[data-move-choice=targetedOneOff]').click();
cy.get('#player-hand-targeting').should('be.visible');
@@ -200,19 +220,39 @@ describe('Playing NINES', () => {
cy.get('#waiting-for-opponent-counter-scrim').should('be.visible');
cy.resolveOpponent();
+ // The jack went to the deck; the point card it was stealing reverts to its owner
assertGameState(0, {
p0Hand: [],
p0Points: [ Card.ACE_OF_SPADES, Card.TEN_OF_SPADES ],
p0FaceCards: [],
- p1Hand: [ Card.JACK_OF_CLUBS ],
+ p1Hand: [],
p1Points: [],
p1FaceCards: [],
scrap: [ Card.NINE_OF_CLUBS ],
+ deckLength: 49,
});
// Should no longer see jack of clubs on screen
cy.get('[data-player-face-card=11-0]').should('not.exist');
- }); // End 9 on jack
+
+ // Opponent draws the topdecked jack back, and player draws the card beneath it
+ cy.drawCardOpponent();
+ cy.get('#deck').click();
+ assertDeckCount(47);
+
+ // Opponent immediately replays the jack, so it was never frozen
+ cy.playJackOpponent(Card.JACK_OF_CLUBS, Card.ACE_OF_SPADES);
+
+ assertGameState(0, {
+ p0Hand: [ Card.TWO_OF_CLUBS ],
+ p0Points: [ Card.TEN_OF_SPADES ],
+ p0FaceCards: [],
+ p1Hand: [],
+ p1Points: [ Card.ACE_OF_SPADES ],
+ p1FaceCards: [],
+ scrap: [ Card.NINE_OF_CLUBS ],
+ });
+ }); // End 9 on jack
it('Plays a 9 on a triple jack to steal back jacked point card', () => {
cy.loadGameFixture(0, {
@@ -223,6 +263,7 @@ describe('Playing NINES', () => {
p1Points: [],
p1FaceCards: [],
});
+ assertDeckCount(45);
// Player plays Ace of Spades
cy.get('[data-player-hand-card=1-3]').click();
@@ -249,7 +290,6 @@ describe('Playing NINES', () => {
p1FaceCards: [],
});
-
// player plays jack
cy.get('[data-player-hand-card=11-3]').click();
cy.get('[data-move-choice=jack]').click();
@@ -259,10 +299,15 @@ describe('Playing NINES', () => {
// Opponent plays jack
cy.playJackOpponent(Card.JACK_OF_DIAMONDS, Card.ACE_OF_SPADES);
- // player plays 9 on jack to steal back card
+ // player plays 9 on the jack stack to steal back the card
cy.get('[data-player-hand-card=9-0]').click();
cy.get('[data-move-choice=targetedOneOff]').click();
cy.get('#player-hand-targeting').should('be.visible');
+
+ // Only the top jack of the stack is a legal target
+ cy.get('.jacks-container').find('.target-overlay')
+ .should('have.length', 1);
+
cy.get('[data-opponent-face-card=11-1]').click();
cy.resolveOpponent();
@@ -271,22 +316,26 @@ describe('Playing NINES', () => {
p0Hand: [],
p0Points: [ Card.TEN_OF_SPADES, Card.ACE_OF_SPADES ],
p0FaceCards: [],
- p1Hand: [ Card.JACK_OF_DIAMONDS, Card.NINE_OF_HEARTS ],
+ p1Hand: [ Card.NINE_OF_HEARTS ],
p1Points: [],
p1FaceCards: [],
+ scrap: [ Card.NINE_OF_CLUBS ],
+ deckLength: 46,
});
- // Opponent nines Jack
+ // Opponent nines the jack that is now on top of the player's ace
cy.playTargetedOneOffOpponent(Card.NINE_OF_HEARTS, Card.JACK_OF_SPADES, 'jack');
cy.get('[data-cy=cannot-counter-resolve]').click();
-
+
assertGameState(0, {
- p0Hand: [ Card.JACK_OF_SPADES ],
- p0Points: [ Card.TEN_OF_SPADES, ],
+ p0Hand: [],
+ p0Points: [ Card.TEN_OF_SPADES ],
p0FaceCards: [],
- p1Hand: [ Card.JACK_OF_DIAMONDS ],
+ p1Hand: [],
p1Points: [ Card.ACE_OF_SPADES ],
p1FaceCards: [],
+ scrap: [ Card.NINE_OF_CLUBS, Card.NINE_OF_HEARTS ],
+ deckLength: 47,
});
});
@@ -298,34 +347,17 @@ describe('Playing NINES', () => {
p1Hand: [ Card.JACK_OF_CLUBS, Card.JACK_OF_DIAMONDS, Card.NINE_OF_HEARTS ],
p1Points: [],
p1FaceCards: [],
+ topCard: Card.TWO_OF_CLUBS,
});
+ assertDeckCount(45);
// Player plays Ace of Spades
cy.get('[data-player-hand-card=1-3]').click();
cy.get('[data-move-choice=points]').click();
- assertGameState(0, {
- p0Hand: [ Card.NINE_OF_CLUBS, Card.JACK_OF_SPADES ],
- p0Points: [ Card.TEN_OF_SPADES, Card.ACE_OF_SPADES ],
- p0FaceCards: [],
- p1Hand: [ Card.JACK_OF_CLUBS, Card.JACK_OF_DIAMONDS, Card.NINE_OF_HEARTS ],
- p1Points: [],
- p1FaceCards: [],
- });
-
// Opponent plays jack
cy.playJackOpponent(Card.JACK_OF_CLUBS, Card.ACE_OF_SPADES);
- assertGameState(0, {
- p0Hand: [ Card.NINE_OF_CLUBS, Card.JACK_OF_SPADES ],
- p0Points: [ Card.TEN_OF_SPADES ],
- p0FaceCards: [],
- p1Hand: [ Card.JACK_OF_DIAMONDS, Card.NINE_OF_HEARTS ],
- p1Points: [ Card.ACE_OF_SPADES ],
- p1FaceCards: [],
- });
-
-
// player plays jack
cy.get('[data-player-hand-card=11-3]').click();
cy.get('[data-move-choice=jack]').click();
@@ -335,7 +367,7 @@ describe('Playing NINES', () => {
// Opponent plays jack
cy.playJackOpponent(Card.JACK_OF_DIAMONDS, Card.ACE_OF_SPADES);
- // player plays 9
+ // player plays 9 on the point card itself
cy.get('[data-player-hand-card=9-0]').click();
cy.get('[data-move-choice=targetedOneOff]').click();
cy.get('#player-hand-targeting').should('be.visible');
@@ -343,14 +375,33 @@ describe('Playing NINES', () => {
cy.resolveOpponent();
+ // The ace is topdecked and all three jacks are scrapped
assertGameState(0, {
- p0Hand: [ ],
+ p0Hand: [],
p0Points: [ Card.TEN_OF_SPADES ],
p0FaceCards: [],
- p1Hand: [ Card.NINE_OF_HEARTS, Card.ACE_OF_SPADES ],
+ p1Hand: [ Card.NINE_OF_HEARTS ],
p1Points: [],
p1FaceCards: [],
- scrap: [ Card.JACK_OF_DIAMONDS, Card.JACK_OF_CLUBS, Card.JACK_OF_SPADES, Card.NINE_OF_CLUBS ]
+ scrap: [ Card.JACK_OF_DIAMONDS, Card.JACK_OF_CLUBS, Card.JACK_OF_SPADES, Card.NINE_OF_CLUBS ],
+ deckLength: 46,
+ });
+
+ // Opponent draws the ace back, and the player draws the card beneath it
+ cy.drawCardOpponent();
+ cy.get('#deck').click();
+ assertDeckCount(44);
+
+ cy.playPointsOpponent(Card.ACE_OF_SPADES);
+
+ assertGameState(0, {
+ p0Hand: [ Card.TWO_OF_CLUBS ],
+ p0Points: [ Card.TEN_OF_SPADES ],
+ p0FaceCards: [],
+ p1Hand: [ Card.NINE_OF_HEARTS ],
+ p1Points: [ Card.ACE_OF_SPADES ],
+ p1FaceCards: [],
+ scrap: [ Card.JACK_OF_DIAMONDS, Card.JACK_OF_CLUBS, Card.JACK_OF_SPADES, Card.NINE_OF_CLUBS ],
});
});
@@ -381,15 +432,16 @@ describe('Playing NINES', () => {
p1Points: [ Card.ACE_OF_DIAMONDS ],
p1FaceCards: [],
scrap: [],
+ deckLength: 46,
});
});
- it('Plays a nine as a ONE-OFF, make sure that the bounced card is playable later', () => {
+ it('Plays a nine as a ONE-OFF; the topdecked card is drawn back and replayed immediately', () => {
/*
- 1). P0 plays a 9, targeting an in-play Queen
+ 1). P0 plays a 9, topdecking an in-play Queen
2). P1 plays a 6's one-off, removing some cards
3). P0 plays a Jack, targeting an in-play 10
- 4). P1 can replay their bounced Queen
+ 4). P1 draws their topdecked Queen and replays it with no delay
*/
// Initial state
@@ -400,10 +452,12 @@ describe('Playing NINES', () => {
p1Hand: [ Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS ],
p1Points: [ Card.TEN_OF_HEARTS ],
p1FaceCards: [ Card.QUEEN_OF_DIAMONDS ],
+ topCard: Card.TWO_OF_CLUBS,
});
+ assertDeckCount(45);
// STEP 1
- cy.log('STEP 1- P0 plays nine, targeting the Queen in play');
+ cy.log('STEP 1- P0 plays nine, topdecking the Queen in play');
cy.get('[data-player-hand-card=9-3]').click(); // nine of spades
cy.get('[data-move-choice=targetedOneOff]').click();
cy.get('#player-hand-targeting').should('be.visible');
@@ -417,10 +471,11 @@ describe('Playing NINES', () => {
p0Hand: [ Card.JACK_OF_SPADES ],
p0Points: [],
p0FaceCards: [ Card.KING_OF_HEARTS ],
- p1Hand: [ Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS, Card.QUEEN_OF_DIAMONDS ],
+ p1Hand: [ Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS ],
p1Points: [ Card.TEN_OF_HEARTS ],
p1FaceCards: [],
scrap: [ Card.NINE_OF_SPADES ],
+ deckLength: 46,
});
// STEP 2
@@ -435,7 +490,7 @@ describe('Playing NINES', () => {
p0Hand: [ Card.JACK_OF_SPADES ],
p0Points: [],
p0FaceCards: [],
- p1Hand: [ Card.QUEEN_OF_HEARTS, Card.QUEEN_OF_DIAMONDS ],
+ p1Hand: [ Card.QUEEN_OF_HEARTS ],
p1Points: [ Card.TEN_OF_HEARTS ],
p1FaceCards: [],
scrap: [ Card.NINE_OF_SPADES, Card.KING_OF_HEARTS, Card.SIX_OF_HEARTS ],
@@ -451,18 +506,22 @@ describe('Playing NINES', () => {
p0Hand: [],
p0Points: [ Card.TEN_OF_HEARTS ],
p0FaceCards: [],
- p1Hand: [ Card.QUEEN_OF_HEARTS, Card.QUEEN_OF_DIAMONDS ],
+ p1Hand: [ Card.QUEEN_OF_HEARTS ],
p1Points: [],
p1FaceCards: [],
scrap: [ Card.NINE_OF_SPADES, Card.KING_OF_HEARTS, Card.SIX_OF_HEARTS ],
});
// STEP 4
- cy.log('STEP 4- P1 plays their previously bounced Queen');
+ cy.log('STEP 4- P1 draws their topdecked Queen and plays it right away');
+ cy.drawCardOpponent();
+ cy.get('#deck').click();
+ assertDeckCount(44);
+
cy.playFaceCardOpponent(Card.QUEEN_OF_DIAMONDS);
assertGameState(0, {
- p0Hand: [],
+ p0Hand: [ Card.TWO_OF_CLUBS ],
p0Points: [ Card.TEN_OF_HEARTS ],
p0FaceCards: [],
p1Hand: [ Card.QUEEN_OF_HEARTS ],
@@ -472,13 +531,13 @@ describe('Playing NINES', () => {
});
});
- it('Plays a nine as a ONE-OFF, make sure that the bounced card is playable even if there is countering', () => {
+ it('Plays a nine as a ONE-OFF; the topdecked card returns even when a later one-off is countered', () => {
/*
- 1). P0 plays 9 one-off, targeting P1's Queen
+ 1). P0 plays 9 one-off, topdecking P1's Queen
2). P1 plays 6's one-off
- 3). P0 counters and it resolves
+ 3). P0 counters and it fizzles
4). P0 plays their Jack, targeting P1's 10
- 5). P1 plays their Queen again (it should no longer be frozen)
+ 5). P1 draws their Queen back and plays it again
*/
// Initial state
@@ -489,10 +548,12 @@ describe('Playing NINES', () => {
p1Hand: [ Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS ],
p1Points: [ Card.TEN_OF_HEARTS ],
p1FaceCards: [ Card.QUEEN_OF_DIAMONDS ],
+ topCard: Card.TWO_OF_CLUBS,
});
+ assertDeckCount(44);
// STEP 1
- cy.log('STEP 1- P0 plays nine, targeting the Queen in play');
+ cy.log('STEP 1- P0 plays nine, topdecking the Queen in play');
cy.get('[data-player-hand-card=9-3]').click(); // nine of spades
cy.get('[data-move-choice=targetedOneOff]').click();
cy.get('#player-hand-targeting').should('be.visible');
@@ -506,10 +567,11 @@ describe('Playing NINES', () => {
p0Hand: [ Card.TWO_OF_SPADES, Card.JACK_OF_SPADES ],
p0Points: [],
p0FaceCards: [ Card.KING_OF_HEARTS ],
- p1Hand: [ Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS, Card.QUEEN_OF_DIAMONDS ],
+ p1Hand: [ Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS ],
p1Points: [ Card.TEN_OF_HEARTS ],
p1FaceCards: [],
scrap: [ Card.NINE_OF_SPADES ],
+ deckLength: 45,
});
// STEP 2
@@ -535,7 +597,7 @@ describe('Playing NINES', () => {
p0Hand: [ Card.JACK_OF_SPADES ],
p0Points: [],
p0FaceCards: [ Card.KING_OF_HEARTS ],
- p1Hand: [ Card.QUEEN_OF_HEARTS, Card.QUEEN_OF_DIAMONDS ],
+ p1Hand: [ Card.QUEEN_OF_HEARTS ],
p1Points: [ Card.TEN_OF_HEARTS ],
p1FaceCards: [],
scrap: [ Card.NINE_OF_SPADES, Card.SIX_OF_HEARTS, Card.TWO_OF_SPADES ],
@@ -548,12 +610,16 @@ describe('Playing NINES', () => {
cy.get('#player-hand-targeting').should('be.visible');
cy.get('[data-opponent-point-card=10-2]').click(); // Ten of hearts
- // STEP 4
- cy.log('STEP 5- P1 plays their previously bounced Queen');
+ // STEP 5
+ cy.log('STEP 5- P1 draws their topdecked Queen and plays it');
+ cy.drawCardOpponent();
+ cy.get('#deck').click();
+ assertDeckCount(43);
+
cy.playFaceCardOpponent(Card.QUEEN_OF_DIAMONDS);
assertGameState(0, {
- p0Hand: [],
+ p0Hand: [ Card.TWO_OF_CLUBS ],
p0Points: [ Card.TEN_OF_HEARTS ],
p0FaceCards: [ Card.KING_OF_HEARTS ],
p1Hand: [ Card.QUEEN_OF_HEARTS ],
@@ -569,7 +635,7 @@ describe('Playing NINES', () => {
cy.setupGameAsP1();
});
- it('Disables playing a frozen number card until the following turn', () => {
+ it('Topdecks the point card, which the player draws back and plays immediately', () => {
cy.loadGameFixture(1, {
p0Hand: [ Card.NINE_OF_CLUBS ],
p0Points: [ Card.THREE_OF_CLUBS ],
@@ -579,8 +645,9 @@ describe('Playing NINES', () => {
p1FaceCards: [],
topCard: Card.TEN_OF_CLUBS,
});
+ assertDeckCount(49);
- // opponent plays nine to return seven to player's hand
+ // opponent plays nine to topdeck the player's seven
cy.playTargetedOneOffOpponent(Card.NINE_OF_CLUBS, Card.SEVEN_OF_CLUBS, 'point');
// Player resolves
@@ -588,6 +655,23 @@ describe('Playing NINES', () => {
.get('[data-cy=cannot-counter-resolve]')
.click();
+ // The seven is back on top of the deck
+ assertGameState(1, {
+ p0Hand: [],
+ p0Points: [ Card.THREE_OF_CLUBS ],
+ p0FaceCards: [],
+ p1Hand: [],
+ p1Points: [],
+ p1FaceCards: [],
+ scrap: [ Card.NINE_OF_CLUBS ],
+ });
+ cy.get('[data-player-hand-card]').should('not.exist');
+ assertDeckCount(50);
+
+ // Player draws and gets their own seven back off the top
+ cy.get('#deck').click();
+ assertDeckCount(49);
+
assertGameState(1, {
p0Hand: [],
p0Points: [ Card.THREE_OF_CLUBS ],
@@ -595,23 +679,29 @@ describe('Playing NINES', () => {
p1Hand: [ Card.SEVEN_OF_CLUBS ],
p1Points: [],
p1FaceCards: [],
+ scrap: [ Card.NINE_OF_CLUBS ],
});
+ // Nothing is frozen any more
+ cy.get('[data-player-hand-card=7-0]').should('not.have.class', 'frozen');
- assertCardIsFrozen(Card.SEVEN_OF_CLUBS);
+ cy.drawCardOpponent();
+ assertDeckCount(48);
- // Player then tries to scuttle
+ // Player scuttles with the card that was topdecked
cy.get('[data-player-hand-card=7-0]').click();
- // Card overlay should have the frozen state shown visually
- cy.get('[data-player-overlay-card=7-0]').should('have.class', 'frozen');
- // Frozen move choice cards should be disabled and display frozen text.
- cy.get('[data-move-choice=scuttle]')
- .should('have.class', 'v-card--disabled')
- .contains('This card is frozen')
- .click({ force: true }); // Break out into separate test case
- // Player attempts to scuttle lower point card
+ cy.get('[data-move-choice=scuttle]').click();
cy.get('#player-hand-targeting').should('be.visible');
cy.get('[data-opponent-point-card=3-0]').click();
- assertSnackbar(SnackBarError.FROZEN_CARD);
+
+ assertGameState(1, {
+ p0Hand: [ Card.TEN_OF_CLUBS ],
+ p0Points: [],
+ p0FaceCards: [],
+ p1Hand: [],
+ p1Points: [],
+ p1FaceCards: [],
+ scrap: [ Card.NINE_OF_CLUBS, Card.SEVEN_OF_CLUBS, Card.THREE_OF_CLUBS ],
+ });
});
it('Opponent plays a NINE on a jack to steal back point card', () => {
@@ -624,6 +714,7 @@ describe('Playing NINES', () => {
p1FaceCards: [],
topCard: Card.TEN_OF_CLUBS,
});
+ assertDeckCount(46);
// opponent plays Ace of Spades
cy.playPointsOpponent(Card.ACE_OF_SPADES);
@@ -650,59 +741,48 @@ describe('Playing NINES', () => {
.get('[data-cy=cannot-counter-resolve]')
.click();
+ // The jack went to the deck rather than back to the player's hand
assertGameState(1, {
p0Hand: [ Card.ACE_OF_DIAMONDS ],
p0Points: [ Card.TEN_OF_SPADES, Card.ACE_OF_SPADES ],
p0FaceCards: [],
- p1Hand: [ Card.JACK_OF_CLUBS, Card.TEN_OF_DIAMONDS ],
+ p1Hand: [ Card.TEN_OF_DIAMONDS ],
p1Points: [],
p1FaceCards: [],
scrap: [ Card.NINE_OF_CLUBS ],
+ deckLength: 47,
});
- // Card should have the frozen state shown visually
- cy.get('[data-player-hand-card=11-0]').should('have.class', 'frozen');
-
- // Player attempts plays the returned jack immediately
- cy.get('[data-player-hand-card=11-0]').click();
-
- // Card overlay should have the frozen state shown visually
- cy.get('[data-player-overlay-card=11-0]').should('have.class', 'frozen');
-
- // Frozen move choice cards should be disabled and display frozen text.
- cy.get('[data-move-choice=jack]')
- .should('have.class', 'v-card--disabled')
- .contains('This card is frozen')
- .click({ force: true }); // Break out into separate test case
-
- cy.get('#player-hand-targeting').should('be.visible');
- cy.get('[data-opponent-point-card=1-3]').click();
- assertSnackbar(SnackBarError.FROZEN_CARD);
- cy.log('Correctly prevented player from re-playing frozen card next turn');
cy.get('[data-player-hand-card=10-1]').click();
cy.get('[data-move-choice=points]').click();
+ cy.playPointsOpponent(Card.ACE_OF_DIAMONDS);
+
+ // Player draws their jack back off the top of the deck
+ cy.get('#deck').click();
+ assertDeckCount(46);
+
assertGameState(1, {
- p0Hand: [ Card.ACE_OF_DIAMONDS ],
- p0Points: [ Card.TEN_OF_SPADES, Card.ACE_OF_SPADES ],
+ p0Hand: [],
+ p0Points: [ Card.TEN_OF_SPADES, Card.ACE_OF_SPADES, Card.ACE_OF_DIAMONDS ],
p0FaceCards: [],
p1Hand: [ Card.JACK_OF_CLUBS ],
p1Points: [ Card.TEN_OF_DIAMONDS ],
p1FaceCards: [],
scrap: [ Card.NINE_OF_CLUBS ],
});
+ cy.get('[data-player-hand-card=11-0]').should('not.have.class', 'frozen');
- cy.playPointsOpponent(Card.ACE_OF_DIAMONDS);
- cy.get('[data-player-hand-card]').should('have.length', 1);
+ cy.drawCardOpponent();
- // Player plays jack after one turn
+ // Player replays the jack
cy.get('[data-player-hand-card=11-0]').click();
cy.get('[data-move-choice=jack]').click();
cy.get('#player-hand-targeting').should('be.visible');
cy.get('[data-opponent-point-card=1-1]').click();
assertGameState(1, {
- p0Hand: [],
+ p0Hand: [ Card.TEN_OF_CLUBS ],
p0Points: [ Card.TEN_OF_SPADES, Card.ACE_OF_SPADES ],
p0FaceCards: [],
p1Hand: [],
@@ -712,7 +792,7 @@ describe('Playing NINES', () => {
});
}); // End 9 on jack
- it('Keeps card frozen after requesting a stalemate', () => {
+ it('Keeps the topdecked card on the deck through a rejected stalemate and a reload', () => {
cy.loadGameFixture(1, {
p0Hand: [ Card.NINE_OF_CLUBS ],
p0Points: [ Card.THREE_OF_CLUBS ],
@@ -723,7 +803,7 @@ describe('Playing NINES', () => {
topCard: Card.TEN_OF_CLUBS,
});
- // opponent plays nine to return seven to player's hand
+ // opponent plays nine to topdeck the player's seven
cy.playTargetedOneOffOpponent(Card.NINE_OF_CLUBS, Card.SEVEN_OF_CLUBS, 'point');
// Player resolves
@@ -731,14 +811,7 @@ describe('Playing NINES', () => {
.get('[data-cy=cannot-counter-resolve]')
.click();
- assertGameState(1, {
- p0Hand: [],
- p0Points: [ Card.THREE_OF_CLUBS ],
- p0FaceCards: [],
- p1Hand: [ Card.SEVEN_OF_CLUBS ],
- p1Points: [],
- p1FaceCards: [],
- });
+ assertDeckCount(50);
// Player requests stalemate; opponent rejects
cy.get('#game-menu-activator').click();
@@ -755,58 +828,39 @@ describe('Playing NINES', () => {
cy.rejectStalemateOpponent();
cy.get('#waiting-for-opponent-stalemate-scrim').should('not.exist');
- // Bounced card is frozen
- assertCardIsFrozen(Card.SEVEN_OF_CLUBS);
+ cy.get('[data-player-hand-card]').should('not.exist');
+ assertDeckCount(50);
- // Still frozen after reload
+ // Still on the deck after reload
cy.reload();
- assertCardIsFrozen(Card.SEVEN_OF_CLUBS);
+ cy.get('[data-player-hand-card]').should('not.exist');
+ assertDeckCount(50);
assertGameState(1, {
p0Hand: [],
p0Points: [ Card.THREE_OF_CLUBS ],
p0FaceCards: [],
- p1Hand: [ Card.SEVEN_OF_CLUBS ],
+ p1Hand: [],
p1Points: [],
p1FaceCards: [],
+ scrap: [ Card.NINE_OF_CLUBS ],
});
- });
-
- it('Clears players frozen card after resolving one-off', () => {
- cy.loadGameFixture(1, {
- p0Hand: [ Card.NINE_OF_SPADES, Card.EIGHT_OF_HEARTS ],
- p0Points: [],
- p0FaceCards: [ Card.KING_OF_HEARTS ],
- p1Hand: [ Card.NINE_OF_HEARTS, Card.EIGHT_OF_DIAMONDS ],
- p1Points: [],
- p1FaceCards: [ Card.KING_OF_DIAMONDS ],
- });
- cy.playTargetedOneOffOpponent(Card.NINE_OF_SPADES, Card.KING_OF_DIAMONDS, 'faceCard');
- cy.get('#cannot-counter-dialog').should('be.visible')
- .get('[data-cy=cannot-counter-resolve]')
- .click();
-
- cy.get('[data-player-hand-card=9-2]').click();
- cy.get('[data-move-choice=targetedOneOff').click();
- cy.get('[data-opponent-face-card=13-2]').click();
- cy.resolveOpponent();
- cy.playPointsOpponent(Card.EIGHT_OF_HEARTS);
- cy.get('[data-player-hand-card=13-1]').click();
- cy.get('[data-move-choice=faceCard]').click();
+ // The seven is still the very next card off the deck
+ cy.get('#deck').click();
assertGameState(1, {
- p0Hand: [ Card.KING_OF_HEARTS ],
- p0Points: [ Card.EIGHT_OF_HEARTS ],
+ p0Hand: [],
+ p0Points: [ Card.THREE_OF_CLUBS ],
p0FaceCards: [],
- p1Hand: [ Card.EIGHT_OF_DIAMONDS ],
+ p1Hand: [ Card.SEVEN_OF_CLUBS ],
p1Points: [],
- p1FaceCards: [ Card.KING_OF_DIAMONDS ],
- scrap: [ Card.NINE_OF_HEARTS, Card.NINE_OF_SPADES ],
+ p1FaceCards: [],
+ scrap: [ Card.NINE_OF_CLUBS ],
});
});
- it('Nine returns card to player hand at hand limit', () => {
+ it('Does not trigger discard-to-hand-limit for the player at the hand limit', () => {
cy.loadGameFixture(1, {
p0Hand: [ Card.NINE_OF_SPADES ],
p0Points: [],
@@ -824,6 +878,7 @@ describe('Playing NINES', () => {
p1Points: [ Card.TEN_OF_CLUBS ],
p1FaceCards: [],
});
+ assertDeckCount(42);
// Opponent (P0) plays nine targeting player's (P1's) point card
cy.playTargetedOneOffOpponent(Card.NINE_OF_SPADES, Card.TEN_OF_CLUBS, 'point');
@@ -831,6 +886,7 @@ describe('Playing NINES', () => {
.get('[data-cy=cannot-counter-resolve]')
.click();
+ // The ten went to the deck, so the player's hand never grows past the limit
assertGameState(1, {
p0Hand: [],
p0Points: [],
@@ -844,21 +900,22 @@ describe('Playing NINES', () => {
Card.SEVEN_OF_CLUBS,
Card.EIGHT_OF_CLUBS,
Card.NINE_OF_CLUBS,
- Card.TEN_OF_CLUBS,
],
p1Points: [],
p1FaceCards: [],
scrap: [ Card.NINE_OF_SPADES ],
+ deckLength: 43,
});
+ cy.get('#discard-to-hand-limit-dialog').should('not.exist');
});
}); // End Opponent playing NINES describe
- describe('Nine triggers discard-to-hand-limit', () => {
+ describe('Nine does not trigger discard-to-hand-limit', () => {
beforeEach(() => {
cy.setupGameAsP0();
});
- it('Nine returns card to opponent hand at hand limit', () => {
+ it('Does not trigger discard-to-hand-limit for the opponent at the hand limit', () => {
cy.loadGameFixture(0, {
p0Hand: [ Card.NINE_OF_SPADES ],
p0Points: [],
@@ -876,6 +933,7 @@ describe('Playing NINES', () => {
p1Points: [ Card.TEN_OF_DIAMONDS ],
p1FaceCards: [],
});
+ assertDeckCount(42);
// Player plays nine as targeted one-off against opponent's point card
cy.get('[data-player-hand-card=9-3]').click();
@@ -897,13 +955,118 @@ describe('Playing NINES', () => {
Card.SIX_OF_HEARTS,
Card.SEVEN_OF_HEARTS,
Card.EIGHT_OF_HEARTS,
- Card.TEN_OF_DIAMONDS,
],
p1Points: [],
p1FaceCards: [],
scrap: [ Card.NINE_OF_SPADES ],
+ deckLength: 43,
+ });
+ cy.get('#discard-to-hand-limit-dialog').should('not.exist');
+ });
+ }); // End Nine does not trigger discard-to-hand-limit
+
+ describe('Playing NINES with an empty deck', () => {
+ beforeEach(() => {
+ cy.setupGameAsP0();
+ });
+
+ it('Puts a card back on an empty deck, replacing the pass with a draw', () => {
+ cy.loadGameFixture(0, {
+ p0Hand: [ Card.NINE_OF_SPADES ],
+ p0Points: [],
+ p0FaceCards: [],
+ p1Hand: [],
+ p1Points: [ Card.TEN_OF_DIAMONDS ],
+ p1FaceCards: [],
+ deck: [],
+ });
+
+ // The deck is exhausted, so the only thing to do with it is pass
+ assertDeckCount(0);
+ cy.get('#empty-deck-text').should('be.visible');
+
+ // Player nines the opponent's ten, putting it back on the empty deck
+ cy.get('[data-player-hand-card=9-3]').click();
+ cy.get('[data-move-choice=targetedOneOff]').click();
+ cy.get('#player-hand-targeting').should('be.visible');
+ cy.get('[data-opponent-point-card=10-1]').click();
+ cy.resolveOpponent();
+
+ // The deck is no longer empty, so passing is no longer on offer
+ assertDeckCount(1);
+ cy.get('#empty-deck-text').should('not.exist');
+
+ // The opponent has to spend their turn drawing their own ten back
+ cy.drawCardOpponent();
+ assertDeckCount(0);
+ cy.get('#empty-deck-text').should('be.visible');
+
+ assertGameState(0, {
+ p0Hand: [],
+ p0Points: [],
+ p0FaceCards: [],
+ p1Hand: [ Card.TEN_OF_DIAMONDS ],
+ p1Points: [],
+ p1FaceCards: [],
+ deck: [],
+ });
+ });
+ }); // End empty deck describe
+
+ describe('Topdeck animation', () => {
+ const fixture = {
+ p0Hand: [ Card.NINE_OF_SPADES ],
+ p0Points: [ Card.TEN_OF_HEARTS ],
+ p0FaceCards: [],
+ p1Hand: [],
+ p1Points: [ Card.ACE_OF_DIAMONDS ],
+ p1FaceCards: [],
+ };
+
+ function topdeckTheAce() {
+ cy.get('[data-player-hand-card=9-3]').click();
+ cy.get('[data-move-choice=targetedOneOff]').click();
+ cy.get('[data-opponent-point-card=1-1]').click();
+ cy.get('#waiting-for-opponent-counter-scrim').should('be.visible');
+ cy.resolveOpponent();
+ }
+
+ beforeEach(() => {
+ cy.setupGameAsP0();
+ });
+
+ it('Flips the target face down, then sends it left toward the deck', () => {
+ cy.viewport(1920, 1080);
+ cy.loadGameFixture(0, fixture);
+ topdeckTheAce();
+
+ // Stage one: still in place, but now showing its back
+ cy.get('[data-opponent-point-card=1-1] img.opponent-card-back').should('exist');
+ // Stage two: leaves toward the deck, which above xs sits to the left
+ cy.get('.to-deck-left-leave-active').should('exist');
+ // Stage three: gone, and the deck has grown
+ cy.get('[data-opponent-point-card=1-1]').should('not.exist');
+
+ assertGameState(0, {
+ p0Hand: [],
+ p0Points: [ Card.TEN_OF_HEARTS ],
+ p0FaceCards: [],
+ p1Hand: [],
+ p1Points: [],
+ p1FaceCards: [],
+ scrap: [ Card.NINE_OF_SPADES ],
+ deckLength: 50,
});
});
- }); // End Nine triggers discard-to-hand-limit
+ it('Sends the target downward toward the deck on xs', () => {
+ cy.viewport('iphone-x');
+ cy.loadGameFixture(0, fixture);
+ topdeckTheAce();
+
+ cy.get('[data-opponent-point-card=1-1] img.opponent-card-back').should('exist');
+ cy.get('.to-deck-down-leave-active').should('exist');
+ cy.get('[data-opponent-point-card=1-1]').should('not.exist');
+ });
+ }); // End topdeck animation describe
});
diff --git a/tests/e2e/specs/in-game/one-offs/targeting_cleanup.spec.js b/tests/e2e/specs/in-game/one-offs/targeting_cleanup.spec.js
index b1a9bad18..754229459 100644
--- a/tests/e2e/specs/in-game/one-offs/targeting_cleanup.spec.js
+++ b/tests/e2e/specs/in-game/one-offs/targeting_cleanup.spec.js
@@ -26,11 +26,12 @@ describe('Clean-up of One-Off Targets', () => {
.get('[data-cy=cannot-counter-resolve]')
.click();
+ // The nine put the ace on top of the deck rather than back into the player's hand
assertGameState(1, {
p0Hand: [ Card.NINE_OF_HEARTS, Card.FIVE_OF_CLUBS ],
p0Points: [ Card.TEN_OF_HEARTS ],
p0FaceCards: [],
- p1Hand: [ Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS, Card.ACE_OF_DIAMONDS ],
+ p1Hand: [ Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS ],
p1Points: [],
p1FaceCards: [],
scrap: [ Card.NINE_OF_SPADES ],
@@ -77,7 +78,7 @@ describe('Clean-up of One-Off Targets', () => {
p0Hand: [ Card.NINE_OF_HEARTS, Card.FIVE_OF_CLUBS ],
p0Points: [ Card.TEN_OF_HEARTS ],
p0FaceCards: [],
- p1Hand: [ Card.SIX_OF_HEARTS, Card.QUEEN_OF_HEARTS ],
+ p1Hand: [ Card.SIX_OF_HEARTS ],
p1Points: [ Card.ACE_OF_DIAMONDS ],
p1FaceCards: [],
scrap: [ Card.NINE_OF_SPADES ],
diff --git a/tests/e2e/specs/playground/videoPlayground.spec.js b/tests/e2e/specs/playground/videoPlayground.spec.js
index 341d20255..ca0b7b074 100644
--- a/tests/e2e/specs/playground/videoPlayground.spec.js
+++ b/tests/e2e/specs/playground/videoPlayground.spec.js
@@ -412,6 +412,43 @@ describe('Video Playground', () => {
cy.resolveOpponent();
});
+ it('Two to scrap a jack', () => {
+ cy.loadGameFixture(0, {
+ p0Hand: [ Card.TEN_OF_SPADES, Card.TWO_OF_CLUBS ],
+ p0Points: [ Card.TEN_OF_HEARTS ],
+ p0FaceCards: [],
+ p1Hand: [ Card.JACK_OF_DIAMONDS, Card.FIVE_OF_HEARTS ],
+ p1Points: [ Card.TEN_OF_CLUBS ],
+ p1FaceCards: [],
+ });
+ markClipStart();
+ cy.wait(1500);
+
+ // START RECORDING //
+ // Player puts the Ten of Spades out for points
+ playerPointsWithDelay(Card.TEN_OF_SPADES);
+
+ // Opponent steals it with a Jack
+ cy.playJackOpponent(Card.JACK_OF_DIAMONDS, Card.TEN_OF_SPADES);
+ cy.get('#turn-indicator').contains('YOUR TURN');
+ cy.wait(2500);
+
+ // Player scraps the Jack with a Two
+ cy.get('[data-player-hand-card=2-0]').click();
+ cy.wait(1200);
+ cy.get('[data-move-choice=targetedOneOff]').click();
+ cy.wait(1200);
+ cy.get('[data-opponent-face-card=11-1]').click();
+ cy.wait(1000);
+ cy.get('#waiting-for-opponent-counter-scrim').should('be.visible');
+ cy.resolveOpponent();
+
+ // The Jack is scrapped and the Ten it was stealing returns to the player
+ cy.get('[data-opponent-face-card=11-1]').should('not.exist');
+ cy.get('[data-player-point-card=10-3]').should('exist');
+ cy.wait(3000);
+ });
+
it('Plays a 2 to counter opponent one-off', () => {
cy.loadGameFixture(0, {
p0Hand: [ Card.TWO_OF_HEARTS, Card.SEVEN_OF_HEARTS ],
@@ -666,23 +703,107 @@ describe('Video Playground', () => {
p1Points: [ Card.TEN_OF_CLUBS ],
p1FaceCards: [],
});
+ markClipStart();
+ cy.wait(1500);
+
+ // START RECORDING //
+ // Player puts the Ten of Spades out for points
cy.get('[data-player-hand-card]').should('have.length', 3);
cy.get('[data-player-hand-card=10-3]').click();
+ cy.wait(1000);
cy.get('[data-move-choice=points]').click();
cy.get('#turn-indicator').contains('OPPONENT\'S TURN');
+ cy.wait(1500);
+
+ // Opponent steals it with a Jack
cy.playJackOpponent(Card.JACK_OF_DIAMONDS, Card.TEN_OF_SPADES);
- cy.wait(2000);
cy.get('#turn-indicator').contains('YOUR TURN');
-
- // START RECORDING HERE //
+ cy.wait(2500);
+
+ // Player answers with a Nine on the Jack
cy.get('[data-player-hand-card=9-0]').click();
- cy.wait(1000);
+ cy.wait(1200);
cy.get('[data-move-choice=targetedOneOff]').click();
- cy.wait(1000);
+ cy.wait(1200);
cy.get('[data-opponent-face-card=11-1]').click();
cy.wait(1000);
cy.get('#waiting-for-opponent-counter-scrim').should('be.visible');
cy.resolveOpponent();
+
+ // The topdeck animation: the Jack flips face down and travels to the deck, and the
+ // Ten it was stealing returns to the player's points
+ cy.get('[data-opponent-face-card=11-1]').should('not.exist');
+ cy.get('[data-player-point-card=10-3]').should('exist');
+ cy.wait(3000);
+ });
+
+ it('Plays a Nine to break check', () => {
+ cy.loadGameFixture(0, {
+ p0Hand: [ Card.ACE_OF_SPADES, Card.NINE_OF_CLUBS ],
+ p0Points: [ Card.TEN_OF_HEARTS ],
+ p0FaceCards: [],
+ p1Hand: [ Card.TEN_OF_SPADES, Card.FIVE_OF_HEARTS ],
+ p1Points: [ Card.TEN_OF_CLUBS ],
+ p1FaceCards: [],
+ });
+ markClipStart();
+ cy.wait(1500);
+
+ // START RECORDING //
+ // Player adds a point of their own
+ playerPointsWithDelay(Card.ACE_OF_SPADES);
+
+ // Opponent goes to 20 of the 21 they need -- any point card now wins for them
+ opponentPointsWithDelay(Card.TEN_OF_SPADES);
+ cy.get('#turn-indicator').contains('YOUR TURN');
+ cy.wait(2500);
+
+ // Player answers with a Nine on the point card that put them in check
+ cy.get('[data-player-hand-card=9-0]').click();
+ cy.wait(1200);
+ cy.get('[data-move-choice=targetedOneOff]').click();
+ cy.wait(1200);
+ cy.get('[data-opponent-point-card=10-3]').click();
+ cy.wait(1000);
+ cy.get('#waiting-for-opponent-counter-scrim').should('be.visible');
+ cy.resolveOpponent();
+
+ // The Ten flips face down and travels to the deck, dropping the opponent back to 10
+ cy.get('[data-opponent-point-card=10-3]').should('not.exist');
+ cy.wait(3000);
+ });
+
+ it('Opponent plays a Nine on the player\'s glasses eight', () => {
+ cy.loadGameFixture(0, {
+ p0Hand: [ Card.ACE_OF_SPADES ],
+ p0Points: [ Card.TEN_OF_HEARTS ],
+ p0FaceCards: [ Card.EIGHT_OF_HEARTS ],
+ p1Hand: [ Card.NINE_OF_CLUBS, Card.FIVE_OF_HEARTS, Card.KING_OF_SPADES ],
+ p1Points: [ Card.TEN_OF_CLUBS ],
+ p1FaceCards: [],
+ });
+ markClipStart();
+
+ // The glasses eight has the opponent's hand face up
+ cy.get('[data-opponent-hand-card=9-0]').should('be.visible');
+ cy.wait(2000);
+
+ // START RECORDING //
+ // Player adds a point, handing over the turn
+ playerPointsWithDelay(Card.ACE_OF_SPADES);
+
+ // Opponent answers by topdecking the glasses
+ cy.playTargetedOneOffOpponent(Card.NINE_OF_CLUBS, Card.EIGHT_OF_HEARTS, 'faceCard');
+ cy.get('#cannot-counter-dialog').should('be.visible');
+ cy.wait(2000);
+ cy.get('[data-cy=cannot-counter-resolve]').click();
+
+ // The glasses flip face down, travel up and left to the deck, and the opponent's
+ // hand goes back to card backs
+ cy.get('[data-player-face-card=8-2]').should('not.exist');
+ cy.get('[data-opponent-hand-card=9-0]').should('not.exist');
+ cy.get('[data-opponent-hand-card]').should('have.length', 2);
+ cy.wait(3000);
});
it('Loses to a jack steal then points for the win', () => {
diff --git a/tests/e2e/support/helpers.js b/tests/e2e/support/helpers.js
index f12cea152..da33c05ef 100644
--- a/tests/e2e/support/helpers.js
+++ b/tests/e2e/support/helpers.js
@@ -303,6 +303,13 @@ function assertDomMatchesFixture(pNum, fixture, spectating) {
if (fixture.scrap) {
cy.get('#scrap').contains(`(${fixture.scrap.length})`);
}
+
+ // Test deck size (if provided)
+ if (typeof fixture.deckLength === 'number') {
+ cy.get('#deck .c-deck-count').invoke('text')
+ .invoke('trim')
+ .should('eq', `(${fixture.deckLength})`);
+ }
}
export function setupSeasons() {
@@ -435,6 +442,16 @@ function assertStoreMatchesFixture(pNum, fixture, isSpectating = false) {
);
}
+ // Deck size if specified. Only the size is assertable from a player's perspective:
+ // create-socket-events' hideDeck() replaces every deck card with { isHidden: true }
+ // except while resolving a seven, so the cards themselves are unknowable here.
+ if (typeof fixture.deckLength === 'number') {
+ expect(game.deck.length).to.eq(
+ fixture.deckLength,
+ `Expected deck to hold ${fixture.deckLength} cards, but it held ${game.deck.length}`,
+ );
+ }
+
// Top Card if specified
if (fixture.topCard) {
expect(cardsMatch(game.topCard, fixture.topCard)).to.eq(
@@ -702,6 +719,7 @@ export function rematchPlayerAsSpectator(userFixture, rematch = true) {
* p1Hand: {suit: number, rank: number}[],
* p1Points: {suit: number, rank: number}[],
* p1FaceCards: {suit: number, rank: number}[],
+ * deckLength?: number - asserts how many cards remain in the deck
* }
* @param pNum: int [0, 1]
*/
diff --git a/tests/unit/fixtures/gameStates/resolveNine.js b/tests/unit/fixtures/gameStates/resolveNine.js
index 6f840a165..4c6e5800b 100644
--- a/tests/unit/fixtures/gameStates/resolveNine.js
+++ b/tests/unit/fixtures/gameStates/resolveNine.js
@@ -14,10 +14,10 @@ const gameStateRow = {
p0Hand: [],
p0Points: [ 'AH', 'TH' ],
p0FaceCards: [],
- p1Hand: [ 'JC' ],
+ p1Hand: [],
p1Points: [],
p1FaceCards: [],
- deck: [ 'AC', '2C', '3C', '4C', '5C', '6C', '7C', '8C', 'TC', 'QC', 'KC',
+ deck: [ 'JC', 'AC', '2C', '3C', '4C', '5C', '6C', '7C', '8C', 'TC', 'QC', 'KC',
'AD', '2D', '3D', '4D', '5D', '6D', '7D', '8D', '9D', 'TD', 'JD', 'QD', 'KD',
'2H', '3H', '4H', '5H', '6H', '7H', '8H', '9H', 'JH', 'QH', 'KH',
'AS', '2S', '3S', '4S', '5S', '6S', '7S', '8S', '9S', 'TS', 'JS', 'QS', 'KS' ],
@@ -49,11 +49,12 @@ export const resolveNine = {
faceCards: [],
},
p1: {
- hand: [ { ...Card.JACK_OF_CLUBS, isFrozen: true } ],
+ hand: [],
points: [],
faceCards: [],
},
deck: [
+ Card.JACK_OF_CLUBS,
Card.ACE_OF_CLUBS, Card.TWO_OF_CLUBS, Card.THREE_OF_CLUBS, Card.FOUR_OF_CLUBS, Card.FIVE_OF_CLUBS,
Card.SIX_OF_CLUBS, Card.SEVEN_OF_CLUBS, Card.EIGHT_OF_CLUBS, Card.TEN_OF_CLUBS, Card.QUEEN_OF_CLUBS,
Card.KING_OF_CLUBS,
@@ -89,6 +90,7 @@ export const resolveNine = {
game: {
chat: [],
deck: [
+ Card.JACK_OF_CLUBS,
Card.ACE_OF_CLUBS, Card.TWO_OF_CLUBS,
Card.THREE_OF_CLUBS, Card.FOUR_OF_CLUBS, Card.FIVE_OF_CLUBS,
Card.SIX_OF_CLUBS, Card.SEVEN_OF_CLUBS, Card.EIGHT_OF_CLUBS, Card.TEN_OF_CLUBS, Card.QUEEN_OF_CLUBS,
@@ -115,7 +117,7 @@ export const resolveNine = {
lock: null,
lockedAt: null,
log: [
- 'The 9♣️ one-off resolves, returning the J♣️ to definitelyNotTheGovernment6969\'s hand. It cannot be played next turn.',
+ 'The 9♣️ one-off resolves, putting the J♣️ on top of the deck.',
],
match: null,
name: 'Test Game',
@@ -143,7 +145,7 @@ export const resolveNine = {
frozenId: null,
},
{
- hand: [ { ...Card.JACK_OF_CLUBS, isFrozen: true } ],
+ hand: [],
points: [],
faceCards: [],
createdAt: '2024-07-14T12:59:47.709Z',
diff --git a/utils/Transitions.json b/utils/Transitions.json
index a334d0e61..f0b252ba5 100644
--- a/utils/Transitions.json
+++ b/utils/Transitions.json
@@ -5,5 +5,9 @@
"ENTER_FROM_UPPER_LEFT": "enter-from-upper-left",
"ENTER_FROM_LEFT": "enter-from-left",
"SLIDE_UP_DOWN": "slide-up-down",
- "CARD_FLIP": "card-flip"
+ "CARD_FLIP": "card-flip",
+ "TO_DECK_DOWN": "to-deck-down",
+ "TO_DECK_DOWN_LEFT": "to-deck-down-left",
+ "TO_DECK_UP_LEFT": "to-deck-up-left",
+ "TO_DECK_LEFT": "to-deck-left"
}
diff --git a/utils/rulesVersion.js b/utils/rulesVersion.js
index b29a83cee..5026b1eee 100644
--- a/utils/rulesVersion.js
+++ b/utils/rulesVersion.js
@@ -23,7 +23,8 @@
* 1.0.0 (#1197) => GameState API release
* 1.0.1 (#1320) => Cannot Three for a Three
* 1.0.2 (#1359) => Hand limit rework: discard down to 8 at end of your turn
+ * 2.0.0 (#1379) => Nine rework: put target card on top of the deck instead of returning it frozen to hand
*/
module.exports = {
- CURRENT_RULES_VERSION: '1.0.2',
+ CURRENT_RULES_VERSION: '2.0.0',
};