From 3b8a75b28db29989bf67fde1dbfa5e65afbd4eaf Mon Sep 17 00:00:00 2001 From: Ryan Emberling Date: Sat, 19 Sep 2026 12:33:15 -0400 Subject: [PATCH 1/3] feat: rework fours to discard two cards at random Fours are the weakest card in Cuttle: a weak effect on a low rank with poor points and scuttling value. Because the victim chose which two cards to discard, the effect always stripped their two *worst* cards, which is what keeps the 4 well below every other card in the game. This buffs it: a four one-off now discards two cards chosen AT RANDOM from the opponent's hand. Worst case it matches the old effect; usually it takes something that actually hurts. Ships as a beta for the Spades season, announced on the home page, with a community poll toward the end of 2026. It is one of the four trials covered by the Patreon post the announcement links to. Rules: - Two cards are discarded at random; fewer than two in hand discards whatever is there, so a four is now always playable (the "opponent has no cards in hand" block is gone). - There is no player input step anymore. A four resolves immediately, like an ace or a six -- no dialog, no pause, no second move. Notable details: - resolve/execute.js moves `case 4` out of the deferred 3/4/5/7 group, which set `phase: oneOff.rank` and returned early, into the immediate group. The new resolve/four.js uses _.sampleSize, which returns *up to* n elements and so expresses the short-hand rule without branching. - The one-off validators keep an explicit `case 4: return exits.success()`. Deleting the arm would drop fours through to the default, which rejects any rank not listed. This also retires a latent bug: one-off/validate.js threw a plain Error here rather than BadRequestError, so the path returned 500. - get-log.js branches on whether the row carries discardedCards. Pre-3.0.0 rows deferred the discard to a separate resolveFour row and carry none, so they keep narrating the old rule instead of printing undefined. - processFours lacked the null guard its processFives twin has. A four against an empty hand sends discardedCards as null (create-socket-events converts empty arrays), which would have crashed the reveal animation. Backward compatibility -- stored games are re-rendered per frame through unpackGamestate and createSocketEvents, never re-executed, so the write path is safe to delete but the read path is not. Retained and commented as legacy: GamePhase.RESOLVING_FOUR (validate-gamestate hard-throws on unknown phases), the getActivePlayerPNum case (its default throws), MoveType/SocketEvent RESOLVE_FOUR, the get-log RESOLVE_FOUR arm, the inGameEvents RESOLVE_FOUR case (that switch has no default, so a missing case leaves the board on the prior frame), and FourDialog with its store computeds and i18n. Deleted: moves/resolve-four/, the hasValidMoveBody arm (its default now answers a clean 400 for stale clients), the AI generator arm and its three fixtures, and cy.discardOpponent. RESOLVE_FOUR is added to get-legal-moves' disallowedMoveTypes -- it indexes helpers by enum value, so keeping the key while removing the directory would TypeError on every AI move. load-fixture-gamestate gains optional phase/turn/resolved/oneOff overrides so the retained legacy path stays testable; without them no test could stage a phase-4 state once the write path is gone. Rules version bumped to 3.0.0. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JnmfJeHNxebpfcDzSN8PqV --- .../game/load-fixture-gamestate.js | 14 +- api/helpers/game-states/ai/get-legal-moves.js | 4 +- .../ai/get-move-bodies-for-move-type.js | 24 -- api/helpers/game-states/get-log.js | 13 +- .../game-states/moves/one-off/validate.js | 6 +- .../game-states/moves/resolve-four/execute.js | 73 ---- .../moves/resolve-four/validate.js | 67 ---- .../game-states/moves/resolve/execute.js | 4 +- api/helpers/game-states/moves/resolve/four.js | 39 +++ .../moves/seven-one-off/validate.js | 5 +- api/models/GameStateRow.js | 2 +- api/policies/hasValidMoveBody.js | 18 +- docs/game-rules.md | 2 +- docs/game-state-api.md | 4 +- src/plugins/sockets/inGameEvents.js | 11 + src/routes/game/GameView.vue | 1 - .../game/components/dialogs/GameDialogs.vue | 12 +- .../dialogs/components/FourDialog.vue | 5 + .../data/announcementData.js | 8 +- src/stores/game.js | 8 +- src/translations/de.json | 22 +- src/translations/en.json | 22 +- src/translations/es.json | 22 +- src/translations/fr.json | 22 +- src/translations/ukr.json | 22 +- tests/e2e/fixtures/snackbarError.js | 1 - tests/e2e/specs/in-game/handLimit.spec.js | 3 +- tests/e2e/specs/in-game/layout.spec.js | 43 --- .../specs/in-game/one-offs/4_fours.spec.js | 327 ++++++------------ .../one-offs/7_sevens/player_sevens.spec.js | 21 +- tests/e2e/specs/in-game/reconnecting.spec.js | 25 +- .../specs/playground/videoPlayground.spec.js | 7 +- tests/e2e/support/commands.js | 27 -- .../ai/resolvingFour1CanDiscardTwoCards.js | 93 ----- .../ai/resolvingFour2HasOnlyTwoCards.js | 79 ----- .../ai/resolvingFourPhase3HasOnlyOneCard.js | 77 ----- tests/unit/specs/sails/ai.spec.js | 6 - utils/rulesVersion.js | 3 +- 38 files changed, 283 insertions(+), 859 deletions(-) delete mode 100644 api/helpers/game-states/moves/resolve-four/execute.js delete mode 100644 api/helpers/game-states/moves/resolve-four/validate.js create mode 100644 api/helpers/game-states/moves/resolve/four.js delete mode 100644 tests/unit/fixtures/gameStates/ai/resolvingFour1CanDiscardTwoCards.js delete mode 100644 tests/unit/fixtures/gameStates/ai/resolvingFour2HasOnlyTwoCards.js delete mode 100644 tests/unit/fixtures/gameStates/ai/resolvingFourPhase3HasOnlyOneCard.js diff --git a/api/controllers/game/load-fixture-gamestate.js b/api/controllers/game/load-fixture-gamestate.js index 0c757005c..cd6b710ec 100644 --- a/api/controllers/game/load-fixture-gamestate.js +++ b/api/controllers/game/load-fixture-gamestate.js @@ -22,6 +22,12 @@ module.exports = async function (req, res) { secondCard = null, scrap = [], deck, + // Overrides so tests can stage states no live move can produce anymore -- notably the + // legacy RESOLVING_FOUR phase, which stored games still hold and replay. + phase = GamePhase.MAIN, + turn = 0, + resolved = null, + oneOff = null, } = req.body; const allFixtureCards = new Set(Object.values(req.body).flat() @@ -58,15 +64,15 @@ module.exports = async function (req, res) { scrap: populatedScrap, twos: [], discardedCards: [], - oneOff: null, + oneOff, oneOffTarget: null, - resolved: null, + resolved, playedCard: null, gameId: game.id, playedBy: 0, moveType: MoveType.LOADFIXTURE, - turn: 0, - phase: GamePhase.MAIN, + turn, + phase, targetCard: null, }; diff --git a/api/helpers/game-states/ai/get-legal-moves.js b/api/helpers/game-states/ai/get-legal-moves.js index 1c458d501..543e02f68 100644 --- a/api/helpers/game-states/ai/get-legal-moves.js +++ b/api/helpers/game-states/ai/get-legal-moves.js @@ -24,7 +24,9 @@ module.exports = { }, sync: true, fn: ({ currentState, playedBy, priorStates }, exits) => { - const disallowedMoveTypes = [ 'DEAL', 'CONCEDE', 'STALEMATE_REQUEST', 'STALEMATE_ACCEPT', 'FIZZLE', 'LOADFIXTURE' ]; + // RESOLVE_FOUR is retained in the enum so stored games still render, but its move helper is + // gone -- fours resolve immediately now -- so it must not be looked up here. + const disallowedMoveTypes = [ 'DEAL', 'CONCEDE', 'STALEMATE_REQUEST', 'STALEMATE_ACCEPT', 'FIZZLE', 'LOADFIXTURE', 'RESOLVE_FOUR' ]; const moveTypes = Object.values(_.omit(MoveType, disallowedMoveTypes)); const res = []; for (let moveType of moveTypes) { 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 04b572dc4..e97820f77 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 @@ -130,30 +130,6 @@ module.exports = { res = scrap.map((card) => ({ moveType, playedBy, cardId: card.id })); break; - case MoveType.RESOLVE_FOUR: { - - if (playerHand.length === 1) { - res = [ - { - moveType, - playedBy, - cardId1: playerHand[0].id, - }, - ]; - } - - for (let i = 0; i < playerHand.length; i++) { - for (let j = i + 1; j < playerHand.length; j++) { - res.push({ - moveType, - playedBy, - cardId1: playerHand[i].id, - cardId2: playerHand[j].id - }); - } - } - break; - } case MoveType.RESOLVE_FIVE: if (!playerHand.length) { diff --git a/api/helpers/game-states/get-log.js b/api/helpers/game-states/get-log.js index 5bd5134a9..e9f86201a 100644 --- a/api/helpers/game-states/get-log.js +++ b/api/helpers/game-states/get-log.js @@ -90,8 +90,17 @@ module.exports = { return `The ${resolvedCardName} resolves; the ${targetCardName} is scrapped.`; case 3: return `The ${resolvedCardName} one-off resolves; ${opponent} will draw one card of their choice from the Scrap pile.`; - case 4: - return `The ${resolvedCardName} one-off resolves; ${player} must discard two cards.`; + case 4: { + // Legacy rows (rules < 3.0.0) deferred the discard to a separate resolveFour + // move, so the cards are named on that row and this one carries none. + if (!discardedCards?.length) { + return `The ${resolvedCardName} one-off resolves; ${player} must discard two cards.`; + } + const discardNames = discardedCards.map(getFullCardName); + const discardList = + discardNames.length > 1 ? `the ${discardNames[0]} and the ${discardNames[1]}` : `the ${discardNames[0]}`; + return `The ${resolvedCardName} one-off resolves, discarding ${discardList} at random from ${player}'s hand.`; + } case 5: return `The ${resolvedCardName} one-off resolves; ${opponent} must discard 1 card, and will draw up to 3.`; case 6: diff --git a/api/helpers/game-states/moves/one-off/validate.js b/api/helpers/game-states/moves/one-off/validate.js index e1cb41517..4c2d9de29 100644 --- a/api/helpers/game-states/moves/one-off/validate.js +++ b/api/helpers/game-states/moves/one-off/validate.js @@ -125,11 +125,9 @@ module.exports = { } return exits.success(); - // Four requires opponent to have cards in hand + // Fours discard at random, so an empty hand just discards nothing. The arm has to + // stay: the default below rejects any rank that isn't explicitly allowed here. case 4: - if (!opponent.hand.length) { - throw new Error('game.snackbar.oneOffs.four.opponentHasNoCards'); - } return exits.success(); // Five and sevens require cards in deck diff --git a/api/helpers/game-states/moves/resolve-four/execute.js b/api/helpers/game-states/moves/resolve-four/execute.js deleted file mode 100644 index 9a5a8f723..000000000 --- a/api/helpers/game-states/moves/resolve-four/execute.js +++ /dev/null @@ -1,73 +0,0 @@ -const GamePhase = require('../../../../../utils/GamePhase.json'); - -module.exports = { - friendlyName: 'Resolve four', - - description: 'Returns new GameState resulting from requested resolve four move', - - inputs: { - currentState: { - type: 'ref', - description: 'The latest GameState before the requesting player resolves a four card', - required: true, - }, - /** - * @param { Object } requestedMove - Object describing the request to resolve a four - * @param { String } requestedMove.cardId1 - First card to be discarded - * @param { String } requestedMove.cardId2 - Second card to be discarded - * @param { MoveType.RESOLVE_FOUR } requestedMove.moveType - Specifies that this is a Resolve Four move - */ - requestedMove: { - type: 'ref', - description: 'The move being requested. Specifies which player is asking to resolve a four', - }, - playedBy: { - type: 'number', - description: 'Player number of player requesting move.', - }, - priorStates: { - type: 'ref', - description: 'List of packed gameStateRows for this game\'s prior states', - required: true, - } - }, - sync: true, // synchronous helper - fn: ({ currentState, requestedMove, playedBy }, exits) => { - const { cardId1, cardId2 } = requestedMove; - let result = _.cloneDeep(currentState); - - const player = playedBy ? result.p1 : result.p0; - const discardedCards = []; - - const cardIndex1 = player.hand.findIndex(({ id }) => id === cardId1); - - discardedCards.push(player.hand[cardIndex1]); - result.scrap.push(result.oneOff, ...player.hand.splice(cardIndex1, 1)); - - const cardIndex2 = cardId2 ? player.hand.findIndex(({ id }) => id === cardId2) : -1; - - if (cardIndex2 !== -1) { - discardedCards.push(player.hand[cardIndex2]); - result.scrap.push(...player.hand.splice(cardIndex2, 1)); - } - - // active player is the opposite of the one who discarded - const activePlayer = playedBy ? result.p0 : result.p1; - const playerMustDiscard = activePlayer.hand.length > 8; - - result = { - ...result, - ...requestedMove, - phase: playerMustDiscard ? GamePhase.DISCARDING_TO_HAND_LIMIT : GamePhase.MAIN, - turn: playerMustDiscard ? result.turn : result.turn + 1, - playedBy, - playedCard: null, - targetCard: null, - discardedCards, - resolved: result.oneOff, - oneOff: null, - }; - - return exits.success(result); - }, -}; diff --git a/api/helpers/game-states/moves/resolve-four/validate.js b/api/helpers/game-states/moves/resolve-four/validate.js deleted file mode 100644 index 63a7cfa89..000000000 --- a/api/helpers/game-states/moves/resolve-four/validate.js +++ /dev/null @@ -1,67 +0,0 @@ -const GamePhase = require('../../../../../utils/GamePhase.json'); -const BadRequestError = require('../../../../errors/badRequestError'); - -module.exports = { - friendlyName: 'Validate request to resolve a four', - - description: 'Verifies whether a request to resolve a four is legal, throwing explanatory error if not.', - - inputs: { - currentState: { - type: 'ref', - descriptions: 'Object containing the current game state', - required: true, - }, - /** - * @param { Object } requestedMove - Object describing the request to resolve a four - * @param { String } requestedMove.cardId1 - First card to be discarded - * @param { String } requestedMove.cardId2 - Second card to be discarded - * @param { MoveType.RESOLVE_FOUR } requestedMove.moveType - Specifies that this is a Resolve Four move - */ - requestedMove: { - type: 'ref', - description: 'Object containing data needed for current move', - required: true, - }, - playedBy: { - type: 'number', - description: 'Player number of player requesting move', - required: true, - }, - priorStates: { - type: 'ref', - description: 'List of packed gameStateRows for this game\'s prior states', - required: true, - } - }, - sync: true, - fn: ({ requestedMove, currentState, playedBy }, exits) => { - try { - if (currentState.turn % 2 === playedBy) { - throw new BadRequestError('game.snackbar.oneOffs.four.notYourTurn'); - } - - if (currentState.phase !== GamePhase.RESOLVING_FOUR) { - throw new BadRequestError('game.snackbar.oneOffs.four.notResolvingFourPhase'); - } - - const player = playedBy ? currentState.p1 : currentState.p0; - const { cardId1, cardId2 } = requestedMove; - - const selectedCard1 = player.hand.find(card => card.id === cardId1); - const selectedCard2 = player.hand.find(card => card.id === cardId2); - - if ((requestedMove.cardId1 && !selectedCard1) || (requestedMove.cardId2 && !selectedCard2)) { - throw new BadRequestError('game.snackbar.oneOffs.four.mustSelectCards'); - } - - if (!selectedCard2 && player.hand.length > 1) { - throw new BadRequestError('game.snackbar.oneOffs.four.mustSelectCards'); - } - - return exits.success(); - } catch (err) { - return exits.error(err); - } - }, -}; diff --git a/api/helpers/game-states/moves/resolve/execute.js b/api/helpers/game-states/moves/resolve/execute.js index 728b0eedb..558af2e19 100644 --- a/api/helpers/game-states/moves/resolve/execute.js +++ b/api/helpers/game-states/moves/resolve/execute.js @@ -79,8 +79,10 @@ module.exports = { case 6: result = sails.helpers.gameStates.moves.resolve.six(result); break; - case 3: case 4: + result = sails.helpers.gameStates.moves.resolve.four(result, playedBy); + break; + case 3: case 5: case 7: // These one-offs require an additional user input after resolving diff --git a/api/helpers/game-states/moves/resolve/four.js b/api/helpers/game-states/moves/resolve/four.js new file mode 100644 index 000000000..88670ea9f --- /dev/null +++ b/api/helpers/game-states/moves/resolve/four.js @@ -0,0 +1,39 @@ +module.exports = { + friendlyName: 'Resolve Four One-Off', + + description: + 'Returns new GameState resulting from resolving a four, which discards two cards at random from the ' + + 'hand of the player resolving it. A hand of fewer than two cards discards whatever it holds.', + + inputs: { + currentState: { + type: 'ref', + description: 'The latest GameState before the four discards its two cards', + required: true, + }, + playedBy: { + type: 'number', + description: 'Player number of player requesting move.', + } + }, + + sync: true, // synchronous helper + fn: ({ currentState, playedBy }, exits) => { + const result = _.cloneDeep(currentState); + + // The player resolving the one-off is the opposite of the one who played it, so `playedBy` + // identifies the player whose hand is discarded -- not the four's caster. + const victim = playedBy ? result.p1 : result.p0; + + // sampleSize returns *up to* two cards, so a short hand simply discards everything it has + const discardedCards = _.sampleSize(victim.hand, 2); + const discardedIds = new Set(discardedCards.map(({ id }) => id)); + + // Filter by id rather than splicing -- sampleSize returns the cards, not their positions + victim.hand = victim.hand.filter(({ id }) => !discardedIds.has(id)); + result.scrap.push(...discardedCards); + result.discardedCards = discardedCards; + + 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 219b3006e..616aec686 100644 --- a/api/helpers/game-states/moves/seven-one-off/validate.js +++ b/api/helpers/game-states/moves/seven-one-off/validate.js @@ -114,10 +114,9 @@ module.exports = { return exits.success(); // Four requires opponent to have cards in hand + // Fours discard at random, so an empty hand just discards nothing. The arm has to + // stay: the default below rejects any rank that isn't explicitly allowed here. case 4: - if (!opponent.hand.length) { - throw new BadRequestError('game.snackbar.oneOffs.four.opponentHasNoCards'); - } return exits.success(); // Five and sevens require cards in deck diff --git a/api/models/GameStateRow.js b/api/models/GameStateRow.js index f04539b53..1e77c3e0f 100644 --- a/api/models/GameStateRow.js +++ b/api/models/GameStateRow.js @@ -66,7 +66,7 @@ module.exports = { * 1 - MAIN * 2 - COUNTERING * 3 - RESOLVING_THREE - * 4 - RESOLVING_FOUR + * 4 - RESOLVING_FOUR (legacy; rules < 3.0.0 only -- fours resolve immediately now) * 5 - RESOLVING_FIVE * 7 - RESOLVING_SEVEN */ diff --git a/api/policies/hasValidMoveBody.js b/api/policies/hasValidMoveBody.js index 69a3a0ed4..6fcf808ce 100644 --- a/api/policies/hasValidMoveBody.js +++ b/api/policies/hasValidMoveBody.js @@ -10,7 +10,7 @@ const DeckIds = require('../../utils/DeckIds.json'); * */ module.exports = function (req, res, next) { - const { moveType, cardId, cardId1, cardId2, targetId, targetType } = req.body; + const { moveType, cardId, targetId, targetType } = req.body; switch (moveType) { // These moves require no extra data @@ -43,22 +43,6 @@ module.exports = function (req, res, next) { } return next(); - // Requires `cardId1` and optionally accepts `cardId2` - case MoveType.RESOLVE_FOUR: - { - if (!cardId1) { - return res.badRequest({ message: 'Must specify a card' }); - } - - if (!DeckIds.includes(cardId1)) { - return res.badRequest({ message: `${cardId1} is not a valid cardId` }); - } - - if (cardId2 && !DeckIds.includes(cardId2)) { - return res.badRequest({ message: `${cardId2} is not a valid cardId` }); - } - } - return next(); // Requires a non-empty `discardedCards` array of valid card IDs case MoveType.DISCARD_TO_HAND_LIMIT: diff --git a/docs/game-rules.md b/docs/game-rules.md index ca77309b3..bf389741b 100644 --- a/docs/game-rules.md +++ b/docs/game-rules.md @@ -44,7 +44,7 @@ On your turn you must make exactly one of the following actions below. 1. Counter **target One-Off** (play this when an opponent plays a **One-Off** effect to stop it from happening) 2. Scrap **target Royal or Glasses Eight** - **Three**: Choose one card from the **Scrap Pile** and put it in your hand. -- **Four**: Your opponent discards two cards of their choice from their hand to the **Scrap Pile** +- **Four**: Two cards are discarded at random from your opponent's hand to the **Scrap Pile**. If they hold fewer than two cards, they discard whatever they have. - **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. diff --git a/docs/game-state-api.md b/docs/game-state-api.md index 26cb0efe5..6075e9798 100644 --- a/docs/game-state-api.md +++ b/docs/game-state-api.md @@ -27,7 +27,7 @@ Enum describing the phase of a turn the game is currently in. Used to validate w * counter * resolve * resolvingThree \- Picking card from scrap. Only legal move is resolveThree -* resolvingFour \- Choosing cards to discard due to opponent four. Only legal move is resolveFour +* resolvingFour \- **Legacy (rules \< 3.0.0).** Choosing cards to discard due to opponent four; the only legal move was resolveFour. As of rules 3.0.0 a four discards two cards at random the moment it resolves, so no new game enters this phase. The value is retained because stored games still hold it and are re-rendered on replay. * resolvingFive \- Choosing card to discard before drawing. Only legal move is resolveFive * resolvingSeven \- Picking one of the top cards from the deck. Must play a seven move next. Legal next moves: * sevenPoints @@ -54,7 +54,7 @@ Enum designating which kind of move was made. * counter * resolve * resolveThree (picking a card from the scrap) -* resolveFour (discarding from hand) +* resolveFour (discarding from hand) \- **Legacy (rules \< 3.0.0);** no longer accepted, retained so stored games still render * sevenPoints * sevenScuttle * sevenFaceCard diff --git a/src/plugins/sockets/inGameEvents.js b/src/plugins/sockets/inGameEvents.js index 52f126805..82626b50e 100644 --- a/src/plugins/sockets/inGameEvents.js +++ b/src/plugins/sockets/inGameEvents.js @@ -63,6 +63,15 @@ export async function handleInGameEvents(evData, newRoute = null) { break; case SocketEvent.RESOLVE: switch (evData.game.lastEvent?.oneOff?.rank) { + // playedBy on a resolve is the player who resolved it -- the one whose hand was + // discarded -- so anyone else watching is the caster, who needs the reveal + case 4: + if (evData.playedBy !== gameStore.myPNum) { + await gameStore.processFours(evData.discardedCards, evData.game); + } else { + gameStore.updateGame(evData.game); + } + break; case 7: await gameStore.processSevens(evData.game); break; @@ -80,6 +89,8 @@ export async function handleInGameEvents(evData, newRoute = null) { case SocketEvent.RESOLVE_THREE: await gameStore.processThrees(evData.chosenCard, evData.game); break; + // Legacy: fours resolve immediately as of rules 3.0.0, but stored games still step + // through a separate resolveFour frame, and this switch has no default to fall back on case SocketEvent.RESOLVE_FOUR: if (evData.playedBy !== gameStore.myPNum) { await gameStore.processFours(evData.discardedCards, evData.game); diff --git a/src/routes/game/GameView.vue b/src/routes/game/GameView.vue index f9d4ae598..ab7f0c322 100644 --- a/src/routes/game/GameView.vue +++ b/src/routes/game/GameView.vue @@ -466,7 +466,6 @@ export default { targeting: false, targetingMoveName: null, targetingMoveDisplayName: null, - showFourDialog: false, topCardIsSelected: false, secondCardIsSelected: false, showHistoryDrawer: false, diff --git a/src/routes/game/components/dialogs/GameDialogs.vue b/src/routes/game/components/dialogs/GameDialogs.vue index ef63e3daf..e13211967 100644 --- a/src/routes/game/components/dialogs/GameDialogs.vue +++ b/src/routes/game/components/dialogs/GameDialogs.vue @@ -154,13 +154,17 @@ export default { this.gameStore.requestCounter(twoId).then(this.clearSelection) .catch(this.handleError); }, + // Legacy: no live game can enter the four-discard phase as of rules 3.0.0, but the dialog + // is still rendered when replaying a stored game that did, so the handler stays wired. discard(cardIds) { const [ cardId1 ] = cardIds; const cardId2 = cardIds.length > 1 ? cardIds[1] : null; - this.gameStore.requestDiscard({ - cardId1, - cardId2, - }); + this.gameStore + .requestDiscard({ + cardId1, + cardId2, + }) + .catch(this.handleError); }, discardToHandLimit(cardIds) { this.gameStore.requestDiscardToHandLimit(cardIds); diff --git a/src/routes/game/components/dialogs/components/FourDialog.vue b/src/routes/game/components/dialogs/components/FourDialog.vue index 429e0554c..57d29d9c6 100644 --- a/src/routes/game/components/dialogs/components/FourDialog.vue +++ b/src/routes/game/components/dialogs/components/FourDialog.vue @@ -1,3 +1,8 @@ +