Skip to content

Commit

Permalink
feat(game-history): generate and insert current game history for each…
Browse files Browse the repository at this point in the history
… play (#358)
  • Loading branch information
antoinezanardi committed Jul 31, 2023
1 parent d96c25e commit dfd80c8
Show file tree
Hide file tree
Showing 17 changed files with 38,794 additions and 34,730 deletions.
2 changes: 1 addition & 1 deletion config/eslint/constants.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const MAX_LENGTH = 180;
const MAX_NESTED_CALLBACK = 5;
const MAX_PARAMS = 6;
const MAX_PARAMS = 8;
const INDENT_SPACE_COUNT = 2;
const ERROR = "error";
const WARNING = "warn";
Expand Down
30 changes: 15 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/modules/game/game.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { GameHistoryRecordService } from "./providers/services/game-history/game
import { GamePhaseService } from "./providers/services/game-phase/game-phase.service";
import { GamePlayMakerService } from "./providers/services/game-play/game-play-maker.service";
import { GamePlayValidatorService } from "./providers/services/game-play/game-play-validator.service";
import { GamePlayVoteService } from "./providers/services/game-play/game-play-vote/game-play-vote.service";
import { GamePlayService } from "./providers/services/game-play/game-play.service";
import { GameRandomCompositionService } from "./providers/services/game-random-composition.service";
import { GameService } from "./providers/services/game.service";
Expand All @@ -31,6 +32,7 @@ import { Game, GameSchema } from "./schemas/game.schema";
GamePlayService,
GamePlayValidatorService,
GamePlayMakerService,
GamePlayVoteService,
GamePhaseService,
GameRepository,
GameHistoryRecordService,
Expand Down
25 changes: 24 additions & 1 deletion src/modules/game/helpers/game.helper.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { cloneDeep } from "lodash";
import type { Types } from "mongoose";
import { createCantFindPlayerUnexpectedException } from "../../../shared/exception/helpers/unexpected-exception.factory";
import { createCantFindPlayerUnexpectedException, createNoCurrentGamePlayUnexpectedException } from "../../../shared/exception/helpers/unexpected-exception.factory";
import { ROLE_NAMES, ROLE_SIDES } from "../../role/enums/role.enum";
import type { CreateGamePlayerDto } from "../dto/create-game/create-game-player/create-game-player.dto";
import { GAME_PLAY_ACTIONS } from "../enums/game-play.enum";
import { PLAYER_ATTRIBUTE_NAMES, PLAYER_GROUPS } from "../enums/player.enum";
import type { GameAdditionalCard } from "../schemas/game-additional-card/game-additional-card.schema";
import type { Game } from "../schemas/game.schema";
import type { Player } from "../schemas/player/player.schema";
import type { GameSource, GetNearestPlayerOptions } from "../types/game.type";
import { createPlayer } from "./player/player.factory";
import { doesPlayerHaveAttribute } from "./player/player.helper";

function getPlayerDtoWithRole(players: CreateGamePlayerDto[], role: ROLE_NAMES): CreateGamePlayerDto | undefined {
Expand Down Expand Up @@ -157,6 +159,26 @@ function getNearestAliveNeighbor(playerId: Types.ObjectId, game: Game, options:
}
}

function getExpectedPlayersToPlay(game: Game): Player[] {
const { players, currentPlay } = game;
const mustIncludeDeadPlayersGamePlayActions = [GAME_PLAY_ACTIONS.SHOOT, GAME_PLAY_ACTIONS.BAN_VOTING, GAME_PLAY_ACTIONS.DELEGATE];
let expectedPlayersToPlay: Player[] = [];
if (currentPlay === null) {
throw createNoCurrentGamePlayUnexpectedException("getExpectedPlayersToPlay", { gameId: game._id });
}
if (isGameSourceGroup(currentPlay.source)) {
expectedPlayersToPlay = getGroupOfPlayers(players, currentPlay.source);
} else if (isGameSourceRole(currentPlay.source)) {
expectedPlayersToPlay = getPlayersWithCurrentRole(players, currentPlay.source);
} else {
expectedPlayersToPlay = getPlayersWithAttribute(players, PLAYER_ATTRIBUTE_NAMES.SHERIFF);
}
if (!mustIncludeDeadPlayersGamePlayActions.includes(currentPlay.action)) {
expectedPlayersToPlay = expectedPlayersToPlay.filter(player => player.isAlive);
}
return expectedPlayersToPlay.map(player => createPlayer(player));
}

export {
getPlayerDtoWithRole,
getPlayerWithCurrentRole,
Expand All @@ -183,4 +205,5 @@ export {
getNonexistentPlayer,
getFoxSniffedPlayers,
getNearestAliveNeighbor,
getExpectedPlayersToPlay,
};
2 changes: 1 addition & 1 deletion src/modules/game/helpers/player/player.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { plainToInstanceDefaultOptions } from "../../../../shared/validation/con
import { Player } from "../../schemas/player/player.schema";

function createPlayer(player: Player): Player {
return plainToInstance(Player, JSON.parse(JSON.stringify(player)), plainToInstanceDefaultOptions);
return plainToInstance(Player, JSON.parse(JSON.stringify(player)), { ...plainToInstanceDefaultOptions, excludeExtraneousValues: true });
}

export { createPlayer };
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
import { Injectable } from "@nestjs/common";
import { plainToInstance } from "class-transformer";
import type { Types } from "mongoose";
import { API_RESOURCES } from "../../../../../shared/api/enums/api.enum";
import { RESOURCE_NOT_FOUND_REASONS } from "../../../../../shared/exception/enums/resource-not-found-error.enum";
import { createNoCurrentGamePlayUnexpectedException } from "../../../../../shared/exception/helpers/unexpected-exception.factory";
import { ResourceNotFoundException } from "../../../../../shared/exception/types/resource-not-found-exception.type";
import { plainToInstanceDefaultOptions } from "../../../../../shared/validation/constants/validation.constant";
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 type { WITCH_POTIONS } from "../../../enums/game-play.enum";
import { getAdditionalCardWithId, getNonexistentPlayer } from "../../../helpers/game.helper";
import type { GameHistoryRecordPlay } from "../../../schemas/game-history-record/game-history-record-play/game-history-record-play.schema";
import { GAME_PLAY_ACTIONS, GAME_PLAY_CAUSES } from "../../../enums/game-play.enum";
import { PLAYER_ATTRIBUTE_NAMES, PLAYER_DEATH_CAUSES } from "../../../enums/player.enum";
import { getAdditionalCardWithId, getExpectedPlayersToPlay, getNonexistentPlayer, getPlayerWithAttribute, getPlayerWithId } from "../../../helpers/game.helper";
import { GameHistoryRecordPlaySource } from "../../../schemas/game-history-record/game-history-record-play/game-history-record-play-source.schema";
import { GameHistoryRecordPlayVoting } from "../../../schemas/game-history-record/game-history-record-play/game-history-record-play-voting.schema";
import { GameHistoryRecordPlay } from "../../../schemas/game-history-record/game-history-record-play/game-history-record-play.schema";
import type { GameHistoryRecord } from "../../../schemas/game-history-record/game-history-record.schema";
import type { Game } from "../../../schemas/game.schema";
import type { GameHistoryRecordToInsert } from "../../../types/game-history-record.type";
import type { Player } from "../../../schemas/player/player.schema";
import { GameHistoryRecordToInsert } from "../../../types/game-history-record.type";
import type { GameWithCurrentPlay } from "../../../types/game-with-current-play";
import { GameHistoryRecordRepository } from "../../repositories/game-history-record.repository";
import { GameRepository } from "../../repositories/game.repository";
import { GamePlayVoteService } from "../game-play/game-play-vote/game-play-vote.service";

@Injectable()
export class GameHistoryRecordService {
public constructor(
private readonly gamePlayVoteService: GamePlayVoteService,
private readonly gameHistoryRecordRepository: GameHistoryRecordRepository,
private readonly gameRepository: GameRepository,
) {}

public async createGameHistoryRecord(gameHistoryRecordToInsert: GameHistoryRecordToInsert): Promise<GameHistoryRecord> {
await this.validateGameHistoryRecordToInsertData(gameHistoryRecordToInsert);
return this.gameHistoryRecordRepository.create(gameHistoryRecordToInsert);
Expand Down Expand Up @@ -55,6 +68,102 @@ export class GameHistoryRecordService {
public async getPreviousGameHistoryRecord(gameId: Types.ObjectId): Promise<GameHistoryRecord | null> {
return this.gameHistoryRecordRepository.getPreviousGameHistoryRecord(gameId);
}

public generateCurrentGameHistoryRecordToInsert(baseGame: Game, newGame: Game, play: MakeGamePlayWithRelationsDto): GameHistoryRecordToInsert {
if (baseGame.currentPlay === null) {
throw createNoCurrentGamePlayUnexpectedException("generateCurrentGameHistoryRecordToInsert", { gameId: baseGame._id });
}
const gameHistoryRecordToInsert: GameHistoryRecordToInsert = {
gameId: baseGame._id,
turn: baseGame.turn,
phase: baseGame.phase,
tick: baseGame.tick,
play: this.generateCurrentGameHistoryRecordPlayToInsert(baseGame as GameWithCurrentPlay, play),
revealedPlayers: this.generateCurrentGameHistoryRecordRevealedPlayersToInsert(baseGame, newGame),
deadPlayers: this.generateCurrentGameHistoryRecordDeadPlayersToInsert(baseGame, newGame),
};
if (gameHistoryRecordToInsert.play.votes) {
gameHistoryRecordToInsert.play.voting = this.generateCurrentGameHistoryRecordPlayVotingToInsert(baseGame as GameWithCurrentPlay, newGame, gameHistoryRecordToInsert);
}
return plainToInstance(GameHistoryRecordToInsert, gameHistoryRecordToInsert, { ...plainToInstanceDefaultOptions, enableCircularCheck: true });
}

private generateCurrentGameHistoryRecordDeadPlayersToInsert(baseGame: Game, newGame: Game): Player[] | undefined {
const { players: basePlayers } = baseGame;
const { players: newPlayers } = newGame;
const currentDeadPlayers = newPlayers.filter(player => {
const matchingBasePlayer = getPlayerWithId(basePlayers, player._id);
return matchingBasePlayer?.isAlive === true && !player.isAlive;
});
return currentDeadPlayers.length ? currentDeadPlayers : undefined;
}

private generateCurrentGameHistoryRecordRevealedPlayersToInsert(baseGame: Game, newGame: Game): Player[] | undefined {
const { players: basePlayers } = baseGame;
const { players: newPlayers } = newGame;
const currentRevealedPlayers = newPlayers.filter(player => {
const matchingBasePlayer = getPlayerWithId(basePlayers, player._id);
return matchingBasePlayer?.role.isRevealed === false && player.role.isRevealed && player.isAlive;
});
return currentRevealedPlayers.length ? currentRevealedPlayers : undefined;
}

private generateCurrentGameHistoryRecordPlayToInsert(baseGame: GameWithCurrentPlay, play: MakeGamePlayWithRelationsDto): GameHistoryRecordPlay {
const gameHistoryRecordPlayToInsert: GameHistoryRecordPlay = {
source: this.generateCurrentGameHistoryRecordPlaySourceToInsert(baseGame),
action: baseGame.currentPlay.action,
didJudgeRequestAnotherVote: play.doesJudgeRequestAnotherVote,
targets: play.targets,
votes: play.votes,
chosenCard: play.chosenCard,
chosenSide: play.chosenSide,
};
return plainToInstance(GameHistoryRecordPlay, gameHistoryRecordPlayToInsert, { ...plainToInstanceDefaultOptions, enableCircularCheck: true });
}

private generateCurrentGameHistoryRecordPlayVotingResultToInsert(
baseGame: GameWithCurrentPlay,
newGame: Game,
gameHistoryRecordToInsert: GameHistoryRecordToInsert,
): GAME_HISTORY_RECORD_VOTING_RESULTS {
const sheriffPlayer = getPlayerWithAttribute(newGame.players, PLAYER_ATTRIBUTE_NAMES.SHERIFF);
const areSomePlayersDeadFromCurrentVotes = gameHistoryRecordToInsert.deadPlayers?.some(({ death }) => {
const deathFromVoteCauses = [PLAYER_DEATH_CAUSES.VOTE, PLAYER_DEATH_CAUSES.VOTE_SCAPEGOATED];
return death?.cause !== undefined && deathFromVoteCauses.includes(death.cause);
}) === true;
if (baseGame.currentPlay.action === GAME_PLAY_ACTIONS.ELECT_SHERIFF) {
return sheriffPlayer ? GAME_HISTORY_RECORD_VOTING_RESULTS.SHERIFF_ELECTION : GAME_HISTORY_RECORD_VOTING_RESULTS.TIE;
}
if (areSomePlayersDeadFromCurrentVotes) {
return GAME_HISTORY_RECORD_VOTING_RESULTS.DEATH;
}
if (baseGame.currentPlay.cause === GAME_PLAY_CAUSES.PREVIOUS_VOTES_WERE_IN_TIES) {
return GAME_HISTORY_RECORD_VOTING_RESULTS.INCONSEQUENTIAL;
}
return GAME_HISTORY_RECORD_VOTING_RESULTS.TIE;
}

private generateCurrentGameHistoryRecordPlayVotingToInsert(
baseGame: GameWithCurrentPlay,
newGame: Game,
gameHistoryRecordToInsert: GameHistoryRecordToInsert,
): GameHistoryRecordPlayVoting {
const votes = gameHistoryRecordToInsert.play.votes ?? [];
const nominatedPlayers = this.gamePlayVoteService.getNominatedPlayers(votes, baseGame);
const gameHistoryRecordPlayVoting: GameHistoryRecordPlayVoting = {
result: this.generateCurrentGameHistoryRecordPlayVotingResultToInsert(baseGame, newGame, gameHistoryRecordToInsert),
nominatedPlayers,
};
return plainToInstance(GameHistoryRecordPlayVoting, gameHistoryRecordPlayVoting, { ...plainToInstanceDefaultOptions, enableCircularCheck: true });
}

private generateCurrentGameHistoryRecordPlaySourceToInsert(baseGame: GameWithCurrentPlay): GameHistoryRecordPlaySource {
const gameHistoryRecordPlaySource: GameHistoryRecordPlaySource = {
name: baseGame.currentPlay.source,
players: getExpectedPlayersToPlay(baseGame),
};
return plainToInstance(GameHistoryRecordPlaySource, gameHistoryRecordPlaySource, { ...plainToInstanceDefaultOptions, enableCircularCheck: true });
}

private validateGameHistoryRecordToInsertPlayData(play: GameHistoryRecordPlay, game: Game): void {
const unmatchedSource = getNonexistentPlayer(game.players, play.source.players);
Expand Down

0 comments on commit dfd80c8

Please sign in to comment.