Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions api/helpers/game-states/ai/get-move-bodies-for-move-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
}
}
Expand Down Expand Up @@ -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,
});
}
Comment thread
itsalaidbacklife marked this conversation as resolved.

if (opponentPoint.attachments.length) {
res.push({
moveType,
Expand Down
2 changes: 1 addition & 1 deletion api/helpers/game-states/get-log.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
30 changes: 30 additions & 0 deletions api/helpers/game-states/is-top-jack.js
Original file line number Diff line number Diff line change
@@ -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));
},
};
10 changes: 10 additions & 0 deletions api/helpers/game-states/moves/one-off/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions api/helpers/game-states/moves/resolve/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
itsalaidbacklife marked this conversation as resolved.
result.phase = playerMustDiscard ? GamePhase.DISCARDING_TO_HAND_LIMIT : GamePhase.MAIN;
result.turn = playerMustDiscard ? result.turn : result.turn + 1;
return exits.success(result);
Expand Down
39 changes: 25 additions & 14 deletions api/helpers/game-states/moves/resolve/nine.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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);
},
};
8 changes: 8 additions & 0 deletions api/helpers/game-states/moves/seven-one-off/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 1 addition & 8 deletions api/helpers/game-states/unpack-gamestate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
Expand Down
2 changes: 1 addition & 1 deletion docs/game-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/game-state-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 9 additions & 4 deletions src/plugins/sockets/inGameEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading
Loading