Skip to content

Commit

Permalink
feat(game-play): targets boundaries for current game play (#613)
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinezanardi committed Oct 28, 2023
1 parent 7f36c49 commit 2b7b72b
Show file tree
Hide file tree
Showing 38 changed files with 39,299 additions and 34,824 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { plainToInstance } from "class-transformer";

import { GamePlayEligibleTargets } from "@/modules/game/schemas/game-play/game-play-eligible-targets/game-play-eligible-targets.schema";

import { PLAIN_TO_INSTANCE_DEFAULT_OPTIONS } from "@/shared/validation/constants/validation.constant";

function createGamePlayEligibleTargets(gamePlayEligibleTargets: GamePlayEligibleTargets): GamePlayEligibleTargets {
return plainToInstance(GamePlayEligibleTargets, gamePlayEligibleTargets, PLAIN_TO_INSTANCE_DEFAULT_OPTIONS);
}

export { createGamePlayEligibleTargets };
Original file line number Diff line number Diff line change
@@ -1,17 +1,39 @@
import { Injectable } from "@nestjs/common";

import { GameHistoryRecordService } from "@/modules/game/providers/services/game-history/game-history-record.service";
import { createGamePlayEligibleTargets } from "@/modules/game/helpers/game-play/game-play-eligible-targets/game-play-eligible-targets.factory";
import type { GamePlayEligibleTargets } from "@/modules/game/schemas/game-play/game-play-eligible-targets/game-play-eligible-targets.schema";
import { WEREWOLF_ROLES } from "@/modules/role/constants/role.constant";
import { GamePlayActions, GamePlayCauses } from "@/modules/game/enums/game-play.enum";
import { PlayerGroups } from "@/modules/game/enums/player.enum";
import { GamePlayActions, GamePlayCauses, WitchPotions } from "@/modules/game/enums/game-play.enum";
import { PlayerAttributeNames, PlayerGroups } from "@/modules/game/enums/player.enum";
import { createGamePlay } from "@/modules/game/helpers/game-play/game-play.factory";
import { getLeftToEatByWerewolvesPlayers } from "@/modules/game/helpers/game.helper";
import { getAlivePlayers, getLeftToCharmByPiedPiperPlayers, getLeftToEatByWerewolvesPlayers, getLeftToEatByWhiteWerewolfPlayers } from "@/modules/game/helpers/game.helper";
import type { GamePlay } from "@/modules/game/schemas/game-play/game-play.schema";
import type { Game } from "@/modules/game/schemas/game.schema";
import type { GamePlaySourceName } from "@/modules/game/types/game-play.type";
import { RoleNames } from "@/modules/role/enums/role.enum";

@Injectable()
export class GamePlayAugmenterService {
private readonly getEligibleTargetsPlayMethods: Partial<
Record<GamePlaySourceName, (gamePlay: GamePlay, game: Game) => GamePlayEligibleTargets | Promise<GamePlayEligibleTargets>>
> = {
[PlayerAttributeNames.SHERIFF]: () => this.getSingleTargetGamePlayEligibleTargets(),
[PlayerGroups.WEREWOLVES]: () => this.getSingleTargetGamePlayEligibleTargets(),
[RoleNames.BIG_BAD_WOLF]: (gamePlay, game) => this.getBigBadWolfGamePlayEligibleTargets(gamePlay, game),
[RoleNames.CUPID]: () => this.getCupidGamePlayEligibleTargets(),
[RoleNames.FOX]: () => this.getFoxGamePlayEligibleTargets(),
[RoleNames.GUARD]: () => this.getSingleTargetGamePlayEligibleTargets(),
[RoleNames.HUNTER]: () => this.getSingleTargetGamePlayEligibleTargets(),
[RoleNames.PIED_PIPER]: (gamePlay, game) => this.getPiedPiperGamePlayEligibleTargets(gamePlay, game),
[RoleNames.RAVEN]: () => this.getRavenGamePlayEligibleTargets(),
[RoleNames.SCAPEGOAT]: (gamePlay, game) => this.getScapegoatGamePlayEligibleTargets(gamePlay, game),
[RoleNames.SEER]: () => this.getSingleTargetGamePlayEligibleTargets(),
[RoleNames.WHITE_WEREWOLF]: (gamePlay, game) => this.getWhiteWerewolfGamePlayEligibleTargets(gamePlay, game),
[RoleNames.WILD_CHILD]: () => this.getSingleTargetGamePlayEligibleTargets(),
[RoleNames.WITCH]: async(gamePlay, game) => this.getWitchGamePlayEligibleTargets(gamePlay, game),
};

private readonly canBeSkippedPlayMethods: Partial<Record<GamePlaySourceName, (gamePlay: GamePlay, game: Game) => boolean>> = {
[PlayerGroups.CHARMED]: () => true,
[PlayerGroups.LOVERS]: () => true,
Expand All @@ -27,12 +49,82 @@ export class GamePlayAugmenterService {
[RoleNames.WITCH]: () => true,
};

public constructor(private readonly gameHistoryRecordService: GameHistoryRecordService) {}

public setGamePlayCanBeSkipped(gamePlay: GamePlay, game: Game): GamePlay {
const clonedGamePlay = createGamePlay(gamePlay);
clonedGamePlay.canBeSkipped = this.canGamePlayBeSkipped(gamePlay, game);
return clonedGamePlay;
}

public async setGamePlayEligibleTargets(gamePlay: GamePlay, game: Game): Promise<GamePlay> {
const clonedGamePlay = createGamePlay(gamePlay);
clonedGamePlay.eligibleTargets = await this.getGamePlayEligibleTargets(gamePlay, game);
return clonedGamePlay;
}

private getBigBadWolfGamePlayEligibleTargets(gamePlay: GamePlay, game: Game): GamePlayEligibleTargets {
const leftToEatByBigBadWolfPlayers = getLeftToEatByWerewolvesPlayers(game);
const leftToEatByBigBadWolfPlayersCount = leftToEatByBigBadWolfPlayers.length ? 1 : 0;
return createGamePlayEligibleTargets({ boundaries: { min: leftToEatByBigBadWolfPlayersCount, max: leftToEatByBigBadWolfPlayersCount } });
}

private getCupidGamePlayEligibleTargets(): GamePlayEligibleTargets {
return createGamePlayEligibleTargets({ boundaries: { min: 2, max: 2 } });
}

private getFoxGamePlayEligibleTargets(): GamePlayEligibleTargets {
return createGamePlayEligibleTargets({ boundaries: { min: 0, max: 1 } });
}

private getPiedPiperGamePlayEligibleTargets(gamePlay: GamePlay, game: Game): GamePlayEligibleTargets {
const leftToCharmByPiedPiperPlayers = getLeftToCharmByPiedPiperPlayers(game);
const { charmedPeopleCountPerNight } = game.options.roles.piedPiper;
const leftToCharmByPiedPiperPlayersCount = leftToCharmByPiedPiperPlayers.length;
const countToCharm = Math.min(charmedPeopleCountPerNight, leftToCharmByPiedPiperPlayersCount);
return createGamePlayEligibleTargets({ boundaries: { min: countToCharm, max: countToCharm } });
}

private getRavenGamePlayEligibleTargets(): GamePlayEligibleTargets {
return createGamePlayEligibleTargets({ boundaries: { min: 0, max: 1 } });
}

private getScapegoatGamePlayEligibleTargets(gamePlay: GamePlay, game: Game): GamePlayEligibleTargets {
const alivePlayers = getAlivePlayers(game);
return createGamePlayEligibleTargets({ boundaries: { min: 0, max: alivePlayers.length } });
}

private getWhiteWerewolfGamePlayEligibleTargets(gamePlay: GamePlay, game: Game): GamePlayEligibleTargets {
const leftToEatByWhiteWerewolfPlayers = getLeftToEatByWhiteWerewolfPlayers(game);
const max = Math.min(1, leftToEatByWhiteWerewolfPlayers.length);
return createGamePlayEligibleTargets({ boundaries: { min: 0, max } });
}

private async getWitchGamePlayEligibleTargets(gamePlay: GamePlay, game: Game): Promise<GamePlayEligibleTargets> {
const hasWitchUsedLifePotion = (await this.gameHistoryRecordService.getGameHistoryWitchUsesSpecificPotionRecords(game._id, WitchPotions.LIFE)).length > 0;
const hasWitchUsedDeathPotion = (await this.gameHistoryRecordService.getGameHistoryWitchUsesSpecificPotionRecords(game._id, WitchPotions.DEATH)).length > 0;
let max = 2;
if (hasWitchUsedLifePotion) {
max--;
}
if (hasWitchUsedDeathPotion) {
max--;
}
return createGamePlayEligibleTargets({ boundaries: { min: 0, max } });
}

private getSingleTargetGamePlayEligibleTargets(): GamePlayEligibleTargets {
return createGamePlayEligibleTargets({ boundaries: { min: 1, max: 1 } });
}

private async getGamePlayEligibleTargets(gamePlay: GamePlay, game: Game): Promise<GamePlayEligibleTargets | undefined> {
const eligibleTargetsPlayMethod = this.getEligibleTargetsPlayMethods[gamePlay.source.name];
if (!eligibleTargetsPlayMethod) {
return undefined;
}
return eligibleTargetsPlayMethod(gamePlay, game);
}

private canSurvivorsSkipGamePlay(gamePlay: GamePlay, game: Game): boolean {
const { canBeSkipped } = game.options.votes;
const isGamePlayVoteCauseAngelPresence = gamePlay.action === GamePlayActions.VOTE && gamePlay.cause === GamePlayCauses.ANGEL_PRESENCE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,26 +103,8 @@ export class GamePlayValidatorService {
}
this.validateGamePlayTargetsBoundaries(infectedTargets, { min: 1, max: 1 });
}

private validateWerewolvesTargetsBoundaries(playTargets: MakeGamePlayTargetWithRelationsDto[], game: GameWithCurrentPlay): void {
const leftToEatByWerewolvesPlayers = getLeftToEatByWerewolvesPlayers(game);
const leftToEatByWhiteWerewolfPlayers = getLeftToEatByWhiteWerewolfPlayers(game);
const bigBadWolfExpectedTargetsCount = leftToEatByWerewolvesPlayers.length ? 1 : 0;
const whiteWerewolfMaxTargetsCount = leftToEatByWhiteWerewolfPlayers.length ? 1 : 0;
const werewolvesSourceTargetsBoundaries: Partial<Record<GamePlaySourceName, { min: number; max: number }>> = {
[PlayerGroups.WEREWOLVES]: { min: 1, max: 1 },
[RoleNames.BIG_BAD_WOLF]: { min: bigBadWolfExpectedTargetsCount, max: bigBadWolfExpectedTargetsCount },
[RoleNames.WHITE_WEREWOLF]: { min: 0, max: whiteWerewolfMaxTargetsCount },
};
const targetsBoundaries = werewolvesSourceTargetsBoundaries[game.currentPlay.source.name];
if (!targetsBoundaries) {
return;
}
this.validateGamePlayTargetsBoundaries(playTargets, targetsBoundaries);
}

private async validateGamePlayWerewolvesTargets(playTargets: MakeGamePlayTargetWithRelationsDto[], game: GameWithCurrentPlay): Promise<void> {
this.validateWerewolvesTargetsBoundaries(playTargets, game);
if (!playTargets.length) {
return;
}
Expand All @@ -144,37 +126,32 @@ export class GamePlayValidatorService {
}

private validateGamePlayHunterTargets(playTargets: MakeGamePlayTargetWithRelationsDto[]): void {
this.validateGamePlayTargetsBoundaries(playTargets, { min: 1, max: 1 });
const targetedPlayer = playTargets[0].player;
if (!targetedPlayer.isAlive) {
throw new BadGamePlayPayloadException(BadGamePlayPayloadReasons.BAD_HUNTER_TARGET);
}
}

private validateGamePlayScapegoatTargets(playTargets: MakeGamePlayTargetWithRelationsDto[], game: Game): void {
this.validateGamePlayTargetsBoundaries(playTargets, { min: 0, max: game.players.length });
private validateGamePlayScapegoatTargets(playTargets: MakeGamePlayTargetWithRelationsDto[]): void {
if (playTargets.some(({ player }) => !player.isAlive)) {
throw new BadGamePlayPayloadException(BadGamePlayPayloadReasons.BAD_SCAPEGOAT_TARGETS);
}
}

private validateGamePlayCupidTargets(playTargets: MakeGamePlayTargetWithRelationsDto[]): void {
this.validateGamePlayTargetsBoundaries(playTargets, { min: 2, max: 2 });
if (playTargets.some(({ player }) => !player.isAlive)) {
throw new BadGamePlayPayloadException(BadGamePlayPayloadReasons.BAD_CUPID_TARGETS);
}
}

private validateGamePlayFoxTargets(playTargets: MakeGamePlayTargetWithRelationsDto[]): void {
this.validateGamePlayTargetsBoundaries(playTargets, { min: 0, max: 1 });
const targetedPlayer = playTargets[0].player;
if (!targetedPlayer.isAlive) {
throw new BadGamePlayPayloadException(BadGamePlayPayloadReasons.BAD_FOX_TARGET);
}
}

private validateGamePlaySeerTargets(playTargets: MakeGamePlayTargetWithRelationsDto[], game: Game): void {
this.validateGamePlayTargetsBoundaries(playTargets, { min: 1, max: 1 });
const seerPlayer = getPlayerWithCurrentRole(game, RoleNames.SEER);
const targetedPlayer = playTargets[0].player;
if (!targetedPlayer.isAlive || seerPlayer?._id.equals(targetedPlayer._id) === true) {
Expand All @@ -183,14 +160,12 @@ export class GamePlayValidatorService {
}

private validateGamePlayRavenTargets(playTargets: MakeGamePlayTargetWithRelationsDto[]): void {
this.validateGamePlayTargetsBoundaries(playTargets, { min: 0, max: 1 });
if (playTargets.length && !playTargets[0].player.isAlive) {
throw new BadGamePlayPayloadException(BadGamePlayPayloadReasons.BAD_RAVEN_TARGET);
}
}

private validateGamePlayWildChildTargets(playTargets: MakeGamePlayTargetWithRelationsDto[], game: Game): void {
this.validateGamePlayTargetsBoundaries(playTargets, { min: 1, max: 1 });
const wildChildPlayer = getPlayerWithCurrentRole(game, RoleNames.WILD_CHILD);
const targetedPlayer = playTargets[0].player;
if (!targetedPlayer.isAlive || wildChildPlayer?._id.equals(targetedPlayer._id) === true) {
Expand All @@ -199,19 +174,14 @@ export class GamePlayValidatorService {
}

private validateGamePlayPiedPiperTargets(playTargets: MakeGamePlayTargetWithRelationsDto[], game: Game): void {
const { charmedPeopleCountPerNight } = game.options.roles.piedPiper;
const leftToCharmByPiedPiperPlayers = getLeftToCharmByPiedPiperPlayers(game);
const leftToCharmByPiedPiperPlayersCount = leftToCharmByPiedPiperPlayers.length;
const countToCharm = Math.min(charmedPeopleCountPerNight, leftToCharmByPiedPiperPlayersCount);
this.validateGamePlayTargetsBoundaries(playTargets, { min: countToCharm, max: countToCharm });
if (playTargets.some(({ player }) => !leftToCharmByPiedPiperPlayers.find(({ _id }) => player._id.equals(_id)))) {
throw new BadGamePlayPayloadException(BadGamePlayPayloadReasons.BAD_PIED_PIPER_TARGETS);
}
}

private async validateGamePlayGuardTargets(playTargets: MakeGamePlayTargetWithRelationsDto[], game: Game): Promise<void> {
const { canProtectTwice } = game.options.roles.guard;
this.validateGamePlayTargetsBoundaries(playTargets, { min: 1, max: 1 });
const lastGuardHistoryRecord = await this.gameHistoryRecordService.getLastGameHistoryGuardProtectsRecord(game._id);
const lastProtectedPlayer = lastGuardHistoryRecord?.play.targets?.[0].player;
const targetedPlayer = playTargets[0].player;
Expand All @@ -221,7 +191,6 @@ export class GamePlayValidatorService {
}

private async validateGamePlaySheriffTargets(playTargets: MakeGamePlayTargetWithRelationsDto[], game: GameWithCurrentPlay): Promise<void> {
this.validateGamePlayTargetsBoundaries(playTargets, { min: 1, max: 1 });
const targetedPlayer = playTargets[0].player;
if (game.currentPlay.action === GamePlayActions.DELEGATE && !targetedPlayer.isAlive) {
throw new BadGamePlayPayloadException(BadGamePlayPayloadReasons.BAD_SHERIFF_DELEGATE_TARGET);
Expand Down Expand Up @@ -256,7 +225,7 @@ export class GamePlayValidatorService {
[RoleNames.SEER]: () => this.validateGamePlaySeerTargets(playTargets, game),
[RoleNames.FOX]: () => this.validateGamePlayFoxTargets(playTargets),
[RoleNames.CUPID]: () => this.validateGamePlayCupidTargets(playTargets),
[RoleNames.SCAPEGOAT]: () => this.validateGamePlayScapegoatTargets(playTargets, game),
[RoleNames.SCAPEGOAT]: () => this.validateGamePlayScapegoatTargets(playTargets),
[RoleNames.HUNTER]: () => this.validateGamePlayHunterTargets(playTargets),
[RoleNames.WITCH]: async() => this.validateGamePlayWitchTargets(playTargets, game),
};
Expand All @@ -280,18 +249,22 @@ export class GamePlayValidatorService {

private async validateGamePlayTargetsWithRelationsDto(playTargets: MakeGamePlayTargetWithRelationsDto[] | undefined, game: GameWithCurrentPlay): Promise<void> {
const targetActions: GamePlayActions[] = [...TARGET_ACTIONS];
const { currentPlay } = game;
if (!targetActions.includes(game.currentPlay.action)) {
if (playTargets !== undefined && playTargets.length > 0) {
throw new BadGamePlayPayloadException(BadGamePlayPayloadReasons.UNEXPECTED_TARGETS);
}
return;
}
if (playTargets === undefined || playTargets.length === 0) {
if (game.currentPlay.canBeSkipped === false) {
if (currentPlay.canBeSkipped === false) {
throw new BadGamePlayPayloadException(BadGamePlayPayloadReasons.REQUIRED_TARGETS);
}
return;
}
if (currentPlay.eligibleTargets?.boundaries) {
this.validateGamePlayTargetsBoundaries(playTargets, currentPlay.eligibleTargets.boundaries);
}
this.validateInfectedTargetsAndPotionUsage(playTargets, game);
await this.validateGamePlaySourceTargets(playTargets, game);
}
Expand All @@ -303,7 +276,7 @@ export class GamePlayValidatorService {
throw new BadGamePlayPayloadException(BadGamePlayPayloadReasons.BAD_VOTE_TARGET_FOR_TIE_BREAKER);
}
}

private validateGamePlayVotesWithRelationsDtoSourceAndTarget(playVotes: MakeGamePlayVoteWithRelationsDto[], game: Game): void {
if (playVotes.some(({ source }) => !source.isAlive || doesPlayerHaveActiveAttributeWithName(source, PlayerAttributeNames.CANT_VOTE, game))) {
throw new BadGamePlayPayloadException(BadGamePlayPayloadReasons.BAD_VOTE_SOURCE);
Expand All @@ -315,7 +288,7 @@ export class GamePlayValidatorService {
throw new BadGamePlayPayloadException(BadGamePlayPayloadReasons.SAME_SOURCE_AND_TARGET_VOTE);
}
}

private async validateGamePlayVotesWithRelationsDto(playVotes: MakeGamePlayVoteWithRelationsDto[] | undefined, game: GameWithCurrentPlay): Promise<void> {
const { currentPlay } = game;
const voteActions: GamePlayActions[] = [...VOTE_ACTIONS];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,22 @@ export class GamePlayService {
return clonedGame;
}

public proceedToNextGamePlay(game: Game): Game {
public async proceedToNextGamePlay(game: Game): Promise<Game> {
let clonedGame = createGame(game);
if (!clonedGame.upcomingPlays.length) {
clonedGame.currentPlay = null;
return clonedGame;
}
clonedGame.currentPlay = clonedGame.upcomingPlays[0];
clonedGame.upcomingPlays.shift();
clonedGame = this.augmentCurrentGamePlay(clonedGame as GameWithCurrentPlay);
clonedGame = await this.augmentCurrentGamePlay(clonedGame as GameWithCurrentPlay);
return clonedGame;
}

public augmentCurrentGamePlay(game: GameWithCurrentPlay): GameWithCurrentPlay {
public async augmentCurrentGamePlay(game: GameWithCurrentPlay): Promise<GameWithCurrentPlay> {
const clonedGame = createGameWithCurrentGamePlay(game);
clonedGame.currentPlay = this.gamePlayAugmenterService.setGamePlayCanBeSkipped(clonedGame.currentPlay, clonedGame);
clonedGame.currentPlay = await this.gamePlayAugmenterService.setGamePlayEligibleTargets(clonedGame.currentPlay, clonedGame);
clonedGame.currentPlay.source.players = getExpectedPlayersToPlay(clonedGame);
return clonedGame;
}
Expand Down
6 changes: 3 additions & 3 deletions src/modules/game/providers/services/game.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class GameService {
upcomingPlays,
});
let createdGame = await this.gameRepository.create(gameToCreate) as GameWithCurrentPlay;
createdGame = this.gamePlayService.augmentCurrentGamePlay(createdGame);
createdGame = await this.gamePlayService.augmentCurrentGamePlay(createdGame);
return this.updateGame(createdGame._id, createdGame);
}

Expand All @@ -75,7 +75,7 @@ export class GameService {
await this.gamePlayValidatorService.validateGamePlayWithRelationsDto(play, clonedGame);
clonedGame = await this.gamePlayMakerService.makeGamePlay(play, clonedGame);
clonedGame = await this.gamePlayService.refreshUpcomingPlays(clonedGame);
clonedGame = this.gamePlayService.proceedToNextGamePlay(clonedGame);
clonedGame = await this.gamePlayService.proceedToNextGamePlay(clonedGame);
clonedGame.tick++;
if (isGamePhaseOver(clonedGame)) {
clonedGame = await this.handleGamePhaseCompletion(clonedGame);
Expand All @@ -94,7 +94,7 @@ export class GameService {
clonedGame = this.playerAttributeService.decreaseRemainingPhasesAndRemoveObsoletePlayerAttributes(clonedGame);
clonedGame = await this.gamePhaseService.switchPhaseAndAppendGamePhaseUpcomingPlays(clonedGame);
clonedGame = this.gamePhaseService.applyStartingGamePhaseOutcomes(clonedGame);
clonedGame = this.gamePlayService.proceedToNextGamePlay(clonedGame);
clonedGame = await this.gamePlayService.proceedToNextGamePlay(clonedGame);
if (isGamePhaseOver(clonedGame)) {
clonedGame = await this.handleGamePhaseCompletion(clonedGame);
}
Expand Down

0 comments on commit 2b7b72b

Please sign in to comment.