Skip to content

Commit

Permalink
feat(game-play): handle sheriff election tie in votes (#314)
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinezanardi authored Jul 5, 2023
1 parent 460e242 commit 09c8606
Show file tree
Hide file tree
Showing 13 changed files with 27,968 additions and 26,773 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { ApiPropertyOptions } from "@nestjs/swagger";
import { GAME_HISTORY_RECORD_VOTING_RESULTS } from "../../../enums/game-history-record.enum";
import type { GameHistoryRecordPlayVoting } from "../../../schemas/game-history-record/game-history-record-play/game-history-record-play-voting.schema";

const gameHistoryRecordPlayVotingFieldsSpecs = Object.freeze<Record<keyof GameHistoryRecordPlayVoting, ApiPropertyOptions>>({
result: {
required: true,
enum: GAME_HISTORY_RECORD_VOTING_RESULTS,
},
nominatedPlayers: { required: false },
});

const gameHistoryRecordPlayVotingApiProperties = Object.freeze<Record<keyof GameHistoryRecordPlayVoting, ApiPropertyOptions>>({
result: {
description: "Define the results and their consequences",
...gameHistoryRecordPlayVotingFieldsSpecs.result,
},
nominatedPlayers: {
description: "Nominated players from the play votes",
...gameHistoryRecordPlayVotingFieldsSpecs.nominatedPlayers,
},
});

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

Expand All @@ -12,10 +11,7 @@ const gameHistoryRecordPlayFieldsSpecs = Object.freeze<Record<keyof GameHistoryR
source: { required: true },
targets: { required: false },
votes: { required: false },
votingResult: {
required: false,
enum: GAME_HISTORY_RECORD_VOTING_RESULTS,
},
voting: { required: false },
didJudgeRequestAnotherVote: { required: false },
chosenCard: { required: false },
chosenSide: {
Expand All @@ -34,16 +30,16 @@ const gameHistoryRecordPlayApiProperties = Object.freeze<Record<keyof GameHistor
...gameHistoryRecordPlayFieldsSpecs.source,
},
targets: {
description: "Players affected by the play. When `votes` are set, `targets` are the players nominated from the vote",
description: "Players affected by the play.",
...gameHistoryRecordPlayFieldsSpecs.targets,
},
votes: {
description: "Play's votes",
...gameHistoryRecordPlayFieldsSpecs.targets,
},
votingResult: {
description: "Only if `votes` are set, define the results and their consequences",
...gameHistoryRecordPlayFieldsSpecs.votingResult,
voting: {
description: "Only if `votes` are set, voting summary and nominated players if applicable",
...gameHistoryRecordPlayFieldsSpecs.voting,
},
didJudgeRequestAnotherVote: {
description: "Only if there is the `stuttering judge` in the game and `action` is either `vote` or `settle-votes`. If set to `true`, there is another vote planned after this play",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class GameHistoryRecordRepository {
const filter: FilterQuery<GameHistoryRecord> = {
gameId,
"play.action": GAME_PLAY_ACTIONS.VOTE,
"play.votingResult": GAME_HISTORY_RECORD_VOTING_RESULTS.TIE,
"play.voting.result": GAME_HISTORY_RECORD_VOTING_RESULTS.TIE,
};
return this.gameHistoryRecordModel.findOne(filter, undefined, { sort: { createdAt: -1 } });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Injectable } from "@nestjs/common";
import { cloneDeep } from "lodash";
import { cloneDeep, sample } from "lodash";
import { createFakeGamePlayAllElectSheriff } from "../../../../../../tests/factories/game/schemas/game-play/game-play.schema.factory";
import { roles } from "../../../../role/constants/role.constant";
import { ROLE_NAMES, ROLE_SIDES } from "../../../../role/enums/role.enum";
import type { MakeGamePlayVoteWithRelationsDto } from "../../../dto/make-game-play/make-game-play-vote/make-game-play-vote-with-relations.dto";
import type { MakeGamePlayWithRelationsDto } from "../../../dto/make-game-play/make-game-play-with-relations.dto";
import { GAME_HISTORY_RECORD_VOTING_RESULTS } from "../../../enums/game-history-record.enum";
import { GAME_PLAY_ACTIONS, GAME_PLAY_CAUSES, WITCH_POTIONS } from "../../../enums/game-play.enum";
import { PLAYER_ATTRIBUTE_NAMES, PLAYER_DEATH_CAUSES, PLAYER_GROUPS } from "../../../enums/player.enum";
import { createGamePlayAllVote, createGamePlaySheriffSettlesVotes } from "../../../helpers/game-play/game-play.factory";
Expand All @@ -19,7 +19,6 @@ import type { PlayerSide } from "../../../schemas/player/player-side.schema";
import type { Player } from "../../../schemas/player/player.schema";
import type { PlayerVoteCount } from "../../../types/game-play.type";
import type { GameSource } from "../../../types/game.type";
import { GameHistoryRecordService } from "../game-history/game-history-record.service";
import { PlayerKillerService } from "../player/player-killer.service";

@Injectable()
Expand All @@ -44,10 +43,7 @@ export class GamePlayMakerService {
[PLAYER_ATTRIBUTE_NAMES.SHERIFF]: async(play, game) => this.sheriffPlays(play, game),
};

public constructor(
private readonly playerKillerService: PlayerKillerService,
private readonly gameHistoryRecordService: GameHistoryRecordService,
) {}
public constructor(private readonly playerKillerService: PlayerKillerService) {}

public async makeGamePlay(play: MakeGamePlayWithRelationsDto, game: Game): Promise<Game> {
const clonedGame = cloneDeep(game);
Expand Down Expand Up @@ -152,8 +148,7 @@ export class GamePlayMakerService {
const gamePlaySheriffSettlesVotes = createGamePlaySheriffSettlesVotes();
return prependUpcomingPlayInGame(gamePlaySheriffSettlesVotes, clonedGame);
}
const previousGameHistoryRecord = await this.gameHistoryRecordService.getPreviousGameHistoryRecord(clonedGame._id);
if (previousGameHistoryRecord?.play.votingResult !== GAME_HISTORY_RECORD_VOTING_RESULTS.TIE) {
if (clonedGame.currentPlay.cause !== GAME_PLAY_CAUSES.PREVIOUS_VOTES_WERE_IN_TIES) {
const gamePlayAllVote = createGamePlayAllVote({ cause: GAME_PLAY_CAUSES.PREVIOUS_VOTES_WERE_IN_TIES });
return prependUpcomingPlayInGame(gamePlayAllVote, clonedGame);
}
Expand All @@ -179,16 +174,34 @@ export class GamePlayMakerService {
}
return clonedGame;
}
private allElectSheriff({ votes }: MakeGamePlayWithRelationsDto, game: Game): Game {

private handleTieInSheriffElection(nominatedPlayers: Player[], game: Game): Game {
const clonedGame = cloneDeep(game);
if (clonedGame.currentPlay.cause !== GAME_PLAY_CAUSES.PREVIOUS_VOTES_WERE_IN_TIES) {
const gamePlayAllElectSheriff = createFakeGamePlayAllElectSheriff({ cause: GAME_PLAY_CAUSES.PREVIOUS_VOTES_WERE_IN_TIES });
return prependUpcomingPlayInGame(gamePlayAllElectSheriff, clonedGame);
}
const randomNominatedPlayer = sample(nominatedPlayers);
if (randomNominatedPlayer) {
const sheriffByAllPlayerAttribute = createSheriffByAllPlayerAttribute();
return addPlayerAttributeInGame(randomNominatedPlayer._id, clonedGame, sheriffByAllPlayerAttribute);
}
return clonedGame;
}

private allElectSheriff(play: MakeGamePlayWithRelationsDto, game: Game): Game {
const clonedGame = cloneDeep(game);
const { votes } = play;
if (!votes) {
return clonedGame;
}
const nominatedPlayers = this.getNominatedPlayers(votes, clonedGame);
if (nominatedPlayers.length !== 1) {
if (!nominatedPlayers.length) {
return clonedGame;
}
if (nominatedPlayers.length !== 1) {
return this.handleTieInSheriffElection(nominatedPlayers, clonedGame);
}
const sheriffByAllPlayerAttribute = createSheriffByAllPlayerAttribute();
return addPlayerAttributeInGame(nominatedPlayers[0]._id, clonedGame, sheriffByAllPlayerAttribute);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { optionalTargetsActions, requiredTargetsActions, requiredVotesActions, s
import type { MakeGamePlayTargetWithRelationsDto } from "../../../dto/make-game-play/make-game-play-target/make-game-play-target-with-relations.dto";
import type { MakeGamePlayVoteWithRelationsDto } from "../../../dto/make-game-play/make-game-play-vote/make-game-play-vote-with-relations.dto";
import type { MakeGamePlayWithRelationsDto } from "../../../dto/make-game-play/make-game-play-with-relations.dto";
import { GAME_PLAY_ACTIONS, WITCH_POTIONS } from "../../../enums/game-play.enum";
import { GAME_PLAY_ACTIONS, GAME_PLAY_CAUSES, WITCH_POTIONS } from "../../../enums/game-play.enum";
import { PLAYER_ATTRIBUTE_NAMES, PLAYER_GROUPS } from "../../../enums/player.enum";
import { getLeftToCharmByPiedPiperPlayers, getLeftToEatByWerewolvesPlayers, getLeftToEatByWhiteWerewolfPlayers, getPlayerWithCurrentRole } from "../../../helpers/game.helper";
import { doesPlayerHaveAttribute, isPlayerAliveAndPowerful, isPlayerOnVillagersSide, isPlayerOnWerewolvesSide } from "../../../helpers/player/player.helper";
Expand All @@ -22,7 +22,7 @@ export class GamePlayValidatorService {
const { votes, targets } = play;
await this.validateGamePlayWithRelationsDtoJudgeRequest(play, game);
this.validateGamePlayWithRelationsDtoChosenSide(play, game);
this.validateGamePlayVotesWithRelationsDto(votes, game);
await this.validateGamePlayVotesWithRelationsDto(votes, game);
await this.validateGamePlayTargetsWithRelationsDto(targets, game);
this.validateGamePlayWithRelationsDtoChosenCard(play, game);
}
Expand Down Expand Up @@ -204,8 +204,9 @@ export class GamePlayValidatorService {
throw new BadGamePlayPayloadException(BAD_GAME_PLAY_PAYLOAD_REASONS.BAD_SHERIFF_DELEGATE_TARGET);
}
const lastTieInVotesRecord = await this.gameHistoryRecordService.getLastGameHistoryTieInVotesRecord(game._id);
const lastTieInVotesRecordTargets = lastTieInVotesRecord?.play.targets ?? [];
if (game.currentPlay.action === GAME_PLAY_ACTIONS.SETTLE_VOTES && !lastTieInVotesRecordTargets.find(({ player }) => player._id === targetedPlayer._id)) {
const lastTieInVotesRecordNominatedPlayers = lastTieInVotesRecord?.play.voting?.nominatedPlayers ?? [];
const isSheriffTargetInLastNominatedPlayers = lastTieInVotesRecordNominatedPlayers.find(({ _id }) => _id === targetedPlayer._id);
if (game.currentPlay.action === GAME_PLAY_ACTIONS.SETTLE_VOTES && !isSheriffTargetInLastNominatedPlayers) {
throw new BadGamePlayPayloadException(BAD_GAME_PLAY_PAYLOAD_REASONS.BAD_SHERIFF_SETTLE_VOTES_TARGET);
}
}
Expand Down Expand Up @@ -268,7 +269,15 @@ export class GamePlayValidatorService {
await this.validateGamePlaySourceTargets(playTargets, game);
}

private validateGamePlayVotesWithRelationsDto(playVotes: MakeGamePlayVoteWithRelationsDto[] | undefined, game: Game): void {
private async validateGamePlayVotesTieBreakerWithRelationsDto(playVotes: MakeGamePlayVoteWithRelationsDto[], game: Game): Promise<void> {
const lastTieInVotesRecord = await this.gameHistoryRecordService.getLastGameHistoryTieInVotesRecord(game._id);
const lastTieInVotesRecordNominatedPlayers = lastTieInVotesRecord?.play.voting?.nominatedPlayers ?? [];
if (playVotes.some(vote => !lastTieInVotesRecordNominatedPlayers.find(player => vote.target._id === player._id))) {
throw new BadGamePlayPayloadException(BAD_GAME_PLAY_PAYLOAD_REASONS.BAD_VOTE_TARGET_FOR_TIE_BREAKER);
}
}

private async validateGamePlayVotesWithRelationsDto(playVotes: MakeGamePlayVoteWithRelationsDto[] | undefined, game: Game): Promise<void> {
if (playVotes === undefined || !playVotes.length) {
if (requiredVotesActions.includes(game.currentPlay.action)) {
throw new BadGamePlayPayloadException(BAD_GAME_PLAY_PAYLOAD_REASONS.REQUIRED_VOTES);
Expand All @@ -281,6 +290,9 @@ export class GamePlayValidatorService {
if (playVotes.some(({ source, target }) => source._id === target._id)) {
throw new BadGamePlayPayloadException(BAD_GAME_PLAY_PAYLOAD_REASONS.SAME_SOURCE_AND_TARGET_VOTE);
}
if (game.currentPlay.cause === GAME_PLAY_CAUSES.PREVIOUS_VOTES_WERE_IN_TIES) {
await this.validateGamePlayVotesTieBreakerWithRelationsDto(playVotes, game);
}
}

private validateGamePlayWithRelationsDtoChosenSide({ chosenSide }: MakeGamePlayWithRelationsDto, game: Game): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { gameHistoryRecordPlayVotingApiProperties, gameHistoryRecordPlayVotingFieldsSpecs } from "../../../constants/game-history-record/game-history-record-play/game-history-record-play-voting.constant";
import { GAME_HISTORY_RECORD_VOTING_RESULTS } from "../../../enums/game-history-record.enum";
import { Player, PlayerSchema } from "../../player/player.schema";

@Schema({
versionKey: false,
id: false,
_id: false,
})
class GameHistoryRecordPlayVoting {
@ApiProperty(gameHistoryRecordPlayVotingApiProperties.result)
@Prop({
required: gameHistoryRecordPlayVotingFieldsSpecs.result.required,
enum: gameHistoryRecordPlayVotingFieldsSpecs.result.enum,
})
@Expose()
public result: GAME_HISTORY_RECORD_VOTING_RESULTS;

@ApiProperty(gameHistoryRecordPlayVotingApiProperties.nominatedPlayers)
@Prop({
required: gameHistoryRecordPlayVotingFieldsSpecs.nominatedPlayers.required,
type: [PlayerSchema],
default: undefined,
})
@Type(() => Player)
@Expose()
public nominatedPlayers?: Player[];
}

const GameHistoryRecordPlayVotingSchema = SchemaFactory.createForClass(GameHistoryRecordPlayVoting);

export { GameHistoryRecordPlayVoting, GameHistoryRecordPlayVotingSchema };
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { ROLE_SIDES } from "../../../../role/enums/role.enum";
import { gameHistoryRecordPlayApiProperties, gameHistoryRecordPlayFieldsSpecs } from "../../../constants/game-history-record/game-history-record-play/game-history-record-play.constant";
import { GAME_HISTORY_RECORD_VOTING_RESULTS } from "../../../enums/game-history-record.enum";
import { GAME_PLAY_ACTIONS } from "../../../enums/game-play.enum";
import { GameAdditionalCardSchema, GameAdditionalCard } from "../../game-additional-card/game-additional-card.schema";
import { GameHistoryRecordPlaySource, GameHistoryRecordPlaySourceSchema } from "./game-history-record-play-source.schema";
import { GameHistoryRecordPlayTargetSchema, GameHistoryRecordPlayTarget } from "./game-history-record-play-target.schema";
import { GameHistoryRecordPlayVoteSchema, GameHistoryRecordPlayVote } from "./game-history-record-play-vote.schema";
import { GameHistoryRecordPlayVoting, GameHistoryRecordPlayVotingSchema } from "./game-history-record-play-voting.schema";

@Schema({
versionKey: false,
Expand Down Expand Up @@ -53,13 +53,14 @@ class GameHistoryRecordPlay {
@Expose()
public votes?: GameHistoryRecordPlayVote[];

@ApiProperty(gameHistoryRecordPlayApiProperties.votingResult)
@ApiProperty(gameHistoryRecordPlayApiProperties.voting)
@Prop({
required: gameHistoryRecordPlayFieldsSpecs.votingResult.required,
enum: gameHistoryRecordPlayFieldsSpecs.votingResult.enum,
required: gameHistoryRecordPlayFieldsSpecs.voting.required,
type: GameHistoryRecordPlayVotingSchema,
})
@Type(() => GameHistoryRecordPlayVoting)
@Expose()
public votingResult?: GAME_HISTORY_RECORD_VOTING_RESULTS;
public voting?: GameHistoryRecordPlayVoting;

@ApiProperty(gameHistoryRecordPlayApiProperties.didJudgeRequestAnotherVote)
@Prop({ required: gameHistoryRecordPlayFieldsSpecs.didJudgeRequestAnotherVote.required })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
enum BAD_GAME_PLAY_PAYLOAD_REASONS {
NO_UPCOMING_GAME_PLAY = "Game doesn't have upcoming plays",
UNEXPECTED_STUTTERING_JUDGE_VOTE_REQUEST = "`doesJudgeRequestAnotherVote` can't be set on this current game's state",
UNEXPECTED_CHOSEN_SIDE = "`chosenSide` can't be set on this current game's state",
REQUIRED_CHOSEN_SIDE = "`chosenSide` is required on this current game's state",
Expand All @@ -8,6 +7,7 @@ enum BAD_GAME_PLAY_PAYLOAD_REASONS {
UNEXPECTED_VOTES = "`votes` can't be set on this current game's state",
REQUIRED_VOTES = "`votes` is required on this current game's state",
SAME_SOURCE_AND_TARGET_VOTE = "One vote has the same source and target",
BAD_VOTE_TARGET_FOR_TIE_BREAKER = "One vote's target is not in the previous tie in votes",
UNEXPECTED_TARGETS = "`targets` can't be set on this current game's state",
REQUIRED_TARGETS = "`targets` is required on this current game's state",
TOO_LESS_TARGETS = "There are too less targets for this current game's state",
Expand Down
Loading

0 comments on commit 09c8606

Please sign in to comment.