Skip to content
Open
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
2 changes: 2 additions & 0 deletions api/controllers/game/load-fixture-gamestate.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ module.exports = async function (req, res) {
discardedCards: [],
oneOff: null,
oneOffTarget: null,
oneOffTargetTwo: null,
resolved: null,
playedCard: null,
targetCardTwo: null,
gameId: game.id,
playedBy: 0,
moveType: MoveType.LOADFIXTURE,
Expand Down
142 changes: 78 additions & 64 deletions api/helpers/game-states/ai/get-move-bodies-for-move-type.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,70 @@
const MoveType = require('../../../../utils/MoveType');
const TargetType = require('../../../../utils/TargetType');

/**
* Legal targets for a two: royals, glasses eights, and the top jack of each point card.
* Point cards themselves are never legal targets for a two.
*/
function getTwoTargets(opponentPoints, opponentFaceCards) {
const faceCardTargets = opponentFaceCards.map(({ id }) => ({
targetId: id,
targetType: TargetType.faceCard,
}));
const jackTargets = opponentPoints
.filter(({ attachments }) => attachments.length)
.map(({ attachments }) => ({ targetId: attachments.at(-1).id, targetType: TargetType.jack }));

return [ ...faceCardTargets, ...jackTargets ];
}

/**
* Every legal pair of targets for a nine. Nines return two cards, so they need two distinct
* targets, and any queen blocks them outright (a queen leaves itself the only legal target).
* Only the top jack of each point card is targetable.
*/
function getNineTargetPairs(opponentPoints, opponentFaceCards) {
if (opponentFaceCards.some(({ rank }) => rank === 12)) {
return [];
}

const targets = [
...opponentPoints.map(({ id }) => ({ targetId: id, targetType: TargetType.point })),
...getTwoTargets(opponentPoints, opponentFaceCards),
];

const pairs = [];
for (let i = 0; i < targets.length; i++) {
for (let j = i + 1; j < targets.length; j++) {
pairs.push([ targets[i], targets[j] ]);
}
}

return pairs;
}

/** Builds the move bodies for playing one targeted one-off card, given its rank's targeting rules */
function getTargetedOneOffBodies({ moveType, playedBy, card, opponentPoints, opponentFaceCards }) {
if (card.rank === 9) {
return getNineTargetPairs(opponentPoints, opponentFaceCards).map(([ targetOne, targetTwo ]) => ({
moveType,
playedBy,
cardId: card.id,
targetId: targetOne.targetId,
targetType: targetOne.targetType,
targetIdTwo: targetTwo.targetId,
targetTypeTwo: targetTwo.targetType,
}));
}

return getTwoTargets(opponentPoints, opponentFaceCards).map(({ targetId, targetType }) => ({
moveType,
playedBy,
cardId: card.id,
targetId,
targetType,
}));
}

module.exports = {
friendlyName: 'Get move bodies for move type',

Expand Down Expand Up @@ -84,38 +148,13 @@ 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: 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,
targetType: TargetType.jack,
});
}
}
res.push(...getTargetedOneOffBodies({
moveType,
playedBy,
card: twoOrNine,
opponentPoints,
opponentFaceCards,
}));
}
break;
}
Expand Down Expand Up @@ -236,38 +275,13 @@ module.exports = {
}

for (let targetedOneOff of targetedOneOffs) {
for (let opponentFaceCard of opponentFaceCards) {
res.push({
moveType,
playedBy,
cardId: targetedOneOff.id,
targetId: opponentFaceCard.id,
targetType: TargetType.faceCard,
});
}

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,
playedBy,
cardId: targetedOneOff.id,
targetId: opponentPoint.attachments.at(-1).id,
targetType: TargetType.jack
});
}
}
res.push(...getTargetedOneOffBodies({
moveType,
playedBy,
card: targetedOneOff,
opponentPoints,
opponentFaceCards,
}));
}

break;
Expand Down
4 changes: 4 additions & 0 deletions api/helpers/game-states/create-socket-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ module.exports = {
'playedBy',
'playedCard',
'targetCard',
'targetCardTwo',
'discardedCards',
'resolved',
]);
Expand Down Expand Up @@ -148,6 +149,8 @@ module.exports = {
oneOff: gameState.oneOff,
oneOffTarget: gameState.oneOffTarget,
oneOffTargetType: gameState.oneOffTargetType,
oneOffTargetTwo: gameState.oneOffTargetTwo,
oneOffTargetTwoType: gameState.oneOffTargetTwoType,
lastEvent: {
change: gameState.moveType,
pNum,
Expand All @@ -172,6 +175,7 @@ module.exports = {
// Conditionally included properties if truthy
...(gameState.playedCard && { playedCard: gameState.playedCard }),
...(gameState.targetCard && { targetCard: gameState.targetCard }),
...(gameState.targetCardTwo && { targetCardTwo: gameState.targetCardTwo }),
...(gameState.resolved && { oneOff: gameState.resolved }),
...(chosenCard && { chosenCard }),
...(discardedCards && { discardedCards }),
Expand Down
2 changes: 2 additions & 0 deletions api/helpers/game-states/deal-cards.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ module.exports = {
playedBy: 1, // p1 "deals"
playedCard: null,
targetCard: null,
targetCardTwo: null,
discardedCards: [],
oneOff: null,
oneOffTarget: null,
oneOffTargetTwo: null,
resolving: null,
};

Expand Down
13 changes: 9 additions & 4 deletions api/helpers/game-states/get-log.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = {

fn: function ({ game }, exits) {
const getMessage = (row, i) => {
const { moveType, playedCard, targetCard, resolved, deck, discardedCards } = row;
const { moveType, playedCard, targetCard, targetCardTwo, resolved, deck, discardedCards } = row;
const { convertStrToCard } = sails.helpers.gameStates;

const getFullCardName = (card) => {
Expand All @@ -31,6 +31,7 @@ module.exports = {

const playedCardName = playedCard ? getFullCardName(playedCard) : null;
const targetCardName = targetCard ? getFullCardName(targetCard) : null;
const targetCardTwoName = targetCardTwo ? getFullCardName(targetCardTwo) : null;
const resolvedCardName = resolved ? getFullCardName(resolved) : null;

const getResolveFiveMessage = () => {
Expand Down Expand Up @@ -67,7 +68,9 @@ module.exports = {
let log = `${player} played the ${playedCardName} as a one-off to
${gameText.moves.effects[playedCardObj.rank]}`;
if (targetCardName) {
log += `, targeting the ${targetCardName}.`;
log += `, targeting the ${targetCardName}${
targetCardTwoName ? ` and the ${targetCardTwoName}` : ''
}.`;
} else {
log += '.';
}
Expand Down Expand Up @@ -108,7 +111,9 @@ module.exports = {
)} and ${getFullCardName(deck[1])}.`;

case 9:
return `The ${resolvedCardName} one-off resolves, putting the ${targetCardName} on top of the deck.`;
return `The ${resolvedCardName} one-off resolves, returning the ${targetCardName}${
targetCardTwoName ? ` and the ${targetCardTwoName}` : ''
} to ${player}'s hand.`;
}
break;

Expand Down Expand Up @@ -170,7 +175,7 @@ module.exports = {
case MoveType.SEVEN_TARGETED_ONE_OFF:
return `${player} played the ${playedCardName} from the top of the deck as a one-off to ${
gameText.moves.effects[playedCard.rank]
}, targeting the ${targetCardName}.`;
}, targeting the ${targetCardName}${targetCardTwoName ? ` and the ${targetCardTwoName}` : ''}.`;

case MoveType.PASS:
return `${player} passed.`;
Expand Down
9 changes: 9 additions & 0 deletions api/helpers/game-states/moves/one-off/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ module.exports = {
opponent,
) : null;

// Nines return two cards, so they carry a second target through countering
const targetCardTwo = requestedMove.targetIdTwo ? sails.helpers.gameStates.findTargetCard(
requestedMove.targetIdTwo,
requestedMove.targetTypeTwo,
opponent,
) : null;

result = {
...result,
...requestedMove,
Expand All @@ -63,6 +70,8 @@ module.exports = {
resolved: null,
oneOffTarget: targetCard,
oneOffTargetType: requestedMove.targetType ?? null,
oneOffTargetTwo: targetCardTwo,
oneOffTargetTwoType: requestedMove.targetTypeTwo ?? null,
};

return exits.success(result);
Expand Down
18 changes: 14 additions & 4 deletions api/helpers/game-states/moves/one-off/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ module.exports = {
* @param { String } requestedMove.cardId - Card Played for points
* @param { String } [ requestedMove.targetId ] - OPTIONAL target of one-off used for 2's and 9's
* @param { 'point' | 'faceCard' | 'jack' } [ requestedMove.targetType ] - OPTIONAL where one-off target is located
* @param { String } [ requestedMove.targetIdTwo ] - OPTIONAL second target, required for 9's
* @param { 'point' | 'faceCard' | 'jack' } [ requestedMove.targetTypeTwo ] - OPTIONAL second target's location
*/
requestedMove: {
type: 'ref',
Expand Down Expand Up @@ -69,9 +71,8 @@ module.exports = {
case 6:
return exits.success();

// 2 and 9 require legal target
case 2:
case 9: {
// Two requires one legal target
case 2: {
const targetCard = sails.helpers.gameStates.findTargetCard(
requestedMove.targetId,
requestedMove.targetType,
Expand All @@ -82,7 +83,7 @@ module.exports = {
throw new BadRequestError(`Can't find the ${requestedMove.targetId} on opponent's board`);
}

if (playedCard.rank === 2 && ![ 'faceCard', 'jack' ].includes(requestedMove.targetType)) {
if (![ 'faceCard', 'jack' ].includes(requestedMove.targetType)) {
throw new BadRequestError('Twos can only target royals or glasses');
}

Expand Down Expand Up @@ -118,6 +119,15 @@ module.exports = {
}
}

// Nine requires two legal targets, and is blocked by any queen
case 9: {
const nineError = sails.helpers.gameStates.validateNineTargets(requestedMove, opponent);
if (nineError) {
throw new BadRequestError(nineError);
}
return exits.success();
}

// Three requires non-three card(s) in scrap
case 3:
if (currentState.scrap.every((card) => card.rank === 3)) {
Expand Down
7 changes: 6 additions & 1 deletion api/helpers/game-states/moves/resolve/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,16 @@ module.exports = {
const activePlayer = result.turn % 2 === 0 ? result.p0 : result.p1;
const playerMustDiscard = activePlayer.hand.length > 8;

// If one-off fizzles, make no other changes
// If one-off fizzles, make no other changes to the board
if (fizzles) {
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.oneOffTargetTwo = null;
result.oneOffTargetTwoType = null;
result.phase = playerMustDiscard ? GamePhase.DISCARDING_TO_HAND_LIMIT : GamePhase.MAIN;
result.turn = playerMustDiscard ? result.turn : result.turn + 1;
return exits.success(result);
Expand Down Expand Up @@ -102,8 +104,11 @@ module.exports = {
...result,
oneOff: null,
targetCard: result.oneOffTarget,
targetCardTwo: result.oneOffTargetTwo ?? null,
oneOffTarget: null,
oneOffTargetType: null,
oneOffTargetTwo: null,
oneOffTargetTwoType: null,
phase: playerMustDiscard ? GamePhase.DISCARDING_TO_HAND_LIMIT : GamePhase.MAIN,
turn: playerMustDiscard ? result.turn : result.turn + 1,
};
Expand Down
Loading
Loading