diff --git a/AGENTS.md b/AGENTS.md index bd37dcaf8..0ddf7a208 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,6 +67,8 @@ In case of conflicting rules, the following hierarchy applies: - **Discovery**: Glob `src/components` for shared, general-purpose components. Page-specific components are often located in `src/routes/[routeName]/components/`. Page entry points are in `src/routes/[routeName]/[RouteName]View.vue`. - **UI Library**: Vuetify is used. See existing components for usage patterns. - **Routing**: `src/router.js` defines routes and navigation guards. +- **State Management**: Pinia is used for state management; stores are located in `src/stores` and use the setup store syntax +- **Syntax** Use ` + + diff --git a/src/stores/game.js b/src/stores/game.js index 939445a74..971a049e4 100644 --- a/src/stores/game.js +++ b/src/stores/game.js @@ -219,11 +219,16 @@ export const useGameStore = defineStore('game', () => { return isPlayersTurn.value; case GamePhase.RESOLVING_FIVE: return !isPlayersTurn.value; + case GamePhase.DISCARDING_TO_HAND_LIMIT: + return (player.value?.hand?.length ?? 0) <= 8; default: return false; } }); const showResolveFive = computed(() => phase.value === GamePhase.RESOLVING_FIVE && isPlayersTurn.value); + const showDiscardToHandLimit = computed( + () => phase.value === GamePhase.DISCARDING_TO_HAND_LIMIT && (player.value?.hand?.length ?? 0) > 8, + ); const playingFromDeck = computed(() => phase.value === GamePhase.RESOLVING_SEVEN && isPlayersTurn.value); const waitingForOpponentToPlayFromDeck = computed( () => phase.value === GamePhase.RESOLVING_SEVEN && !isPlayersTurn.value, @@ -443,6 +448,7 @@ export const useGameStore = defineStore('game', () => { case 'resolveThree': case 'resolveFour': case 'resolveFive': + case 'discardToHandLimit': case 'seven/points': case 'seven/scuttle': case 'seven/faceCard': @@ -624,6 +630,12 @@ export const useGameStore = defineStore('game', () => { const reqData = cardId2 ? { moveType, cardId1, cardId2 } : { moveType, cardId1 }; await makeSocketRequest('resolveFour', reqData); } + async function requestDiscardToHandLimit(cardIds) { + await makeSocketRequest('discardToHandLimit', { + moveType: MoveType.DISCARD_TO_HAND_LIMIT, + discardedCards: cardIds, + }); + } async function requestResolve() { const moveType = MoveType.RESOLVE; await makeSocketRequest('resolve', { moveType }); @@ -763,6 +775,7 @@ export const useGameStore = defineStore('game', () => { showResolveFour, waitingForOpponentToDiscard, showResolveFive, + showDiscardToHandLimit, playingFromDeck, waitingForOpponentToPlayFromDeck, waitingForOpponentToStalemate, @@ -801,6 +814,7 @@ export const useGameStore = defineStore('game', () => { requestPlayTargetedOneOff, requestPlayJack, requestDiscard, + requestDiscardToHandLimit, requestResolve, requestResolveThree, requestResolveFive, diff --git a/src/translations/de.json b/src/translations/de.json index e4b993327..92b7e1b88 100644 --- a/src/translations/de.json +++ b/src/translations/de.json @@ -122,6 +122,11 @@ "discard": "Abwerfen", "opponentHasResolved": "Dein Gegner hat einen Vier-Einmal-Abwurf gelöst. Du musst zwei Karten abwerfen. Klicke, um Karten auszuwählen." }, + "discardToHandLimit": { + "title": "Discard Excess Cards", + "body": "You have more than 8 cards in hand. Discard down to the hand limit of 8.", + "discard": "Discard" + }, "five": { "discardAndDraw": "Abwerfen und Ziehen", "nice": "Schön!", @@ -338,6 +343,11 @@ "notResolvingFourPhase": "Kann nicht abgeworfen werden, es sei denn, dein Gegner hat eine 4 gespielt", "mustSelectCards": "Du musst zwei Karten zum Abwerfen auswählen" }, + "discardToHandLimit": { + "notDiscardingPhase": "You can only discard to the hand limit during the discard phase", + "handNotOverLimit": "Your hand is not over the limit", + "mustSelectCards": "You must select cards to discard down to the hand limit" + }, "five": { "selectCardToDiscard": "Du musst eine Karte zum Abwerfen auswählen", "incorrectCard": "Falsche Karte gespielt", diff --git a/src/translations/en.json b/src/translations/en.json index 7c938b570..99b185d13 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -122,6 +122,11 @@ "discard": "Discard", "opponentHasResolved": "Your Opponent has resolved a Four One-Off. You must discard two cards. Click to select cards to discard." }, + "discardToHandLimit": { + "title": "Discard Excess Cards", + "body": "You have more than 8 cards in hand. Discard down to the hand limit of 8.", + "discard": "Discard" + }, "five": { "discardAndDraw": "Discard and Draw", "nice": "Nice!", @@ -339,6 +344,11 @@ "notResolvingFourPhase": "Can't discard unless your opponent played a four", "mustSelectCards": "You must select two cards to discard" }, + "discardToHandLimit": { + "notDiscardingPhase": "You can only discard to the hand limit during the discard phase", + "handNotOverLimit": "Your hand is not over the limit", + "mustSelectCards": "You must select cards to discard down to the hand limit" + }, "five": { "selectCardToDiscard": "You must select one card to discard", "incorrectCard": "Incorrect card played", diff --git a/src/translations/es.json b/src/translations/es.json index b44e50d61..d3c87bbae 100644 --- a/src/translations/es.json +++ b/src/translations/es.json @@ -133,6 +133,11 @@ "discard": "Descartar", "opponentHasResolved": "El rival ha usado la habilidad Única del Cuatro. Debes descartar dos cartas. Haz clic para seleccionar las cartas para descartar." }, + "discardToHandLimit": { + "title": "Discard Excess Cards", + "body": "You have more than 8 cards in hand. Discard down to the hand limit of 8.", + "discard": "Discard" + }, "five": { "discardAndDraw": "Descartar y robar", "nice": "¡Bien!", @@ -338,6 +343,11 @@ "notResolvingFourPhase": "No es posible descartar salvo que tu oponente haya jugado un cuatro", "mustSelectCards": "Debes seleccionar dos cartas para descartar" }, + "discardToHandLimit": { + "notDiscardingPhase": "You can only discard to the hand limit during the discard phase", + "handNotOverLimit": "Your hand is not over the limit", + "mustSelectCards": "You must select cards to discard down to the hand limit" + }, "five": { "selectCardToDiscard": "Debes seleccionar una carta para descartar", "incorrectCard": "Carta incorrecta jugada", diff --git a/src/translations/fr.json b/src/translations/fr.json index 585035987..b87f2eaf0 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -133,6 +133,11 @@ "discard": "Défausser", "opponentHasResolved": "Votre adversaire a joué un quatre en attaque. Vous devez défausser deux cartes. Cliquez pour sélectionner les cartes à défausser." }, + "discardToHandLimit": { + "title": "Discard Excess Cards", + "body": "You have more than 8 cards in hand. Discard down to the hand limit of 8.", + "discard": "Discard" + }, "five": { "discardAndDraw": "Jeter et piocher", "nice": "Bon!", @@ -338,6 +343,11 @@ "notResolvingFourPhase": "Vous ne pouvez pas défausser à moins que votre adversaire n'ait joué un Quatre", "mustSelectCards": "Vous devez sélectionner deux cartes à défausser" }, + "discardToHandLimit": { + "notDiscardingPhase": "You can only discard to the hand limit during the discard phase", + "handNotOverLimit": "Your hand is not over the limit", + "mustSelectCards": "You must select cards to discard down to the hand limit" + }, "five": { "selectCardToDiscard": "Vous devez sélectionner une carte à défausser", "incorrectCard": "Mauvaise carte jouée", diff --git a/src/translations/ukr.json b/src/translations/ukr.json index ec292e9bf..1254ec152 100644 --- a/src/translations/ukr.json +++ b/src/translations/ukr.json @@ -122,6 +122,11 @@ "discard": "Відкинути", "opponentHasResolved": "Ваш суперник вирішив чотирку з відривом. Ви повинні відкинути дві карти. Клікніть, щоб вибрати карти для відкидання." }, + "discardToHandLimit": { + "title": "Discard Excess Cards", + "body": "You have more than 8 cards in hand. Discard down to the hand limit of 8.", + "discard": "Discard" + }, "five": { "discardAndDraw": "Відкинути та взяти", "nice": "Класно!", @@ -338,6 +343,11 @@ "notResolvingFourPhase": "Не можна відкидати картки, якщо ваш суперник не зіграв чотвірку", "mustSelectCards": "Ви повинні вибрати дві картки для відкидання" }, + "discardToHandLimit": { + "notDiscardingPhase": "You can only discard to the hand limit during the discard phase", + "handNotOverLimit": "Your hand is not over the limit", + "mustSelectCards": "You must select cards to discard down to the hand limit" + }, "five": { "selectCardToDiscard": "Ви повинні вибрати одну картку для відкидання", "incorrectCard": "Невірна картка зіграна", diff --git a/tests/e2e/specs/in-game/basicMoves.spec.js b/tests/e2e/specs/in-game/basicMoves.spec.js index 25cff7fcb..8f8da7fe6 100644 --- a/tests/e2e/specs/in-game/basicMoves.spec.js +++ b/tests/e2e/specs/in-game/basicMoves.spec.js @@ -467,14 +467,66 @@ describe('Game Basic Moves - P1 Perspective', () => { cy.drawCardOpponent(); // Opponent now has 8 cards in hand cy.get('[data-opponent-hand-card]').should('have.length', 8); - // Player attempts to draw with full hand + // Player draws with full hand - now has 9 cards, triggering discard-to-hand-limit dialog cy.get('#deck').click(); - // Test that Error snackbar for hand limit - assertSnackbar('You are at the hand limit; you cannot draw.'); - // Player still has 8 cards in hand + // Player now has 9 cards in hand + cy.get('[data-player-hand-card]').should('have.length', 9); + // Discard-to-hand-limit dialog appears + cy.get('#discard-to-hand-limit-dialog').should('be.visible'); + // Player selects one card to discard + cy.get('[data-discard-hand-limit-card]').first() + .click(); + cy.get('[data-cy=submit-discard-to-hand-limit-dialog]').click(); + // Player is back to 8 cards in hand cy.get('[data-player-hand-card]').should('have.length', 8); - // Opponent still has 8 cards in hand - cy.get('[data-opponent-hand-card]').should('have.length', 8); + // Dialog is gone + cy.get('#discard-to-hand-limit-dialog').should('not.exist'); + }); + + it('Draws at hand limit triggers discard dialog (P1 perspective)', () => { + cy.loadGameFixture(1, { + p0Hand: [ + Card.ACE_OF_CLUBS, + Card.TWO_OF_CLUBS, + Card.THREE_OF_CLUBS, + Card.FOUR_OF_CLUBS, + Card.SIX_OF_CLUBS, + Card.SEVEN_OF_CLUBS, + Card.EIGHT_OF_CLUBS, + Card.NINE_OF_CLUBS, + ], + p0Points: [], + p0FaceCards: [], + p1Hand: [ Card.ACE_OF_SPADES ], + p1Points: [], + p1FaceCards: [], + topCard: Card.TEN_OF_CLUBS, + secondCard: Card.JACK_OF_CLUBS, + }); + // Opponent (P0) goes first and draws with a full hand of 8 + cy.drawCardOpponent(); + // Opponent now has 9 cards - wait for their discard + cy.get('#waiting-for-opponent-discard-scrim').should('be.visible'); + cy.discardToHandLimitOpponent(Card.ACE_OF_CLUBS); + cy.get('#waiting-for-opponent-discard-scrim').should('not.exist'); + assertGameState(1, { + p0Hand: [ + Card.TWO_OF_CLUBS, + Card.THREE_OF_CLUBS, + Card.FOUR_OF_CLUBS, + Card.SIX_OF_CLUBS, + Card.SEVEN_OF_CLUBS, + Card.EIGHT_OF_CLUBS, + Card.NINE_OF_CLUBS, + Card.TEN_OF_CLUBS, + ], + p0Points: [], + p0FaceCards: [], + p1Hand: [ Card.ACE_OF_SPADES ], + p1Points: [], + p1FaceCards: [], + scrap: [ Card.ACE_OF_CLUBS ], + }); }); it('draws last card from deck, and displays snackbar', () => { diff --git a/tests/e2e/specs/in-game/handLimit.spec.js b/tests/e2e/specs/in-game/handLimit.spec.js new file mode 100644 index 000000000..ed9cf0451 --- /dev/null +++ b/tests/e2e/specs/in-game/handLimit.spec.js @@ -0,0 +1,221 @@ +import { assertGameState } from '../../support/helpers'; +import { Card } from '../../fixtures/cards'; + +describe('Hand Limit — Discard to Hand Limit Phase', () => { + describe('P0 draws at hand limit', () => { + beforeEach(() => { + cy.setupGameAsP0(); + }); + + it('Player draws 9th card and discards one to reach hand limit', () => { + cy.loadGameFixture(0, { + p0Hand: [ + Card.ACE_OF_CLUBS, + Card.TWO_OF_CLUBS, + Card.THREE_OF_CLUBS, + Card.FOUR_OF_CLUBS, + Card.SIX_OF_CLUBS, + Card.SEVEN_OF_CLUBS, + Card.EIGHT_OF_CLUBS, + Card.NINE_OF_CLUBS, + ], + p0Points: [], + p0FaceCards: [], + p1Hand: [ Card.ACE_OF_SPADES ], + p1Points: [], + p1FaceCards: [], + topCard: Card.TEN_OF_CLUBS, + secondCard: Card.JACK_OF_CLUBS, + }); + + // Player draws with a full hand of 8 + cy.get('#deck').click(); + // Player now has 9 cards in hand + cy.get('[data-player-hand-card]').should('have.length', 9); + + // Discard-to-hand-limit dialog appears + cy.get('#discard-to-hand-limit-dialog').should('be.visible'); + + // Player selects one card to discard + cy.get('[data-discard-hand-limit-card=1-0]').click(); + cy.get('[data-cy=submit-discard-to-hand-limit-dialog]').click(); + + // Player is back to 8 cards + cy.get('[data-player-hand-card]').should('have.length', 8); + cy.get('#discard-to-hand-limit-dialog').should('not.exist'); + + assertGameState(0, { + p0Hand: [ + Card.TWO_OF_CLUBS, + Card.THREE_OF_CLUBS, + Card.FOUR_OF_CLUBS, + Card.SIX_OF_CLUBS, + Card.SEVEN_OF_CLUBS, + Card.EIGHT_OF_CLUBS, + Card.NINE_OF_CLUBS, + Card.TEN_OF_CLUBS, + ], + p0Points: [], + p0FaceCards: [], + p1Hand: [ Card.ACE_OF_SPADES ], + p1Points: [], + p1FaceCards: [], + scrap: [ Card.ACE_OF_CLUBS ], + }); + }); + }); + + describe('P1 draws at hand limit (opponent discards)', () => { + beforeEach(() => { + cy.setupGameAsP1(); + }); + + it('Opponent draws 9th card and must discard — player sees waiting overlay', () => { + cy.loadGameFixture(1, { + p0Hand: [ + Card.ACE_OF_CLUBS, + Card.TWO_OF_CLUBS, + Card.THREE_OF_CLUBS, + Card.FOUR_OF_CLUBS, + Card.SIX_OF_CLUBS, + Card.SEVEN_OF_CLUBS, + Card.EIGHT_OF_CLUBS, + Card.NINE_OF_CLUBS, + ], + p0Points: [], + p0FaceCards: [], + p1Hand: [ Card.ACE_OF_SPADES ], + p1Points: [], + p1FaceCards: [], + topCard: Card.TEN_OF_CLUBS, + secondCard: Card.JACK_OF_CLUBS, + }); + + // Opponent (P0) goes first and draws with a full hand of 8 + cy.drawCardOpponent(); + + // Waiting overlay appears for player (P1) while opponent discards + cy.get('#waiting-for-opponent-discard-scrim').should('be.visible'); + + // Opponent discards one card + cy.discardToHandLimitOpponent(Card.ACE_OF_CLUBS); + + // Overlay disappears and it's now P1's turn (P0 discarded, turn passed to P1) + cy.get('#waiting-for-opponent-discard-scrim').should('not.exist'); + + assertGameState(1, { + p0Hand: [ + Card.TWO_OF_CLUBS, + Card.THREE_OF_CLUBS, + Card.FOUR_OF_CLUBS, + Card.SIX_OF_CLUBS, + Card.SEVEN_OF_CLUBS, + Card.EIGHT_OF_CLUBS, + Card.NINE_OF_CLUBS, + Card.TEN_OF_CLUBS, + ], + p0Points: [], + p0FaceCards: [], + p1Hand: [ Card.ACE_OF_SPADES ], + p1Points: [], + p1FaceCards: [], + scrap: [ Card.ACE_OF_CLUBS ], + }); + }); + }); + + describe('Five at 8 cards (overflows by 1)', () => { + beforeEach(() => { + cy.setupGameAsP0(); + }); + + it('Five resolves to 9 cards triggering discard of 1', () => { + cy.loadGameFixture(0, { + p0Hand: [ + Card.FIVE_OF_CLUBS, + Card.ACE_OF_CLUBS, + Card.TWO_OF_CLUBS, + Card.THREE_OF_CLUBS, + Card.FOUR_OF_CLUBS, + Card.SIX_OF_CLUBS, + Card.SEVEN_OF_CLUBS, + Card.EIGHT_OF_CLUBS, + ], + p0Points: [], + p0FaceCards: [], + p1Hand: [ Card.ACE_OF_SPADES ], + p1Points: [], + p1FaceCards: [], + topCard: Card.NINE_OF_CLUBS, + secondCard: Card.TEN_OF_CLUBS, + }); + + // Play five and resolve — discard one card first + cy.playOneOffAndResolveAsPlayer(Card.FIVE_OF_CLUBS); + cy.get('[data-cy=five-discard-dialog]').should('be.visible'); + cy.get('[data-discard-card=1-0]').click(); + cy.get('[data-cy=submit-five-dialog]').click(); + + // Player now has 9 cards (7 remaining - 1 discarded + 3 drawn), triggering discard-to-hand-limit + cy.get('[data-player-hand-card]').should('have.length', 9); + cy.get('#discard-to-hand-limit-dialog').should('be.visible'); + + // Player discards 1 card + cy.get('[data-discard-hand-limit-card]').first() + .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'); + }); + }); + + describe('Five at 9 cards (overflows by 2)', () => { + beforeEach(() => { + cy.setupGameAsP0(); + }); + + it('Five resolves to 10 cards triggering discard of 2', () => { + cy.loadGameFixture(0, { + p0Hand: [ + Card.FIVE_OF_CLUBS, + Card.ACE_OF_CLUBS, + Card.TWO_OF_CLUBS, + Card.THREE_OF_CLUBS, + Card.FOUR_OF_CLUBS, + Card.SIX_OF_CLUBS, + Card.SEVEN_OF_CLUBS, + Card.EIGHT_OF_CLUBS, + Card.NINE_OF_CLUBS, + ], + p0Points: [], + p0FaceCards: [], + p1Hand: [ Card.ACE_OF_SPADES ], + p1Points: [], + p1FaceCards: [], + topCard: Card.TEN_OF_CLUBS, + secondCard: Card.JACK_OF_CLUBS, + }); + + // Play five and resolve — discard one card first (9 cards → 8 after discard → 10 after drawing 3) + cy.playOneOffAndResolveAsPlayer(Card.FIVE_OF_CLUBS); + cy.get('[data-cy=five-discard-dialog]').should('be.visible'); + cy.get('[data-discard-card=1-0]').click(); + cy.get('[data-cy=submit-five-dialog]').click(); + + // Player now has 10 cards (8 remaining - 1 discarded + 3 drawn), must discard 2 + cy.get('[data-player-hand-card]').should('have.length', 10); + cy.get('#discard-to-hand-limit-dialog').should('be.visible'); + + // Player must select 2 cards to discard + cy.get('[data-discard-hand-limit-card]').eq(0) + .click(); + cy.get('[data-discard-hand-limit-card]').eq(1) + .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'); + }); + }); +}); diff --git a/tests/e2e/specs/in-game/one-offs/5_fives.spec.js b/tests/e2e/specs/in-game/one-offs/5_fives.spec.js index bd304edd8..f86f54da0 100644 --- a/tests/e2e/specs/in-game/one-offs/5_fives.spec.js +++ b/tests/e2e/specs/in-game/one-offs/5_fives.spec.js @@ -141,8 +141,8 @@ describe('FIVES', () => { cy.get('[data-player-hand-card]').should('have.length', 2); }); - it('Plays a 5 to draw two cards when already at hand limit (8)', () => { - // Setup: there are three cards in the deck and player has a 5 + it('Plays a 5 to draw three cards when already at hand limit (8) and must discard one', () => { + // Setup: there are three cards in the deck and player has 8 cards including a 5 cy.loadGameFixture(0, { p0Hand: [ Card.FIVE_OF_CLUBS, @@ -165,41 +165,24 @@ describe('FIVES', () => { }); cy.get('#deck').should('contain', '(3)'); - // Play 5 and resolve + // Play 5 and resolve — discard FIVE_OF_SPADES before drawing cy.playOneOffAndResolveAsPlayer(Card.FIVE_OF_CLUBS); cy.get('[data-cy=five-discard-dialog]').should('be.visible'); cy.get('[data-discard-card=5-3]').click(); cy.get('[data-cy=submit-five-dialog]').click(); - assertGameState(0, { - p0Hand: [ - Card.THREE_OF_CLUBS, - Card.EIGHT_OF_HEARTS, - Card.ACE_OF_DIAMONDS, - Card.EIGHT_OF_CLUBS, - Card.TEN_OF_CLUBS, - Card.TWO_OF_CLUBS, - Card.THREE_OF_HEARTS, - Card.FOUR_OF_HEARTS, - ], - p0Points: [], - p0FaceCards: [], - p1Hand: [ Card.TWO_OF_HEARTS ], - p1Points: [], - p1FaceCards: [], - scrap: [ Card.FIVE_OF_CLUBS, Card.FIVE_OF_SPADES, Card.ACE_OF_CLUBS, - Card.FOUR_OF_CLUBS, Card.SIX_OF_CLUBS, Card.SEVEN_OF_CLUBS, Card.NINE_OF_CLUBS, - Card.JACK_OF_CLUBS, Card.QUEEN_OF_CLUBS, Card.KING_OF_CLUBS, Card.TWO_OF_DIAMONDS, - Card.THREE_OF_DIAMONDS, Card.FOUR_OF_DIAMONDS, Card.FIVE_OF_DIAMONDS, Card.SIX_OF_DIAMONDS, - Card.SEVEN_OF_DIAMONDS, Card.EIGHT_OF_DIAMONDS, Card.NINE_OF_DIAMONDS, Card.TEN_OF_DIAMONDS, - Card.JACK_OF_DIAMONDS, Card.QUEEN_OF_DIAMONDS, Card.KING_OF_DIAMONDS, Card.ACE_OF_HEARTS, - Card.FIVE_OF_HEARTS, Card.SIX_OF_HEARTS, Card.SEVEN_OF_HEARTS, Card.NINE_OF_HEARTS, - Card.TEN_OF_HEARTS, Card.JACK_OF_HEARTS, Card.QUEEN_OF_HEARTS, Card.KING_OF_HEARTS, - Card.TWO_OF_SPADES, Card.THREE_OF_SPADES, Card.FOUR_OF_SPADES, - Card.SIX_OF_SPADES, Card.SEVEN_OF_SPADES, Card.EIGHT_OF_SPADES, Card.NINE_OF_SPADES, - Card.TEN_OF_SPADES, Card.JACK_OF_SPADES, Card.QUEEN_OF_SPADES, Card.KING_OF_SPADES ], - }); - cy.get('#deck').should('contain', '(1)'); + // Player now has 9 cards (7 remaining - 1 discarded + 3 drawn), triggering discard-to-hand-limit + cy.get('[data-player-hand-card]').should('have.length', 9); + cy.get('#discard-to-hand-limit-dialog').should('be.visible'); + + // Discard one card to get back to 8 + cy.get('[data-discard-hand-limit-card=1-1]').click(); + cy.get('[data-cy=submit-discard-to-hand-limit-dialog]').click(); + + // Player is back at 8 cards, deck is empty (all 3 drawn) + cy.get('[data-player-hand-card]').should('have.length', 8); + cy.get('#discard-to-hand-limit-dialog').should('not.exist'); + cy.get('#deck').should('contain', '(0)'); }); it('Draws only 1 card when last card in deck', () => { 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 5d16f1031..e7b2099ab 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 @@ -805,5 +805,117 @@ describe('Playing NINES', () => { scrap: [ Card.NINE_OF_HEARTS, Card.NINE_OF_SPADES ], }); }); + + it('Nine returns card to player hand at hand limit, triggering discard dialog for 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: [], + }); + + // 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), must discard + cy.get('#discard-to-hand-limit-dialog').should('be.visible'); + cy.get('[data-discard-hand-limit-card=1-0]').click(); + cy.get('[data-cy=submit-discard-to-hand-limit-dialog]').click(); + + // Player is back to 8 cards, it's now player's turn + 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.NINE_OF_CLUBS, + Card.TEN_OF_CLUBS, + ], + p1Points: [], + p1FaceCards: [], + scrap: [ Card.NINE_OF_SPADES, Card.ACE_OF_CLUBS ] + }); + }); }); // End Opponent playing NINES describe + + describe('Nine triggers discard-to-hand-limit', () => { + beforeEach(() => { + cy.setupGameAsP0(); + }); + + it('Nine returns card to opponent hand at hand limit, triggering discard dialog for opponent', () => { + cy.loadGameFixture(0, { + p0Hand: [ Card.NINE_OF_SPADES ], + p0Points: [], + p0FaceCards: [], + p1Hand: [ + Card.ACE_OF_HEARTS, + Card.TWO_OF_HEARTS, + Card.THREE_OF_HEARTS, + Card.FOUR_OF_HEARTS, + Card.FIVE_OF_HEARTS, + Card.SIX_OF_HEARTS, + Card.SEVEN_OF_HEARTS, + Card.EIGHT_OF_HEARTS, + ], + p1Points: [ Card.TEN_OF_DIAMONDS ], + p1FaceCards: [], + }); + + // Player plays nine as targeted one-off against opponent's point card + 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(); + + // Opponent now has 9 cards (8 in hand + TEN_OF_DIAMONDS returned), must discard + cy.get('#waiting-for-opponent-discard-scrim').should('be.visible'); + cy.discardToHandLimitOpponent(Card.ACE_OF_HEARTS); + cy.get('#waiting-for-opponent-discard-scrim').should('not.exist'); + + assertGameState(0, { + p0Hand: [], + p0Points: [], + p0FaceCards: [], + p1Hand: [ + Card.TWO_OF_HEARTS, + Card.THREE_OF_HEARTS, + Card.FOUR_OF_HEARTS, + Card.FIVE_OF_HEARTS, + Card.SIX_OF_HEARTS, + Card.SEVEN_OF_HEARTS, + Card.EIGHT_OF_HEARTS, + Card.TEN_OF_DIAMONDS, + ], + p1Points: [], + p1FaceCards: [], + scrap: [ Card.NINE_OF_SPADES, Card.ACE_OF_HEARTS ], + }); + }); + + }); // End Nine triggers discard-to-hand-limit }); diff --git a/tests/e2e/support/commands.js b/tests/e2e/support/commands.js index 32c2eb91f..e7d9c31cd 100644 --- a/tests/e2e/support/commands.js +++ b/tests/e2e/support/commands.js @@ -39,6 +39,7 @@ const transformGameUrl = (api, slug, gameId = null) => { case 'resolveThree': case 'resolveFour': case 'resolveFive': + case 'discardToHandLimit': case 'seven/points': case 'seven/scuttle': case 'seven/faceCard': @@ -474,6 +475,13 @@ Cypress.Commands.add('resolveOpponent', (gameId = null) => { * @param card1 {suit: number, rank: number} OPTIONAL * @param card2 {suit: number, rank: number} OPTIONAL */ +Cypress.Commands.add('discardToHandLimitOpponent', (...cards) => { + cy.makeSocketRequest('game', 'discardToHandLimit', { + moveType: MoveType.DISCARD_TO_HAND_LIMIT, + discardedCards: cards.map((card) => card.id), + }); +}); + Cypress.Commands.add('discardOpponent', (card1, card2) => { const moveType = MoveType.RESOLVE_FOUR; diff --git a/types/SocketEvent.js b/types/SocketEvent.js index 6834d2c83..ad548ffb4 100644 --- a/types/SocketEvent.js +++ b/types/SocketEvent.js @@ -19,6 +19,7 @@ export default { RESOLVE_THREE: 'resolveThree', RESOLVE_FOUR: 'resolveFour', RESOLVE_FIVE: 'resolveFive', + DISCARD_TO_HAND_LIMIT: 'discardToHandLimit', SCUTTLE: 'scuttle', SEVEN_FACE_CARD: 'sevenFaceCard', SEVEN_JACK: 'sevenJack', diff --git a/utils/GamePhase.json b/utils/GamePhase.json index bc1e97a8f..a213e2a88 100644 --- a/utils/GamePhase.json +++ b/utils/GamePhase.json @@ -4,6 +4,7 @@ "RESOLVING_THREE" : 3, "RESOLVING_FOUR" : 4, "RESOLVING_FIVE": 5, + "DISCARDING_TO_HAND_LIMIT": 6, "RESOLVING_SEVEN": 7, "CONSIDERING_STALEMATE": -1 } diff --git a/utils/MoveType.json b/utils/MoveType.json index 351d62202..05e5a7f81 100644 --- a/utils/MoveType.json +++ b/utils/MoveType.json @@ -12,6 +12,7 @@ "RESOLVE_THREE": "resolveThree", "RESOLVE_FOUR": "resolveFour", "RESOLVE_FIVE": "resolveFive", + "DISCARD_TO_HAND_LIMIT": "discardToHandLimit", "SEVEN_POINTS": "sevenPoints", "SEVEN_SCUTTLE": "sevenScuttle", "SEVEN_FACE_CARD": "sevenFaceCard",