Skip to content

Commit

Permalink
feat(thief): thief additional cards validation on game creation (#415)
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinezanardi authored Aug 26, 2023
1 parent b6c4ca9 commit b4bf9de
Show file tree
Hide file tree
Showing 89 changed files with 68,237 additions and 56,984 deletions.
1 change: 1 addition & 0 deletions config/eslint/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const OFF = "off";
const MAX_LENGTH_DEFAULT_CONFIG = {
code: MAX_LENGTH,
ignoreTemplateLiterals: true,
ignoreRegExpLiterals: true,
ignorePattern: "^import\\s.+\\sfrom\\s.+;$",
};
const BOOLEAN_PREFIXES = ["is", "was", "are", "were", "should", "has", "can", "does", "do", "did", "must"];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { ApiPropertyOptions } from "@nestjs/swagger";
import { roles } from "../../../role/constants/role.constant";
import { ROLE_NAMES } from "../../../role/enums/role.enum";
import type { GameAdditionalCard } from "../../schemas/game-additional-card/game-additional-card.schema";

const gameAdditionalCardsThiefRoleNames: Readonly<ROLE_NAMES[]> = Object.freeze(roles.filter(({ minInGame, name }) => name !== ROLE_NAMES.THIEF && minInGame === undefined).map(({ name }) => name));

const gameAdditionalCardFieldsSpecs: Readonly<Record<keyof GameAdditionalCard, ApiPropertyOptions>> = Object.freeze({
_id: { required: true },
roleName: {
Expand All @@ -24,7 +27,7 @@ const gameAdditionalCardApiProperties: Readonly<Record<keyof GameAdditionalCard,
...gameAdditionalCardFieldsSpecs._id,
},
roleName: {
description: "Game additional card role name",
description: `Game additional card role name. If \`recipient\` is \`${ROLE_NAMES.THIEF}\`, possible values are : ${gameAdditionalCardsThiefRoleNames.toString()}`,
...gameAdditionalCardFieldsSpecs.roleName,
},
recipient: {
Expand All @@ -37,4 +40,8 @@ const gameAdditionalCardApiProperties: Readonly<Record<keyof GameAdditionalCard,
},
});

export { gameAdditionalCardFieldsSpecs, gameAdditionalCardApiProperties };
export {
gameAdditionalCardFieldsSpecs,
gameAdditionalCardApiProperties,
gameAdditionalCardsThiefRoleNames,
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ApiPropertyOptions } from "@nestjs/swagger";
import { ROLE_SIDES } from "../../../../role/enums/role.enum";
import { GAME_PLAY_ACTIONS } from "../../../enums/game-play.enum";
import { GAME_PLAY_ACTIONS, GAME_PLAY_CAUSES } from "../../../enums/game-play.enum";
import type { GameHistoryRecordPlay } from "../../../schemas/game-history-record/game-history-record-play/game-history-record-play.schema";

const gameHistoryRecordPlayFieldsSpecs = Object.freeze<Record<keyof GameHistoryRecordPlay, ApiPropertyOptions>>({
Expand All @@ -9,6 +9,10 @@ const gameHistoryRecordPlayFieldsSpecs = Object.freeze<Record<keyof GameHistoryR
enum: GAME_PLAY_ACTIONS,
},
source: { required: true },
cause: {
required: false,
enum: GAME_PLAY_CAUSES,
},
targets: { required: false },
votes: { required: false },
voting: { required: false },
Expand All @@ -29,6 +33,10 @@ const gameHistoryRecordPlayApiProperties = Object.freeze<Record<keyof GameHistor
description: "Play's source",
...gameHistoryRecordPlayFieldsSpecs.source,
},
cause: {
description: "Play's cause",
...gameHistoryRecordPlayFieldsSpecs.cause,
},
targets: {
description: "Players affected by the play.",
...gameHistoryRecordPlayFieldsSpecs.targets,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ApiPropertyOptions } from "@nestjs/swagger";
import type { GamePlaySource } from "../../schemas/game-play/game-play-source.schema";
import type { GamePlaySource } from "../../schemas/game-play/game-play-source/game-play-source.schema";
import { gameSourceValues } from "../game.constant";

const gamePlaySourceFieldsSpecs = Object.freeze<Record<keyof GamePlaySource, ApiPropertyOptions>>({
Expand Down
47 changes: 35 additions & 12 deletions src/modules/game/constants/game-play/game-play.constant.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import type { ApiPropertyOptions } from "@nestjs/swagger";
import { GAME_PLAY_ACTIONS } from "../../enums/game-play.enum";
import { createFakeGamePlaySheriffSettlesVotes } from "../../../../../tests/factories/game/schemas/game-play/game-play.schema.factory";
import { GAME_PLAY_ACTIONS, GAME_PLAY_CAUSES } from "../../enums/game-play.enum";
import { createGamePlayAllElectSheriff, createGamePlayAllVote, createGamePlayBigBadWolfEats, createGamePlayCharmedMeetEachOther, createGamePlayCupidCharms, createGamePlayDogWolfChoosesSide, createGamePlayFoxSniffs, createGamePlayGuardProtects, createGamePlayHunterShoots, createGamePlayLoversMeetEachOther, createGamePlayPiedPiperCharms, createGamePlayRavenMarks, createGamePlayScapegoatBansVoting, createGamePlaySeerLooks, createGamePlaySheriffDelegates, createGamePlayStutteringJudgeChoosesSign, createGamePlayThiefChoosesCard, createGamePlayThreeBrothersMeetEachOther, createGamePlayTwoSistersMeetEachOther, createGamePlayWerewolvesEat, createGamePlayWhiteWerewolfEats, createGamePlayWildChildChoosesModel, createGamePlayWitchUsesPotions } from "../../helpers/game-play/game-play.factory";
import type { GamePlay } from "../../schemas/game-play/game-play.schema";

const gamePlayApiProperties: Readonly<Record<keyof GamePlay, ApiPropertyOptions>> = Object.freeze({
source: { description: "Which role or group of people need to perform this action, with expected players to play" },
action: {
description: "What action need to be performed for this play",
example: GAME_PLAY_ACTIONS.VOTE,
},
cause: { description: "Why this play needs to be performed" },
});

const requiredTargetsActions: Readonly<GAME_PLAY_ACTIONS[]> = Object.freeze([
GAME_PLAY_ACTIONS.LOOK,
GAME_PLAY_ACTIONS.CHARM,
Expand Down Expand Up @@ -39,10 +31,41 @@ const stutteringJudgeRequestOpportunityActions: Readonly<GAME_PLAY_ACTIONS[]> =
GAME_PLAY_ACTIONS.SETTLE_VOTES,
]);

const gamePlaysPriorityList: Readonly<GamePlay[]> = Object.freeze([
createGamePlayHunterShoots(),
createGamePlayAllElectSheriff({ cause: GAME_PLAY_CAUSES.PREVIOUS_VOTES_WERE_IN_TIES }),
createGamePlayAllElectSheriff(),
createGamePlaySheriffDelegates(),
createGamePlayScapegoatBansVoting(),
createFakeGamePlaySheriffSettlesVotes(),
createGamePlayAllVote({ cause: GAME_PLAY_CAUSES.PREVIOUS_VOTES_WERE_IN_TIES }),
createGamePlayAllVote({ cause: GAME_PLAY_CAUSES.ANGEL_PRESENCE }),
createGamePlayAllVote({ cause: GAME_PLAY_CAUSES.STUTTERING_JUDGE_REQUEST }),
createGamePlayAllVote(),
createGamePlayThiefChoosesCard(),
createGamePlayDogWolfChoosesSide(),
createGamePlayCupidCharms(),
createGamePlaySeerLooks(),
createGamePlayFoxSniffs(),
createGamePlayLoversMeetEachOther(),
createGamePlayStutteringJudgeChoosesSign(),
createGamePlayTwoSistersMeetEachOther(),
createGamePlayThreeBrothersMeetEachOther(),
createGamePlayWildChildChoosesModel(),
createGamePlayRavenMarks(),
createGamePlayGuardProtects(),
createGamePlayWerewolvesEat(),
createGamePlayWhiteWerewolfEats(),
createGamePlayBigBadWolfEats(),
createGamePlayWitchUsesPotions(),
createGamePlayPiedPiperCharms(),
createGamePlayCharmedMeetEachOther(),
]);

export {
gamePlayApiProperties,
requiredTargetsActions,
optionalTargetsActions,
requiredVotesActions,
stutteringJudgeRequestOpportunityActions,
gamePlaysPriorityList,
};
56 changes: 5 additions & 51 deletions src/modules/game/constants/game.constant.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,14 @@
import type { ApiPropertyOptions } from "@nestjs/swagger";
import { ROLE_NAMES } from "../../role/enums/role.enum";
import { GAME_PLAY_ACTIONS, GAME_PLAY_CAUSES } from "../enums/game-play.enum";
import { GAME_PHASES, GAME_STATUSES } from "../enums/game.enum";
import { PLAYER_ATTRIBUTE_NAMES, PLAYER_GROUPS } from "../enums/player.enum";
import type { GamePlay } from "../schemas/game-play/game-play.schema";
import type { Game } from "../schemas/game.schema";
import type { GameSource } from "../types/game.type";

const gameFieldsSpecs = Object.freeze({
players: {
minItems: 4,
maxItems: 40,
},
turn: { default: 1 },
phase: { default: GAME_PHASES.NIGHT },
tick: { default: 1 },
status: { default: GAME_STATUSES.PLAYING },
});

const gameApiProperties: Readonly<Record<keyof Game, ApiPropertyOptions>> = Object.freeze({
_id: {
description: "Game's Mongo ObjectId",
example: "507f1f77bcf86cd799439011",
},
turn: {
description: "Starting at `1`, a turn starts with the first phase (the `night`) and ends with the second phase (the `day`)",
...gameFieldsSpecs.turn,
},
phase: {
description: "Each turn has two phases, `day` and `night`. Starting at `night`",
...gameFieldsSpecs.phase,
},
tick: {
description: "Starting at `1`, tick increments each time a play is made",
...gameFieldsSpecs.tick,
},
status: {
description: "Game's current status",
...gameFieldsSpecs.status,
},
players: {
description: "Players of the game",
...gameFieldsSpecs.players,
},
currentPlay: { description: "Current play which needs to be performed" },
upcomingPlays: { description: "Queue of upcoming plays that needs to be performed to continue the game right after the current play" },
options: { description: "Game's options" },
additionalCards: { description: "Game's additional cards" },
victory: { description: "Victory data set when `status` is `over`" },
createdAt: { description: "When the game was created" },
updatedAt: { description: "When the game was updated" },
});

const gameSourceValues: Readonly<GameSource[]> = Object.freeze([...Object.values(PLAYER_GROUPS), ...Object.values(ROLE_NAMES), PLAYER_ATTRIBUTE_NAMES.SHERIFF]);
const gameSourceValues: Readonly<GameSource[]> = Object.freeze([
...Object.values(PLAYER_GROUPS),
...Object.values(ROLE_NAMES),
PLAYER_ATTRIBUTE_NAMES.SHERIFF,
]);

const gamePlaysNightOrder: Readonly<(GamePlay & { isFirstNightOnly?: boolean })[]> = Object.freeze([
{
Expand Down Expand Up @@ -142,8 +98,6 @@ const gamePlaysNightOrder: Readonly<(GamePlay & { isFirstNightOnly?: boolean })[
]);

export {
gameFieldsSpecs,
gameApiProperties,
gameSourceValues,
gamePlaysNightOrder,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { ValidationOptions } from "class-validator";
import { registerDecorator } from "class-validator";
import { gameAdditionalCardsThiefRoleNames } from "../../../../constants/game-additional-card/game-additional-card.constant";
import type { CreateGameAdditionalCardDto } from "../../../create-game/create-game-additional-card/create-game-additional-card.dto";

function areAdditionalCardsForThiefRolesRespected(value: unknown): boolean {
if (value === undefined) {
return true;
}
const thiefAdditionalCards = value as CreateGameAdditionalCardDto[];
return thiefAdditionalCards.every(({ roleName }) => gameAdditionalCardsThiefRoleNames.includes(roleName));
}

function getAdditionalCardsForThiefRolesDefaultMessage(): string {
return `additionalCards.roleName must be one of the following values: ${gameAdditionalCardsThiefRoleNames.toString()}`;
}

function AdditionalCardsForThiefRoles(validationOptions?: ValidationOptions) {
return (object: object, propertyName: string): void => {
registerDecorator({
name: "AdditionalCardsForThiefRoles",
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
validate: areAdditionalCardsForThiefRolesRespected,
defaultMessage: getAdditionalCardsForThiefRolesDefaultMessage,
},
});
};
}

export {
AdditionalCardsForThiefRoles,
getAdditionalCardsForThiefRolesDefaultMessage,
areAdditionalCardsForThiefRolesRespected,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { ValidationArguments, ValidationOptions } from "class-validator";
import { registerDecorator } from "class-validator";
import type { CreateGameDto } from "../../../create-game/create-game.dto";

function isAdditionalCardsForThiefSizeRespected(value: unknown, validationArguments: ValidationArguments): boolean {
const { options } = validationArguments.object as CreateGameDto;
if (value === undefined) {
return true;
}
if (!Array.isArray(value)) {
return false;
}
return options.roles.thief.additionalCardsCount === value.length;
}

function getAdditionalCardsForThiefSizeDefaultMessage(): string {
return "additionalCards length must be equal to options.roles.thief.additionalCardsCount";
}

function AdditionalCardsForThiefSize(validationOptions?: ValidationOptions) {
return (object: object, propertyName: string): void => {
registerDecorator({
name: "AdditionalCardsForThiefSize",
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
validate: isAdditionalCardsForThiefSizeRespected,
defaultMessage: getAdditionalCardsForThiefSizeDefaultMessage,
},
});
};
}

export {
AdditionalCardsForThiefSize,
getAdditionalCardsForThiefSizeDefaultMessage,
isAdditionalCardsForThiefSizeRespected,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { ValidationArguments, ValidationOptions } from "class-validator";
import { registerDecorator } from "class-validator";
import { ROLE_NAMES } from "../../../../../role/enums/role.enum";
import type { CreateGameDto } from "../../../create-game/create-game.dto";

function isAdditionalCardsPresenceRespected(value: unknown, validationArguments: ValidationArguments): boolean {
const { players } = validationArguments.object as Partial<CreateGameDto>;
const doSomePlayersNeedAdditionalCards = players?.some(player => player.role.name === ROLE_NAMES.THIEF) === true;
return doSomePlayersNeedAdditionalCards ? Array.isArray(value) : value === undefined;
}

function getAdditionalCardsPresenceDefaultMessage(validationArguments: ValidationArguments): string {
if (!Array.isArray(validationArguments.value)) {
return "additionalCards must be set if there is a player with role `thief`";
}
return "additionalCards can't be set if there is no player with role `thief`";
}

function AdditionalCardsPresence(validationOptions?: ValidationOptions) {
return (object: object, propertyName: string): void => {
registerDecorator({
name: "AdditionalCardsForThiefSize",
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
validate: isAdditionalCardsPresenceRespected,
defaultMessage: getAdditionalCardsPresenceDefaultMessage,
},
});
};
}

export {
isAdditionalCardsPresenceRespected,
getAdditionalCardsPresenceDefaultMessage,
AdditionalCardsPresence,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { registerDecorator } from "class-validator";
import type { ValidationArguments, ValidationOptions } from "class-validator";
import { roles } from "../../../../../role/constants/role.constant";
import type { CreateGameAdditionalCardDto } from "../../../create-game/create-game-additional-card/create-game-additional-card.dto";
import type { CreateGameDto } from "../../../create-game/create-game.dto";

function areAdditionalCardsRolesMaxInGameRespected(value: unknown, validationArguments: ValidationArguments): boolean {
if (value === undefined) {
return true;
}
const { players } = validationArguments.object as Partial<CreateGameDto>;
if (players === undefined) {
return false;
}
const additionalCards = value as CreateGameAdditionalCardDto[];
return roles.every(role => {
const playersRoleCount = players.filter(player => player.role.name === role.name).length;
const additionalCardsRoleCount = additionalCards.filter(additionalCard => additionalCard.roleName === role.name).length;
return playersRoleCount + additionalCardsRoleCount <= role.maxInGame;
});
}

function getAdditionalCardsRolesMaxInGameDefaultMessage(): string {
return "additionalCards.roleName can't exceed role maximum occurrences in game. Please check `maxInGame` property of roles";
}

function AdditionalCardsRolesMaxInGame(validationOptions?: ValidationOptions) {
return (object: object, propertyName: string): void => {
registerDecorator({
name: "AdditionalCardsRolesMaxInGame",
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
validate: areAdditionalCardsRolesMaxInGameRespected,
defaultMessage: getAdditionalCardsRolesMaxInGameDefaultMessage,
},
});
};
}

export {
AdditionalCardsRolesMaxInGame,
areAdditionalCardsRolesMaxInGameRespected,
getAdditionalCardsRolesMaxInGameDefaultMessage,
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { applyDecorators } from "@nestjs/common";
import { ArrayMaxSize, ArrayMinSize } from "class-validator";
import { gameFieldsSpecs } from "../../../constants/game.constant";
import { gameFieldsSpecs } from "../../../../schemas/game.schema.constant";

function CompositionBounds(): <TFunction extends () => void, Y>(target: (TFunction | object), propertyKey?: (string | symbol), descriptor?: TypedPropertyDescriptor<Y>) => void {
return applyDecorators(
Expand Down
Loading

0 comments on commit b4bf9de

Please sign in to comment.