Skip to content

Commit

Permalink
feat(accursed-wolf-father): accursed-wolf-father game play (#790)
Browse files Browse the repository at this point in the history
Closes #789
  • Loading branch information
antoinezanardi committed Jan 5, 2024
1 parent 7cfe83a commit 350e4fa
Show file tree
Hide file tree
Showing 43 changed files with 56,220 additions and 52,651 deletions.
2 changes: 2 additions & 0 deletions src/modules/game/constants/game-play/game-play.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const GAME_PLAY_SOURCE_NAMES = [
RoleNames.WITCH,
RoleNames.ACTOR,
RoleNames.BEAR_TAMER,
RoleNames.ACCURSED_WOLF_FATHER,
] as const satisfies Readonly<(PlayerAttributeNames | PlayerGroups | RoleNames)[]>;

const TARGET_ACTIONS = [
Expand All @@ -43,6 +44,7 @@ const TARGET_ACTIONS = [
GamePlayActions.SNIFF,
GamePlayActions.BAN_VOTING,
GamePlayActions.BURY_DEAD_BODIES,
GamePlayActions.INFECT,
] as const satisfies Readonly<GamePlayActions[]>;

const VOTE_ACTIONS = [
Expand Down
5 changes: 5 additions & 0 deletions src/modules/game/constants/game.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ const GAME_PLAYS_PRIORITY_LIST: ReadonlyDeep<GamePlay[]> = [
action: GamePlayActions.EAT,
occurrence: GamePlayOccurrences.ON_NIGHTS,
},
{
source: { name: RoleNames.ACCURSED_WOLF_FATHER },
action: GamePlayActions.INFECT,
occurrence: GamePlayOccurrences.ON_NIGHTS,
},
{
source: { name: RoleNames.WHITE_WEREWOLF },
action: GamePlayActions.EAT,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsBoolean, IsEnum, IsMongoId, IsOptional } from "class-validator";
import { IsEnum, IsMongoId, IsOptional } from "class-validator";
import { Types } from "mongoose";

import { GamePlayActions, WitchPotions } from "@/modules/game/enums/game-play.enum";
Expand All @@ -11,13 +11,7 @@ class MakeGamePlayTargetDto {
@IsMongoId()
public playerId: Types.ObjectId;

@ApiProperty({ description: `Can be set only if there is a \`accursed wolf-father\` in the game and game's upcoming action is \`${GamePlayActions.EAT}\`. If set to \`true\`, the \`werewolves\` victim will instantly join the \`werewolves\` side if possible.` })
@IsOptional()
@IsBoolean()
@Expose()
public isInfected?: boolean;

@ApiProperty({ description: `Can be set only if game's upcoming action is \`${GamePlayActions.USE_POTIONS}\`. If set to \`${WitchPotions.LIFE}\`, the \`witch\` saves target's life from \`werewolves\` meal. If set to \`${WitchPotions.DEATH}\`, the \`witch\` kills the target` })
@ApiProperty({ description: `Can be set only if game's current action is \`${GamePlayActions.USE_POTIONS}\`. If set to \`${WitchPotions.LIFE}\`, the \`witch\` saves target's life from \`werewolves\` meal. If set to \`${WitchPotions.DEATH}\`, the \`witch\` kills the target` })
@IsOptional()
@IsEnum(WitchPotions)
@Expose()
Expand Down
1 change: 1 addition & 0 deletions src/modules/game/enums/game-play.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum GamePlayActions {
SETTLE_VOTES = "settle-votes",
BURY_DEAD_BODIES = "bury-dead-bodies",
GROWL = "growl",
INFECT = "infect",
}

enum GamePlayCauses {
Expand Down
1 change: 1 addition & 0 deletions src/modules/game/enums/player.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ enum PlayerInteractionTypes {
TRANSFER_SHERIFF_ROLE = "transfer-sheriff-role",
SENTENCE_TO_DEATH = "sentence-to-death",
STEAL_ROLE = "steal-role",
INFECT = "infect",
}

export {
Expand Down
11 changes: 10 additions & 1 deletion src/modules/game/helpers/player/player.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { PlayerAttributeNames } from "@/modules/game/enums/player.enum";
import { doesPlayerHaveActiveAttributeWithName } from "@/modules/game/helpers/player/player-attribute/player-attribute.helper";
import type { Game } from "@/modules/game/schemas/game.schema";
import type { Player } from "@/modules/game/schemas/player/player.schema";
import { RoleSides } from "@/modules/role/enums/role.enum";
import { RoleNames, RoleSides } from "@/modules/role/enums/role.enum";

function isPlayerPowerful(player: Player, game: Game): boolean {
return !doesPlayerHaveActiveAttributeWithName(player, PlayerAttributeNames.POWERLESS, game);
Expand All @@ -20,9 +20,18 @@ function isPlayerOnVillagersSide(player: Player): boolean {
return player.side.current === RoleSides.VILLAGERS;
}

function isPlayerPowerlessOnWerewolvesSide(player: Player, game: Game): boolean {
const { prejudicedManipulator, piedPiper, actor } = game.options.roles;
const { current: roleName } = player.role;
return roleName === RoleNames.PREJUDICED_MANIPULATOR && prejudicedManipulator.isPowerlessOnWerewolvesSide ||
roleName === RoleNames.PIED_PIPER && piedPiper.isPowerlessOnWerewolvesSide ||
roleName === RoleNames.ACTOR && actor.isPowerlessOnWerewolvesSide;
}

export {
isPlayerPowerful,
isPlayerAliveAndPowerful,
isPlayerOnWerewolvesSide,
isPlayerOnVillagersSide,
isPlayerPowerlessOnWerewolvesSide,
};
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ export class GameHistoryRecordRepository {
return this.gameHistoryRecordModel.findOne(filter, undefined, { sort: { createdAt: -1 } });
}

public async getLastGameHistoryAccursedWolfFatherInfectsRecord(gameId: Types.ObjectId, accursedWolfFatherPlayerId: Types.ObjectId): Promise<GameHistoryRecord | null> {
const filter: FilterQuery<GameHistoryRecord> = {
gameId,
"play.action": GamePlayActions.INFECT,
"play.source.name": RoleNames.ACCURSED_WOLF_FATHER,
"play.source.players": { $elemMatch: { _id: accursedWolfFatherPlayerId } },
};
return this.gameHistoryRecordModel.findOne(filter, undefined, { sort: { createdAt: -1 } });
}

public async getGameHistoryWitchUsesSpecificPotionRecords(gameId: Types.ObjectId, witchPlayerId: Types.ObjectId, potion: WitchPotions): Promise<GameHistoryRecord[]> {
const filter: FilterQuery<GameHistoryRecord> = {
gameId,
Expand All @@ -67,17 +77,13 @@ export class GameHistoryRecordRepository {
return this.gameHistoryRecordModel.find(filter);
}

public async getGameHistoryAccursedWolfFatherInfectedRecords(gameId: Types.ObjectId, accursedWolfFatherPlayerId: Types.ObjectId): Promise<GameHistoryRecord[]> {
public async getGameHistoryAccursedWolfFatherInfectsWithTargetRecords(gameId: Types.ObjectId, accursedWolfFatherPlayerId: Types.ObjectId): Promise<GameHistoryRecord[]> {
const filter: FilterQuery<GameHistoryRecord> = {
gameId,
"play.action": GamePlayActions.EAT,
"play.targets.isInfected": true,
"play.source.players": {
$elemMatch: {
"_id": accursedWolfFatherPlayerId,
"role.current": RoleNames.ACCURSED_WOLF_FATHER,
},
},
"play.action": GamePlayActions.INFECT,
"play.source.name": RoleNames.ACCURSED_WOLF_FATHER,
"play.source.players": { $elemMatch: { _id: accursedWolfFatherPlayerId } },
"play.targets": { $exists: true, $ne: [] },
};
return this.gameHistoryRecordModel.find(filter);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,16 @@ export class GameHistoryRecordService {
return this.gameHistoryRecordRepository.getLastGameHistoryTieInVotesRecord(gameId, action);
}

public async getLastGameHistoryAccursedWolfFatherInfectsRecord(gameId: Types.ObjectId, accursedWolfFatherPlayerId: Types.ObjectId): Promise<GameHistoryRecord | null> {
return this.gameHistoryRecordRepository.getLastGameHistoryAccursedWolfFatherInfectsRecord(gameId, accursedWolfFatherPlayerId);
}

public async getGameHistoryWitchUsesSpecificPotionRecords(gameId: Types.ObjectId, witchPlayerId: Types.ObjectId, potion: WitchPotions): Promise<GameHistoryRecord[]> {
return this.gameHistoryRecordRepository.getGameHistoryWitchUsesSpecificPotionRecords(gameId, witchPlayerId, potion);
}

public async getGameHistoryAccursedWolfFatherInfectedRecords(gameId: Types.ObjectId, accursedWolfFatherPlayer: Types.ObjectId): Promise<GameHistoryRecord[]> {
return this.gameHistoryRecordRepository.getGameHistoryAccursedWolfFatherInfectedRecords(gameId, accursedWolfFatherPlayer);
public async getGameHistoryAccursedWolfFatherInfectsWithTargetRecords(gameId: Types.ObjectId, accursedWolfFatherPlayerId: Types.ObjectId): Promise<GameHistoryRecord[]> {
return this.gameHistoryRecordRepository.getGameHistoryAccursedWolfFatherInfectsWithTargetRecords(gameId, accursedWolfFatherPlayerId);
}

public async getGameHistoryJudgeRequestRecords(gameId: Types.ObjectId, stutteringJudgePlayedId: Types.ObjectId): Promise<GameHistoryRecord[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { createGamePlayEligibleTargets } from "@/modules/game/helpers/game-play/
import { createInteractablePlayer } from "@/modules/game/helpers/game-play/game-play-eligible-targets/interactable-player/interactable-player.factory";
import { createGamePlay } from "@/modules/game/helpers/game-play/game-play.factory";
import { getAlivePlayers, getAliveVillagerSidedPlayers, getAllowedToVotePlayers, getGroupOfPlayers, getEligiblePiedPiperTargets, getEligibleWerewolvesTargets, getEligibleWhiteWerewolfTargets, getPlayersWithActiveAttributeName, getPlayersWithCurrentRole, getPlayerWithCurrentRole, getEligibleCupidTargets, isGameSourceGroup, isGameSourceRole } from "@/modules/game/helpers/game.helper";
import { doesPlayerHaveActiveAttributeWithName } from "@/modules/game/helpers/player/player-attribute/player-attribute.helper";
import { doesPlayerHaveActiveAttributeWithName, doesPlayerHaveActiveAttributeWithNameAndSource } from "@/modules/game/helpers/player/player-attribute/player-attribute.helper";
import { createPlayer } from "@/modules/game/helpers/player/player.factory";
import { isPlayerAliveAndPowerful } from "@/modules/game/helpers/player/player.helper";
import { GameHistoryRecordService } from "@/modules/game/providers/services/game-history/game-history-record.service";
Expand Down Expand Up @@ -45,6 +45,7 @@ export class GamePlayAugmenterService {
[RoleNames.WHITE_WEREWOLF]: game => this.getWhiteWerewolfGamePlayEligibleTargets(game),
[RoleNames.WILD_CHILD]: game => this.getWildChildGamePlayEligibleTargets(game),
[RoleNames.WITCH]: async game => this.getWitchGamePlayEligibleTargets(game),
[RoleNames.ACCURSED_WOLF_FATHER]: async game => this.getAccursedWolfFatherGamePlayEligibleTargets(game),
};

private readonly canBeSkippedPlayMethods: Partial<Record<GamePlaySourceName, (game: Game, gamePlay: GamePlay) => boolean>> = {
Expand All @@ -62,6 +63,7 @@ export class GamePlayAugmenterService {
[RoleNames.WITCH]: () => true,
[RoleNames.ACTOR]: () => true,
[RoleNames.CUPID]: (game: Game) => this.canCupidSkipGamePlay(game),
[RoleNames.ACCURSED_WOLF_FATHER]: () => true,
};

public constructor(private readonly gameHistoryRecordService: GameHistoryRecordService) {}
Expand Down Expand Up @@ -335,6 +337,23 @@ export class GamePlayAugmenterService {
return createGamePlayEligibleTargets({ interactablePlayers, boundaries });
}

private async getAccursedWolfFatherGamePlayEligibleTargets(game: Game): Promise<GamePlayEligibleTargets | undefined> {
const accursedWolfFatherPlayer = getPlayerWithCurrentRole(game, RoleNames.ACCURSED_WOLF_FATHER);
if (!accursedWolfFatherPlayer) {
throw createCantFindPlayerWithCurrentRoleUnexpectedException("getAccursedWolfFatherGamePlayEligibleTargets", { gameId: game._id, roleName: RoleNames.ACCURSED_WOLF_FATHER });
}
const infectedTargetRecords = await this.gameHistoryRecordService.getGameHistoryAccursedWolfFatherInfectsWithTargetRecords(game._id, accursedWolfFatherPlayer._id);
if (infectedTargetRecords.length) {
return undefined;
}
const eatenByWerewolvesPlayers = game.players.filter(player =>
doesPlayerHaveActiveAttributeWithNameAndSource(player, PlayerAttributeNames.EATEN, PlayerGroups.WEREWOLVES, game));
const interactions: PlayerInteraction[] = [{ type: PlayerInteractionTypes.INFECT, source: RoleNames.ACCURSED_WOLF_FATHER }];
const interactablePlayers: InteractablePlayer[] = eatenByWerewolvesPlayers.map(player => ({ player, interactions }));
const boundaries: GamePlayEligibleTargetsBoundaries = { min: 0, max: 1 };
return createGamePlayEligibleTargets({ interactablePlayers, boundaries });
}

private async getGamePlayEligibleTargets(gamePlay: GamePlay, game: Game): Promise<GamePlayEligibleTargets | undefined> {
const eligibleTargetsPlayMethod = this.getEligibleTargetsPlayMethods[gamePlay.source.name];
if (!eligibleTargetsPlayMethod) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { Injectable } from "@nestjs/common";
import { sample } from "lodash";

import type { DeadPlayer } from "@/modules/game/schemas/player/dead-player.schema";
import { DevotedServantGamePlayMakerService } from "@/modules/game/providers/services/game-play/game-play-maker/devoted-servant-game-play-maker.service";
import { GameHistoryRecordService } from "@/modules/game/providers/services/game-history/game-history-record.service";
import type { MakeGamePlayWithRelationsDto } from "@/modules/game/dto/make-game-play/make-game-play-with-relations.dto";
import { GamePlayActions, GamePlayCauses, WitchPotions } from "@/modules/game/enums/game-play.enum";
import { PlayerAttributeNames, PlayerGroups } from "@/modules/game/enums/player.enum";
import { PlayerAttributeNames, PlayerDeathCauses, PlayerGroups } from "@/modules/game/enums/player.enum";
import { createGamePlaySheriffSettlesVotes, createGamePlaySurvivorsElectSheriff, createGamePlaySurvivorsVote } from "@/modules/game/helpers/game-play/game-play.factory";
import { createGame, createGameWithCurrentGamePlay } from "@/modules/game/helpers/game.factory";
import { getFoxSniffedPlayers, getPlayersWithIds, getPlayerWithActiveAttributeName, getPlayerWithCurrentRole } from "@/modules/game/helpers/game.helper";
import { addPlayerAttributeInGame, addPlayersAttributeInGame, appendUpcomingPlayInGame, prependUpcomingPlayInGame, removePlayerAttributeByNameInGame, updateAdditionalCardInGame, updatePlayerInGame } from "@/modules/game/helpers/game.mutator";
import { createActingByActorPlayerAttribute, createCantVoteByScapegoatPlayerAttribute, createCharmedByPiedPiperPlayerAttribute, createDrankDeathPotionByWitchPlayerAttribute, createDrankLifePotionByWitchPlayerAttribute, createEatenByBigBadWolfPlayerAttribute, createEatenByWerewolvesPlayerAttribute, createEatenByWhiteWerewolfPlayerAttribute, createInLoveByCupidPlayerAttribute, createPowerlessByAccursedWolfFatherPlayerAttribute, createPowerlessByActorPlayerAttribute, createPowerlessByFoxPlayerAttribute, createProtectedByDefenderPlayerAttribute, createScandalmongerMarkByScandalmongerPlayerAttribute, createSeenBySeerPlayerAttribute, createSheriffBySheriffPlayerAttribute, createSheriffBySurvivorsPlayerAttribute, createWorshipedByWildChildPlayerAttribute } from "@/modules/game/helpers/player/player-attribute/player-attribute.factory";
import { createPlayerShotByHunterDeath, createPlayerVoteBySheriffDeath, createPlayerVoteBySurvivorsDeath, createPlayerVoteScapegoatedBySurvivorsDeath } from "@/modules/game/helpers/player/player-death/player-death.factory";
import { isPlayerAliveAndPowerful } from "@/modules/game/helpers/player/player.helper";
import { isPlayerAliveAndPowerful, isPlayerPowerlessOnWerewolvesSide } from "@/modules/game/helpers/player/player.helper";
import { GameHistoryRecordService } from "@/modules/game/providers/services/game-history/game-history-record.service";
import { DevotedServantGamePlayMakerService } from "@/modules/game/providers/services/game-play/game-play-maker/devoted-servant-game-play-maker.service";
import { GamePlayVoteService } from "@/modules/game/providers/services/game-play/game-play-vote/game-play-vote.service";
import { PlayerKillerService } from "@/modules/game/providers/services/player/player-killer.service";
import type { Game } from "@/modules/game/schemas/game.schema";
import type { DeadPlayer } from "@/modules/game/schemas/player/dead-player.schema";
import type { PlayerRole } from "@/modules/game/schemas/player/player-role/player-role.schema";
import type { PlayerSide } from "@/modules/game/schemas/player/player-side/player-side.schema";
import type { Player } from "@/modules/game/schemas/player/player.schema";
Expand All @@ -32,7 +32,7 @@ import { createCantFindLastDeadPlayersUnexpectedException, createNoCurrentGamePl
@Injectable()
export class GamePlayMakerService {
private readonly gameSourcePlayMethods: Partial<Record<GamePlaySourceName, (play: MakeGamePlayWithRelationsDto, game: GameWithCurrentPlay) => Game | Promise<Game>>> = {
[PlayerGroups.WEREWOLVES]: async(play, game) => this.werewolvesEat(play, game),
[PlayerGroups.WEREWOLVES]: (play, game) => this.werewolvesEat(play, game),
[PlayerGroups.SURVIVORS]: async(play, game) => this.survivorsPlay(play, game),
[PlayerAttributeNames.SHERIFF]: async(play, game) => this.sheriffPlays(play, game),
[RoleNames.BIG_BAD_WOLF]: (play, game) => this.bigBadWolfEats(play, game),
Expand All @@ -50,6 +50,7 @@ export class GamePlayMakerService {
[RoleNames.THIEF]: (play, game) => this.thiefChoosesCard(play, game),
[RoleNames.SCANDALMONGER]: (play, game) => this.scandalmongerMarks(play, game),
[RoleNames.ACTOR]: (play, game) => this.actorChoosesCard(play, game),
[RoleNames.ACCURSED_WOLF_FATHER]: async(play, game) => this.accursedWolfFatherInfects(play, game),
};

public constructor(
Expand Down Expand Up @@ -396,30 +397,32 @@ export class GamePlayMakerService {
return addPlayerAttributeInGame(targetedPlayer._id, clonedGame, eatenByBigBadWolfPlayerAttribute);
}

private accursedWolfFatherInfects(targetedPlayer: Player, game: GameWithCurrentPlay): Game {
let clonedGame = createGame(game);
const { roles } = game.options;
const playerDataToUpdate: Partial<Player> = { side: { ...targetedPlayer.side, current: RoleSides.WEREWOLVES } };
if (targetedPlayer.role.current === RoleNames.PREJUDICED_MANIPULATOR && roles.prejudicedManipulator.isPowerlessOnWerewolvesSide ||
targetedPlayer.role.current === RoleNames.PIED_PIPER && roles.piedPiper.isPowerlessOnWerewolvesSide ||
targetedPlayer.role.current === RoleNames.ACTOR && roles.actor.isPowerlessOnWerewolvesSide) {
clonedGame = addPlayerAttributeInGame(targetedPlayer._id, clonedGame, createPowerlessByAccursedWolfFatherPlayerAttribute());
private async accursedWolfFatherInfects({ targets }: MakeGamePlayWithRelationsDto, game: GameWithCurrentPlay): Promise<Game> {
const clonedGame = createGame(game);
const expectedTargetCount = 1;
if (targets?.length !== expectedTargetCount) {
return clonedGame;
}
const { player: targetedPlayer } = targets[0];
if (targetedPlayer.role.current === RoleNames.ELDER && !await this.playerKillerService.isElderKillable(clonedGame, targetedPlayer, PlayerDeathCauses.EATEN)) {
return clonedGame;
}
const playerDataToUpdate: Partial<Player> = { side: { ...targetedPlayer.side, current: RoleSides.WEREWOLVES }, attributes: targetedPlayer.attributes };
if (isPlayerPowerlessOnWerewolvesSide(targetedPlayer, clonedGame)) {
playerDataToUpdate.attributes?.push(createPowerlessByAccursedWolfFatherPlayerAttribute());
}
playerDataToUpdate.attributes = playerDataToUpdate.attributes?.filter(({ name, source }) => name !== PlayerAttributeNames.EATEN || source !== PlayerGroups.WEREWOLVES);
return updatePlayerInGame(targetedPlayer._id, playerDataToUpdate, clonedGame);
}

private async werewolvesEat({ targets }: MakeGamePlayWithRelationsDto, game: GameWithCurrentPlay): Promise<Game> {
private werewolvesEat({ targets }: MakeGamePlayWithRelationsDto, game: GameWithCurrentPlay): Game {
const clonedGame = createGame(game);
const expectedTargetCount = 1;
if (targets?.length !== expectedTargetCount) {
return clonedGame;
}
const { player: targetedPlayer, isInfected: isTargetInfected } = targets[0];
const { player: targetedPlayer } = targets[0];
const eatenByWerewolvesPlayerAttribute = createEatenByWerewolvesPlayerAttribute();
const elderLivesCount = await this.playerKillerService.getElderLivesCountAgainstWerewolves(clonedGame, targetedPlayer);
if (isTargetInfected === true && (targetedPlayer.role.current !== RoleNames.ELDER || elderLivesCount <= 1)) {
return this.accursedWolfFatherInfects(targetedPlayer, clonedGame as GameWithCurrentPlay);
}
return addPlayerAttributeInGame(targetedPlayer._id, clonedGame, eatenByWerewolvesPlayerAttribute);
}
}

0 comments on commit 350e4fa

Please sign in to comment.