Skip to content

Commit

Permalink
feat(player-death): new game play for survivors to bury dead bodies (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinezanardi committed Nov 18, 2023
1 parent 18f0168 commit 2cbbbe7
Show file tree
Hide file tree
Showing 42 changed files with 34,512 additions and 33,190 deletions.
17 changes: 17 additions & 0 deletions .run/Checkout to main.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Checkout to main" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="git checkout main &amp;&amp; git pull origin main &amp;&amp; exit;" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/bin/zsh" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>
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 @@ -12,6 +12,11 @@ const GAME_SOURCES = [
] as const satisfies Readonly<(PlayerAttributeNames | PlayerGroups | RoleNames)[]>;

const GAME_PLAYS_PRIORITY_LIST: ReadonlyDeep<GamePlay[]> = [
{
source: { name: PlayerGroups.SURVIVORS },
action: GamePlayActions.BURY_DEAD_BODIES,
occurrence: GamePlayOccurrences.CONSEQUENTIAL,
},
{
source: { name: RoleNames.HUNTER },
action: GamePlayActions.SHOOT,
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 @@ -17,6 +17,7 @@ enum GamePlayActions {
VOTE = "vote",
DELEGATE = "delegate",
SETTLE_VOTES = "settle-votes",
BURY_DEAD_BODIES = "bury-dead-bodies",
}

enum GamePlayCauses {
Expand Down
10 changes: 10 additions & 0 deletions src/modules/game/helpers/game-play/game-play.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ import { RoleNames } from "@/modules/role/enums/role.enum";

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

function createGamePlaySurvivorsBuryDeadBodies(gamePlay: Partial<GamePlay> = {}): GamePlay {
return createGamePlay({
source: createGamePlaySource({ name: PlayerGroups.SURVIVORS }),
action: GamePlayActions.BURY_DEAD_BODIES,
occurrence: GamePlayOccurrences.CONSEQUENTIAL,
...gamePlay,
});
}

function createGamePlaySheriffSettlesVotes(gamePlay: Partial<GamePlay> = {}): GamePlay {
return createGamePlay({
source: createGamePlaySource({ name: PlayerAttributeNames.SHERIFF }),
Expand Down Expand Up @@ -239,6 +248,7 @@ function createGamePlay(gamePlay: GamePlay): GamePlay {
}

export {
createGamePlaySurvivorsBuryDeadBodies,
createGamePlaySheriffSettlesVotes,
createGamePlaySheriffDelegates,
createGamePlaySurvivorsVote,
Expand Down
8 changes: 8 additions & 0 deletions src/modules/game/helpers/game.helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Types } from "mongoose";

import type { GamePlayActions } from "@/modules/game/enums/game-play.enum";
import type { CreateGamePlayerDto } from "@/modules/game/dto/create-game/create-game-player/create-game-player.dto";
import type { CreateGameDto } from "@/modules/game/dto/create-game/create-game.dto";
import { PlayerAttributeNames, PlayerGroups } from "@/modules/game/enums/player.enum";
Expand Down Expand Up @@ -181,6 +182,12 @@ function getAllowedToVotePlayers(game: Game): Player[] {
return game.players.filter(player => player.isAlive && !doesPlayerHaveActiveAttributeWithName(player, PlayerAttributeNames.CANT_VOTE, game));
}

function doesGameHaveCurrentOrUpcomingPlaySourceAndAction(game: Game, source: GameSource, action: GamePlayActions): boolean {
const { currentPlay, upcomingPlays } = game;
const gamePlays = currentPlay ? [currentPlay, ...upcomingPlays] : upcomingPlays;
return gamePlays.some(play => play.source.name === source && play.action === action);
}

export {
getPlayerDtoWithRole,
getPlayerWithCurrentRole,
Expand Down Expand Up @@ -210,4 +217,5 @@ export {
getFoxSniffedPlayers,
getNearestAliveNeighbor,
getAllowedToVotePlayers,
doesGameHaveCurrentOrUpcomingPlaySourceAndAction,
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { createCantFindLastNominatedPlayersUnexpectedException, createMalformedC
@Injectable()
export class GamePlayAugmenterService {
private readonly getEligibleTargetsPlayMethods: Partial<
Record<GamePlaySourceName, (game: Game, gamePlay: GamePlay) => GamePlayEligibleTargets | Promise<GamePlayEligibleTargets>>
Record<GamePlaySourceName, (game: Game, gamePlay: GamePlay) => GamePlayEligibleTargets | Promise<GamePlayEligibleTargets | undefined>>
> = {
[PlayerAttributeNames.SHERIFF]: async(game, gamePlay) => this.getSheriffGamePlayEligibleTargets(game, gamePlay),
[PlayerGroups.SURVIVORS]: async(game, gamePlay) => this.getSurvivorsGamePlayEligibleTargets(game, gamePlay),
Expand Down Expand Up @@ -144,7 +144,10 @@ export class GamePlayAugmenterService {
return createGamePlayEligibleTargets({ interactablePlayers, boundaries });
}

private async getSurvivorsGamePlayEligibleTargets(game: Game, gamePlay: GamePlay): Promise<GamePlayEligibleTargets> {
private async getSurvivorsGamePlayEligibleTargets(game: Game, gamePlay: GamePlay): Promise<GamePlayEligibleTargets | undefined> {
if (gamePlay.action === GamePlayActions.BURY_DEAD_BODIES) {
return undefined;
}
if (gamePlay.action === GamePlayActions.VOTE) {
return this.getSurvivorsVoteGamePlayEligibleTargets(game, gamePlay);
}
Expand Down Expand Up @@ -304,7 +307,7 @@ export class GamePlayAugmenterService {
return undefined;
}
const eligibleTargets = await eligibleTargetsPlayMethod(game, gamePlay);
const areEligibleTargetsRelevant = eligibleTargets.interactablePlayers !== undefined && eligibleTargets.interactablePlayers.length > 0;
const areEligibleTargetsRelevant = eligibleTargets?.interactablePlayers !== undefined && eligibleTargets.interactablePlayers.length > 0;
return areEligibleTargetsRelevant ? eligibleTargets : undefined;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Injectable } from "@nestjs/common";
import { createAngelGameVictory, createLoversGameVictory, createNoneGameVictory, createPiedPiperGameVictory, createVillagersGameVictory, createWerewolvesGameVictory, createWhiteWerewolfGameVictory } from "@/modules/game/helpers/game-victory/game-victory.factory";
import { GamePlayActions } from "@/modules/game/enums/game-play.enum";
import { GamePhases } from "@/modules/game/enums/game.enum";
import { PlayerAttributeNames, PlayerDeathCauses } from "@/modules/game/enums/player.enum";
import { areAllPlayersDead, getLeftToCharmByPiedPiperPlayers, getPlayersWithActiveAttributeName, getPlayersWithCurrentSide, getPlayerWithCurrentRole } from "@/modules/game/helpers/game.helper";
import { PlayerAttributeNames, PlayerDeathCauses, PlayerGroups } from "@/modules/game/enums/player.enum";
import { areAllPlayersDead, doesGameHaveCurrentOrUpcomingPlaySourceAndAction, getLeftToCharmByPiedPiperPlayers, getPlayersWithActiveAttributeName, getPlayersWithCurrentSide, getPlayerWithCurrentRole } from "@/modules/game/helpers/game.helper";
import { doesPlayerHaveActiveAttributeWithName } from "@/modules/game/helpers/player/player-attribute/player-attribute.helper";
import { isPlayerAliveAndPowerful, isPlayerPowerful } from "@/modules/game/helpers/player/player.helper";
import type { GameVictory } from "@/modules/game/schemas/game-victory/game-victory.schema";
Expand All @@ -16,13 +16,14 @@ import { createNoCurrentGamePlayUnexpectedException } from "@/shared/exception/h
@Injectable()
export class GameVictoryService {
public isGameOver(game: Game): boolean {
const { upcomingPlays, currentPlay } = game;
const { currentPlay } = game;
if (!currentPlay) {
throw createNoCurrentGamePlayUnexpectedException("isGameOver", { gameId: game._id });
}
const isShootPlayIncoming = !!upcomingPlays.find(({ action, source }) => action === GamePlayActions.SHOOT && source.name === RoleNames.HUNTER);
const isHunterShootPlayIncoming = doesGameHaveCurrentOrUpcomingPlaySourceAndAction(game, RoleNames.HUNTER, GamePlayActions.SHOOT);
const isSurvivorsBuryDeadBodiesPlayIncoming = doesGameHaveCurrentOrUpcomingPlaySourceAndAction(game, PlayerGroups.SURVIVORS, GamePlayActions.BURY_DEAD_BODIES);
const gameVictoryData = this.generateGameVictoryData(game);
return areAllPlayersDead(game) || currentPlay.action !== GamePlayActions.SHOOT && !isShootPlayIncoming && !!gameVictoryData;
return areAllPlayersDead(game) || !isHunterShootPlayIncoming && !isSurvivorsBuryDeadBodiesPlayIncoming && !!gameVictoryData;
}

public generateGameVictoryData(game: Game): GameVictory | undefined {
Expand Down
2 changes: 2 additions & 0 deletions src/modules/game/providers/services/game.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,11 @@ export class GameService {
clonedGame = this.playerAttributeService.decreaseRemainingPhasesAndRemoveObsoletePlayerAttributes(clonedGame);
clonedGame = await this.gamePhaseService.switchPhaseAndAppendGamePhaseUpcomingPlays(clonedGame);
clonedGame = this.gamePhaseService.applyStartingGamePhaseOutcomes(clonedGame);
clonedGame = await this.gamePlayService.refreshUpcomingPlays(clonedGame);
clonedGame = this.gamePlayService.proceedToNextGamePlay(clonedGame);
if (isGamePhaseOver(clonedGame)) {
clonedGame = await this.handleGamePhaseCompletion(clonedGame);
clonedGame = await this.gamePlayService.refreshUpcomingPlays(clonedGame);
}
return clonedGame;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Injectable } from "@nestjs/common";
import type { Types } from "mongoose";

import { PlayerAttributeNames, PlayerDeathCauses } from "@/modules/game/enums/player.enum";
import { createGamePlayHunterShoots, createGamePlayScapegoatBansVoting, createGamePlaySheriffDelegates } from "@/modules/game/helpers/game-play/game-play.factory";
import { GamePlayActions } from "@/modules/game/enums/game-play.enum";
import { PlayerAttributeNames, PlayerDeathCauses, PlayerGroups } from "@/modules/game/enums/player.enum";
import { createGamePlayHunterShoots, createGamePlayScapegoatBansVoting, createGamePlaySheriffDelegates, createGamePlaySurvivorsBuryDeadBodies } from "@/modules/game/helpers/game-play/game-play.factory";
import { createGame } from "@/modules/game/helpers/game.factory";
import { getAliveVillagerSidedPlayers, getNearestAliveNeighbor, getPlayerWithCurrentRole, getPlayerWithIdOrThrow } from "@/modules/game/helpers/game.helper";
import { doesGameHaveCurrentOrUpcomingPlaySourceAndAction, getAliveVillagerSidedPlayers, getNearestAliveNeighbor, getPlayerWithCurrentRole, getPlayerWithIdOrThrow } from "@/modules/game/helpers/game.helper";
import { addPlayerAttributeInGame, addPlayersAttributeInGame, prependUpcomingPlayInGame, updatePlayerInGame } from "@/modules/game/helpers/game.mutator";
import { createCantVoteBySurvivorsPlayerAttribute, createContaminatedByRustySwordKnightPlayerAttribute, createPowerlessByAncientPlayerAttribute } from "@/modules/game/helpers/player/player-attribute/player-attribute.factory";
import { doesPlayerHaveActiveAttributeWithName } from "@/modules/game/helpers/player/player-attribute/player-attribute.helper";
Expand Down Expand Up @@ -231,7 +232,11 @@ export class PlayerKillerService {
const cantFindPlayerException = createCantFindPlayerUnexpectedException("applyPlayerDeathOutcomes", { gameId: game._id, playerId: killedPlayer._id });
clonedGame = this.applyPlayerRoleDeathOutcomes(clonedPlayerToKill, clonedGame, death);
clonedPlayerToKill = getPlayerWithIdOrThrow(clonedPlayerToKill._id, clonedGame, cantFindPlayerException);
return this.applyPlayerAttributesDeathOutcomes(clonedPlayerToKill, clonedGame);
clonedGame = this.applyPlayerAttributesDeathOutcomes(clonedPlayerToKill, clonedGame);
if (!doesGameHaveCurrentOrUpcomingPlaySourceAndAction(clonedGame, PlayerGroups.SURVIVORS, GamePlayActions.BURY_DEAD_BODIES)) {
clonedGame = prependUpcomingPlayInGame(createGamePlaySurvivorsBuryDeadBodies(), clonedGame);
}
return clonedGame;
}

private killPlayer(playerToKill: Player, game: Game, death: PlayerDeath): Game {
Expand Down

0 comments on commit 2cbbbe7

Please sign in to comment.