Skip to content

Commit

Permalink
feat(roles): eligible additional card recipients for each role (#714)
Browse files Browse the repository at this point in the history
Closes #707
  • Loading branch information
antoinezanardi committed Dec 3, 2023
1 parent f3277db commit 0b75987
Show file tree
Hide file tree
Showing 15 changed files with 20,282 additions and 20,412 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ROLES } from "@/modules/role/constants/role.constant";
import { RoleNames } from "@/modules/role/enums/role.enum";

const GAME_ADDITIONAL_CARDS_THIEF_ROLE_NAMES: Readonly<RoleNames[]> = Object.freeze(ROLES.filter(({ minInGame, name }) => name !== RoleNames.THIEF && minInGame === undefined).map(({ name }) => name));
const GAME_ADDITIONAL_CARDS_RECIPIENTS = [RoleNames.THIEF] as const satisfies Readonly<(RoleNames)[]>;

export { GAME_ADDITIONAL_CARDS_THIEF_ROLE_NAMES };
export { GAME_ADDITIONAL_CARDS_RECIPIENTS };
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { registerDecorator } from "class-validator";
import type { ValidationOptions } from "class-validator";

import { GAME_ADDITIONAL_CARDS_THIEF_ROLE_NAMES } from "@/modules/game/constants/game-additional-card/game-additional-card.constant";
import { ELIGIBLE_THIEF_ADDITIONAL_CARDS_ROLES } from "@/modules/role/constants/role.constant";
import type { CreateGameAdditionalCardDto } from "@/modules/game/dto/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 }) => GAME_ADDITIONAL_CARDS_THIEF_ROLE_NAMES.includes(roleName));
const eligibleThiefAdditionalCardsRoleNames = ELIGIBLE_THIEF_ADDITIONAL_CARDS_ROLES.map(({ name }) => name);
return thiefAdditionalCards.every(({ roleName }) => eligibleThiefAdditionalCardsRoleNames.includes(roleName));
}

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

function AdditionalCardsForThiefRoles(validationOptions?: ValidationOptions) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class GameRandomCompositionService {
const randomRole = sample(leftRolesToPick);
if (randomRole === undefined) {
randomRolesToPickCount = 1;
randomRoles.push(defaultSidedRole);
randomRoles.push(defaultSidedRole as Role);
} else {
randomRolesToPickCount = randomRole.minInGame ?? 1;
for (let j = 0; j < randomRolesToPickCount; j++) {
Expand Down Expand Up @@ -72,6 +72,6 @@ export class GameRandomCompositionService {
const isRolePermitted = !excludedRoles.includes(role.name);
const isRoleMinInGameRespected = !areRecommendedMinPlayersRespected || role.recommendedMinPlayers === undefined || role.recommendedMinPlayers <= players.length;
return isRolePermitted && isRoleMinInGameRespected;
});
}) as Role[];
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { ApiPropertyOptions } from "@nestjs/swagger";
import type { ReadonlyDeep } from "type-fest";

import { GAME_ADDITIONAL_CARDS_THIEF_ROLE_NAMES } from "@/modules/game/constants/game-additional-card/game-additional-card.constant";
import { ELIGIBLE_THIEF_ADDITIONAL_CARDS_ROLES } from "@/modules/role/constants/role.constant";
import { GAME_ADDITIONAL_CARDS_RECIPIENTS } from "@/modules/game/constants/game-additional-card/game-additional-card.constant";
import type { GameAdditionalCard } from "@/modules/game/schemas/game-additional-card/game-additional-card.schema";
import { RoleNames } from "@/modules/role/enums/role.enum";

Expand All @@ -16,7 +17,7 @@ const GAME_ADDITIONAL_CARDS_FIELDS_SPECS = {
},
recipient: {
required: true,
enum: [RoleNames.THIEF],
enum: GAME_ADDITIONAL_CARDS_RECIPIENTS,
},
isUsed: {
required: true,
Expand All @@ -30,7 +31,7 @@ const GAME_ADDITIONAL_CARDS_API_PROPERTIES: ReadonlyDeep<Record<keyof GameAdditi
...convertMongoosePropOptionsToApiPropertyOptions(GAME_ADDITIONAL_CARDS_FIELDS_SPECS._id),
},
roleName: {
description: `Game additional card role name. If \`recipient\` is \`${RoleNames.THIEF}\`, possible values are : ${GAME_ADDITIONAL_CARDS_THIEF_ROLE_NAMES.toString()}`,
description: `Game additional card role name. If \`recipient\` is \`${RoleNames.THIEF}\`, possible values are : ${ELIGIBLE_THIEF_ADDITIONAL_CARDS_ROLES.toString()}`,
...convertMongoosePropOptionsToApiPropertyOptions(GAME_ADDITIONAL_CARDS_FIELDS_SPECS.roleName),
},
recipient: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ApiProperty } from "@nestjs/swagger";
import { Expose, Transform } from "class-transformer";
import { Types } from "mongoose";

import { GameAdditionalCardRecipientRoleName } from "@/modules/game/types/game-additional-card.types";
import { GAME_ADDITIONAL_CARDS_API_PROPERTIES, GAME_ADDITIONAL_CARDS_FIELDS_SPECS } from "@/modules/game/schemas/game-additional-card/game-additional-card.schema.constant";
import { RoleNames } from "@/modules/role/enums/role.enum";

Expand All @@ -24,7 +25,7 @@ class GameAdditionalCard {
@ApiProperty(GAME_ADDITIONAL_CARDS_API_PROPERTIES.recipient as ApiPropertyOptions)
@Prop(GAME_ADDITIONAL_CARDS_FIELDS_SPECS.recipient)
@Expose()
public recipient: RoleNames.THIEF;
public recipient: GameAdditionalCardRecipientRoleName;

@ApiProperty(GAME_ADDITIONAL_CARDS_API_PROPERTIES.isUsed as ApiPropertyOptions)
@Prop(GAME_ADDITIONAL_CARDS_FIELDS_SPECS.isUsed)
Expand Down
7 changes: 7 additions & 0 deletions src/modules/game/types/game-additional-card.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { TupleToUnion } from "type-fest";

import type { GAME_ADDITIONAL_CARDS_RECIPIENTS } from "@/modules/game/constants/game-additional-card/game-additional-card.constant";

type GameAdditionalCardRecipientRoleName = TupleToUnion<typeof GAME_ADDITIONAL_CARDS_RECIPIENTS>;

export type { GameAdditionalCardRecipientRoleName };
28 changes: 28 additions & 0 deletions src/modules/role/constants/role.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const DEFAULT_WEREWOLF_ROLE: ReadonlyDeep<Role> = plainToInstance(Role, {
side: RoleSides.WEREWOLVES,
type: RoleTypes.WEREWOLF,
origin: RoleOrigins.CLASSIC,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
maxInGame: 99,
});

Expand All @@ -20,6 +21,7 @@ const WEREWOLF_ROLES: ReadonlyDeep<Role[]> = plainToInstance(Role, [
type: RoleTypes.WEREWOLF,
origin: RoleOrigins.CHARACTERS,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
recommendedMinPlayers: 15,
},
{
Expand All @@ -28,6 +30,7 @@ const WEREWOLF_ROLES: ReadonlyDeep<Role[]> = plainToInstance(Role, [
type: RoleTypes.WEREWOLF,
origin: RoleOrigins.CHARACTERS,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
recommendedMinPlayers: 12,
},
{
Expand All @@ -36,6 +39,7 @@ const WEREWOLF_ROLES: ReadonlyDeep<Role[]> = plainToInstance(Role, [
type: RoleTypes.LONELY,
origin: RoleOrigins.THE_VILLAGE,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
recommendedMinPlayers: 12,
},
]);
Expand All @@ -46,6 +50,7 @@ const DEFAULT_VILLAGER_ROLE: ReadonlyDeep<Role> = plainToInstance(Role, {
type: RoleTypes.VILLAGER,
origin: RoleOrigins.CLASSIC,
maxInGame: 99,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
});

const VILLAGER_ROLES: ReadonlyDeep<Role[]> = plainToInstance(Role, [
Expand All @@ -56,69 +61,79 @@ const VILLAGER_ROLES: ReadonlyDeep<Role[]> = plainToInstance(Role, [
type: RoleTypes.VILLAGER,
origin: RoleOrigins.CHARACTERS,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.SEER,
side: RoleSides.VILLAGERS,
type: RoleTypes.VILLAGER,
origin: RoleOrigins.CLASSIC,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.CUPID,
side: RoleSides.VILLAGERS,
type: RoleTypes.VILLAGER,
origin: RoleOrigins.CLASSIC,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.WITCH,
side: RoleSides.VILLAGERS,
type: RoleTypes.VILLAGER,
origin: RoleOrigins.CLASSIC,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.HUNTER,
side: RoleSides.VILLAGERS,
type: RoleTypes.VILLAGER,
origin: RoleOrigins.CLASSIC,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.LITTLE_GIRL,
side: RoleSides.VILLAGERS,
type: RoleTypes.VILLAGER,
origin: RoleOrigins.CLASSIC,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.DEFENDER,
side: RoleSides.VILLAGERS,
type: RoleTypes.VILLAGER,
origin: RoleOrigins.NEW_MOON,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.ELDER,
side: RoleSides.VILLAGERS,
type: RoleTypes.VILLAGER,
origin: RoleOrigins.NEW_MOON,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.SCAPEGOAT,
side: RoleSides.VILLAGERS,
type: RoleTypes.VILLAGER,
origin: RoleOrigins.NEW_MOON,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.IDIOT,
side: RoleSides.VILLAGERS,
type: RoleTypes.VILLAGER,
origin: RoleOrigins.NEW_MOON,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.TWO_SISTERS,
Expand All @@ -145,27 +160,31 @@ const VILLAGER_ROLES: ReadonlyDeep<Role[]> = plainToInstance(Role, [
origin: RoleOrigins.CHARACTERS,
maxInGame: 1,
recommendedMinPlayers: 12,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.BEAR_TAMER,
side: RoleSides.VILLAGERS,
type: RoleTypes.VILLAGER,
origin: RoleOrigins.CHARACTERS,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.STUTTERING_JUDGE,
side: RoleSides.VILLAGERS,
type: RoleTypes.VILLAGER,
origin: RoleOrigins.CHARACTERS,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.RUSTY_SWORD_KNIGHT,
side: RoleSides.VILLAGERS,
type: RoleTypes.VILLAGER,
origin: RoleOrigins.CHARACTERS,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.THIEF,
Expand All @@ -180,27 +199,31 @@ const VILLAGER_ROLES: ReadonlyDeep<Role[]> = plainToInstance(Role, [
type: RoleTypes.AMBIGUOUS,
origin: RoleOrigins.CHARACTERS,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.WOLF_HOUND,
side: RoleSides.VILLAGERS,
type: RoleTypes.AMBIGUOUS,
origin: RoleOrigins.CHARACTERS,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.ANGEL,
side: RoleSides.VILLAGERS,
type: RoleTypes.LONELY,
origin: RoleOrigins.CHARACTERS,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.PIED_PIPER,
side: RoleSides.VILLAGERS,
type: RoleTypes.LONELY,
origin: RoleOrigins.NEW_MOON,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
recommendedMinPlayers: 12,
},
{
Expand All @@ -209,13 +232,15 @@ const VILLAGER_ROLES: ReadonlyDeep<Role[]> = plainToInstance(Role, [
type: RoleTypes.VILLAGER,
origin: RoleOrigins.THE_VILLAGE,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
},
{
name: RoleNames.PREJUDICED_MANIPULATOR,
side: RoleSides.VILLAGERS,
type: RoleTypes.LONELY,
origin: RoleOrigins.CHARACTERS,
maxInGame: 1,
additionalCardsEligibleRecipients: [RoleNames.THIEF],
recommendedMinPlayers: 12,
},
]);
Expand All @@ -225,10 +250,13 @@ const ROLES: ReadonlyDeep<Role[]> = plainToInstance(Role, [
...VILLAGER_ROLES,
]);

const ELIGIBLE_THIEF_ADDITIONAL_CARDS_ROLES = ROLES.filter(({ additionalCardsEligibleRecipients }) => additionalCardsEligibleRecipients?.includes(RoleNames.THIEF));

export {
ROLES,
DEFAULT_WEREWOLF_ROLE,
DEFAULT_VILLAGER_ROLE,
WEREWOLF_ROLES,
VILLAGER_ROLES,
ELIGIBLE_THIEF_ADDITIONAL_CARDS_ROLES,
};
4 changes: 2 additions & 2 deletions src/modules/role/controllers/role.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class RoleController {
@Get()
@ApiOperation({ summary: "Get all available roles for games" })
@ApiResponse({ status: HttpStatus.OK, type: Role, isArray: true })
private getRoles(): readonly Role[] {
return ROLES;
private getRoles(): Role[] {
return ROLES as Role[];
}
}
15 changes: 14 additions & 1 deletion src/modules/role/types/role.type.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose } from "class-transformer";
import { IsEnum, IsInt, IsOptional, Min } from "class-validator";
import { IsArray, IsEnum, IsInt, IsOptional, Min } from "class-validator";

import { GAME_ADDITIONAL_CARDS_RECIPIENTS } from "@/modules/game/constants/game-additional-card/game-additional-card.constant";
import type { GameAdditionalCardRecipientRoleName } from "@/modules/game/types/game-additional-card.types";
import { RoleNames, RoleOrigins, RoleSides, RoleTypes } from "@/modules/role/enums/role.enum";

class Role {
Expand Down Expand Up @@ -37,6 +39,17 @@ class Role {
@Expose()
public origin: RoleOrigins;

@ApiProperty({
description: "If set, this role can be used as an additional card for the recipients set. Otherwise, it can't be used as an additional card by anyone",
required: false,
isArray: true,
})
@IsOptional()
@IsArray()
@IsEnum(GAME_ADDITIONAL_CARDS_RECIPIENTS, { each: true })
@Expose()
public additionalCardsEligibleRecipients?: GameAdditionalCardRecipientRoleName[];

@ApiProperty({ description: "If the role is chosen by at least one player, then `minInGame` players must choose it to start the game" })
@IsOptional()
@IsInt()
Expand Down

0 comments on commit 0b75987

Please sign in to comment.