Skip to content

Commit

Permalink
feat(game-history): player attribute alterations (#1155)
Browse files Browse the repository at this point in the history
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Added functionality to track and record player attribute alterations
in game history.
- Introduced a new service to generate game history records based on
player actions.

- **Bug Fixes**
- Updated import paths to ensure correct functionality of game history
features.

- **Refactor**
- Simplified the logic for handling player states in game history
records.
- Replaced existing methods with a streamlined service for game history
record generation.

- **Tests**
- Added comprehensive tests for new factories and services related to
game history records.
- Enhanced acceptance tests to cover scenarios involving player
attribute alterations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
antoinezanardi authored Jul 13, 2024
1 parent c535c3c commit 1c3da1e
Show file tree
Hide file tree
Showing 35 changed files with 45,051 additions and 42,555 deletions.
2 changes: 1 addition & 1 deletion config/eslint/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 = 8;
const MAX_PARAMS = 10;
const INDENT_SPACE_COUNT = 2;
const ERROR = "error";
const WARNING = "warn";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,13 @@ const GAME_HISTORY_RECORD_VOTING_RESULTS = [
"skipped",
] as const;

export { GAME_HISTORY_RECORD_VOTING_RESULTS };
const GAME_HISTORY_RECORD_PLAYER_ATTRIBUTE_ALTERATION_STATUSES = [
"attached",
"detached",
"activated",
] as const;

export {
GAME_HISTORY_RECORD_VOTING_RESULTS,
GAME_HISTORY_RECORD_PLAYER_ATTRIBUTE_ALTERATION_STATUSES,
};
2 changes: 2 additions & 0 deletions src/modules/game/game.module.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GameHistoryRecordToInsertGeneratorService } from "@/modules/game/providers/services/game-history/game-history-record-to-insert-generator.service";
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";

Expand Down Expand Up @@ -43,6 +44,7 @@ import { Game, GAME_SCHEMA } from "@/modules/game/schemas/game.schema";
GameVictoryService,
GameRepository,
GameHistoryRecordService,
GameHistoryRecordToInsertGeneratorService,
GameHistoryRecordRepository,
PlayerKillerService,
PlayerAttributeService,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { GameHistoryRecordPlaySource } from "@/modules/game/schemas/game-history-record/game-history-record-play/game-history-record-play-source/game-history-record-play-source.schema";
import { toJSON } from "@/shared/misc/helpers/object.helpers";
import { DEFAULT_PLAIN_TO_INSTANCE_OPTIONS } from "@/shared/validation/constants/validation.constants";
import { plainToInstance } from "class-transformer";

function createGameHistoryRecordPlaySource(gameHistoryRecordPlaySource: GameHistoryRecordPlaySource): GameHistoryRecordPlaySource {
return plainToInstance(GameHistoryRecordPlaySource, toJSON(gameHistoryRecordPlaySource), DEFAULT_PLAIN_TO_INSTANCE_OPTIONS);
}

export {
createGameHistoryRecordPlaySource,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { GameHistoryRecordPlayVoting } from "@/modules/game/schemas/game-history-record/game-history-record-play/game-history-record-play-voting/game-history-record-play-voting.schema";
import { toJSON } from "@/shared/misc/helpers/object.helpers";
import { DEFAULT_PLAIN_TO_INSTANCE_OPTIONS } from "@/shared/validation/constants/validation.constants";
import { plainToInstance } from "class-transformer";

function createGameHistoryRecordPlayVoting(gameHistoryRecordPlayVoting: GameHistoryRecordPlayVoting): GameHistoryRecordPlayVoting {
return plainToInstance(GameHistoryRecordPlayVoting, toJSON(gameHistoryRecordPlayVoting), DEFAULT_PLAIN_TO_INSTANCE_OPTIONS);
}

export {
createGameHistoryRecordPlayVoting,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { GameHistoryRecordPlay } from "@/modules/game/schemas/game-history-record/game-history-record-play/game-history-record-play.schema";
import { toJSON } from "@/shared/misc/helpers/object.helpers";
import { DEFAULT_PLAIN_TO_INSTANCE_OPTIONS } from "@/shared/validation/constants/validation.constants";
import { plainToInstance } from "class-transformer";

function createGameHistoryRecordPlay(gameHistoryRecordPlay: GameHistoryRecordPlay): GameHistoryRecordPlay {
return plainToInstance(GameHistoryRecordPlay, toJSON(gameHistoryRecordPlay), DEFAULT_PLAIN_TO_INSTANCE_OPTIONS);
}

export {
createGameHistoryRecordPlay,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { GameHistoryRecordPlayerAttributeAlteration } from "@/modules/game/schemas/game-history-record/game-history-record-player-attribute-alteration/game-history-record-player-attribute-alteration.schema";
import { toJSON } from "@/shared/misc/helpers/object.helpers";
import { DEFAULT_PLAIN_TO_INSTANCE_OPTIONS } from "@/shared/validation/constants/validation.constants";
import { plainToInstance } from "class-transformer";

function createGameHistoryRecordPlayerAttributeAlteration(playerAttributeAlteration: GameHistoryRecordPlayerAttributeAlteration): GameHistoryRecordPlayerAttributeAlteration {
return plainToInstance(GameHistoryRecordPlayerAttributeAlteration, toJSON(playerAttributeAlteration), DEFAULT_PLAIN_TO_INSTANCE_OPTIONS);
}

export {
createGameHistoryRecordPlayerAttributeAlteration,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { GameHistoryRecordToInsert } from "@/modules/game/types/game-history-record/game-history-record.types";
import { toJSON } from "@/shared/misc/helpers/object.helpers";
import { DEFAULT_PLAIN_TO_INSTANCE_OPTIONS } from "@/shared/validation/constants/validation.constants";
import { plainToInstance } from "class-transformer";

function createGameHistoryRecordToInsert(gameHistoryRecordToInsert: GameHistoryRecordToInsert): GameHistoryRecordToInsert {
return plainToInstance(GameHistoryRecordToInsert, toJSON(gameHistoryRecordToInsert), DEFAULT_PLAIN_TO_INSTANCE_OPTIONS);
}

export {
createGameHistoryRecordToInsert,
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Model } from "mongoose";

import { GamePhaseName } from "@/modules/game/types/game-phase/game-phase.types";
import type { GetGameHistoryDto } from "@/modules/game/dto/get-game-history/get-game-history.dto";
import { convertGetGameHistoryDtoToMongooseQueryOptions } from "@/modules/game/helpers/game-history/game-history-record.mappers";
import { convertGetGameHistoryDtoToMongooseQueryOptions } from "@/modules/game/helpers/game-history-record/game-history-record.mappers";
import { GameHistoryRecord } from "@/modules/game/schemas/game-history-record/game-history-record.schema";
import type { GamePlay } from "@/modules/game/schemas/game-play/game-play.schema";
import type { Player } from "@/modules/game/schemas/player/player.schema";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import type { MakeGamePlayWithRelationsDto } from "@/modules/game/dto/make-game-play/make-game-play-with-relations.dto";
import { createGameHistoryRecordPlaySource } from "@/modules/game/helpers/game-history-record/game-history-record-play/game-history-record-play-source/game-history-record-play-source.factory";
import { createGameHistoryRecordPlayVoting } from "@/modules/game/helpers/game-history-record/game-history-record-play/game-history-record-play-voting/game-history-record-play-voting.factory";
import { createGameHistoryRecordPlay } from "@/modules/game/helpers/game-history-record/game-history-record-play/game-history-record-play.factory";
import { createGameHistoryRecordPlayerAttributeAlteration } from "@/modules/game/helpers/game-history-record/game-history-record-player-attribute-alteration/game-history-record-player-attribute-alteration.factory";
import { createGameHistoryRecordToInsert } from "@/modules/game/helpers/game-history-record/game-history-record.factory";
import { doesGamePlayHaveCause } from "@/modules/game/helpers/game-play/game-play.helpers";
import { getPlayerWithActiveAttributeName, getPlayerWithId } from "@/modules/game/helpers/game.helpers";
import { doesPlayerHaveAttributeWithNameAndSource, isPlayerAttributeActive } from "@/modules/game/helpers/player/player-attribute/player-attribute.helpers";
import { GamePlayVoteService } from "@/modules/game/providers/services/game-play/game-play-vote/game-play-vote.service";
import { GameHistoryRecordPlaySource } from "@/modules/game/schemas/game-history-record/game-history-record-play/game-history-record-play-source/game-history-record-play-source.schema";
import { GameHistoryRecordPlayVoting } from "@/modules/game/schemas/game-history-record/game-history-record-play/game-history-record-play-voting/game-history-record-play-voting.schema";
import { GameHistoryRecordPlay } from "@/modules/game/schemas/game-history-record/game-history-record-play/game-history-record-play.schema";
import { GameHistoryRecordPlayerAttributeAlteration } from "@/modules/game/schemas/game-history-record/game-history-record-player-attribute-alteration/game-history-record-player-attribute-alteration.schema";
import type { Game } from "@/modules/game/schemas/game.schema";
import type { DeadPlayer } from "@/modules/game/schemas/player/dead-player.schema";
import type { Player } from "@/modules/game/schemas/player/player.schema";
import { GameHistoryRecordToInsert, GameHistoryRecordVotingResult } from "@/modules/game/types/game-history-record/game-history-record.types";
import type { GameWithCurrentPlay } from "@/modules/game/types/game-with-current-play.types";
import { createNoCurrentGamePlayUnexpectedException } from "@/shared/exception/helpers/unexpected-exception.factory";
import { Injectable } from "@nestjs/common";

@Injectable()
export class GameHistoryRecordToInsertGeneratorService {
public constructor(private readonly gamePlayVoteService: GamePlayVoteService) {}

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),
playerAttributeAlterations: this.generateCurrentGameHistoryRecordPlayerAttributeAlterationsToInsert(baseGame, newGame),
};
if (gameHistoryRecordToInsert.play.type === "vote") {
gameHistoryRecordToInsert.play.voting = this.generateCurrentGameHistoryRecordPlayVotingToInsert(baseGame as GameWithCurrentPlay, newGame, gameHistoryRecordToInsert);
}
return createGameHistoryRecordToInsert(gameHistoryRecordToInsert);
}

private generateCurrentGameHistoryRecordDeadPlayersToInsert(baseGame: Game, newGame: Game): DeadPlayer[] | undefined {
const { players: newPlayers } = newGame;
const currentDeadPlayers = newPlayers.filter(player => {
const matchingBasePlayer = getPlayerWithId(baseGame, player._id);

return matchingBasePlayer?.isAlive === true && !player.isAlive;
}) as DeadPlayer[];

return currentDeadPlayers.length ? currentDeadPlayers : undefined;
}

private generateCurrentGameHistoryRecordRevealedPlayersToInsert(baseGame: Game, newGame: Game): Player[] | undefined {
const { players: newPlayers } = newGame;
const currentRevealedPlayers = newPlayers.filter(player => {
const matchingBasePlayer = getPlayerWithId(baseGame, player._id);

return matchingBasePlayer?.role.isRevealed === false && player.role.isRevealed && player.isAlive;
});

return currentRevealedPlayers.length ? currentRevealedPlayers : undefined;
}

private generateCurrentGameHistoryRecordAttachedPlayerAttributesToInsertForPlayer(basePlayer: Player, newPlayer: Player): GameHistoryRecordPlayerAttributeAlteration[] {
return newPlayer.attributes.reduce<GameHistoryRecordPlayerAttributeAlteration[]>((alterations, playerAttribute) => {
if (!doesPlayerHaveAttributeWithNameAndSource(basePlayer, playerAttribute.name, playerAttribute.source)) {
alterations.push(createGameHistoryRecordPlayerAttributeAlteration({
playerName: newPlayer.name,
name: playerAttribute.name,
source: playerAttribute.source,
status: "attached",
}));
}
return alterations;
}, []);
}

private generateCurrentGameHistoryRecordDetachedPlayerAttributesToInsertForPlayer(basePlayer: Player, newPlayer: Player): GameHistoryRecordPlayerAttributeAlteration[] {
return basePlayer.attributes.reduce<GameHistoryRecordPlayerAttributeAlteration[]>((alterations, playerAttribute) => {
if (!doesPlayerHaveAttributeWithNameAndSource(newPlayer, playerAttribute.name, playerAttribute.source)) {
alterations.push(createGameHistoryRecordPlayerAttributeAlteration({
playerName: newPlayer.name,
name: playerAttribute.name,
source: playerAttribute.source,
status: "detached",
}));
}
return alterations;
}, []);
}

private generateCurrentGameHistoryRecordActivatedPlayerAttributesToInsertForPlayer(baseGame: Game, newGame: Game, basePlayer: Player, newPlayer: Player):
GameHistoryRecordPlayerAttributeAlteration[] {
return newPlayer.attributes.reduce<GameHistoryRecordPlayerAttributeAlteration[]>((alterations, playerAttribute) => {
const doesBasePlayerHaveAttribute = doesPlayerHaveAttributeWithNameAndSource(basePlayer, playerAttribute.name, playerAttribute.source);
const doesPlayerAttributeBecomesActive = !isPlayerAttributeActive(playerAttribute, baseGame) && isPlayerAttributeActive(playerAttribute, newGame);
if (doesBasePlayerHaveAttribute && doesPlayerAttributeBecomesActive) {
alterations.push(createGameHistoryRecordPlayerAttributeAlteration({
playerName: newPlayer.name,
name: playerAttribute.name,
source: playerAttribute.source,
status: "activated",
}));
}
return alterations;
}, []);
}

private generateCurrentGameHistoryRecordPlayerAttributeAlterationsToInsert(baseGame: Game, newGame: Game): GameHistoryRecordPlayerAttributeAlteration[] | undefined {
const { players: newPlayers } = newGame;
const playerAttributeAlterations = newPlayers.reduce<GameHistoryRecordPlayerAttributeAlteration[]>((alterations, player) => {
const matchingBasePlayer = getPlayerWithId(baseGame, player._id);
if (!matchingBasePlayer) {
return alterations;
}
const attachedPlayerAttributes = this.generateCurrentGameHistoryRecordAttachedPlayerAttributesToInsertForPlayer(matchingBasePlayer, player);
const detachedPlayerAttributes = this.generateCurrentGameHistoryRecordDetachedPlayerAttributesToInsertForPlayer(matchingBasePlayer, player);
const activatedPlayerAttributes = this.generateCurrentGameHistoryRecordActivatedPlayerAttributesToInsertForPlayer(baseGame, newGame, matchingBasePlayer, player);

return alterations.concat(attachedPlayerAttributes, detachedPlayerAttributes, activatedPlayerAttributes);
}, []);

return playerAttributeAlterations.length ? playerAttributeAlterations : undefined;
}

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

return createGameHistoryRecordPlay(gameHistoryRecordPlayToInsert);
}

private generateCurrentGameHistoryRecordPlayVotingResultToInsert(
baseGame: GameWithCurrentPlay,
newGame: Game,
nominatedPlayers: Player[],
gameHistoryRecordToInsert: GameHistoryRecordToInsert,
): GameHistoryRecordVotingResult {
const sheriffPlayer = getPlayerWithActiveAttributeName(newGame, "sheriff");
const areSomePlayersDeadFromCurrentVotes = gameHistoryRecordToInsert.deadPlayers?.some(({ death }) => {
const deathFromVoteCauses = ["vote", "vote-scapegoated"];

return deathFromVoteCauses.includes(death.cause);
}) === true;
if (baseGame.currentPlay.action === "elect-sheriff") {
return sheriffPlayer ? "sheriff-election" : "tie";
}
if (!gameHistoryRecordToInsert.play.votes || gameHistoryRecordToInsert.play.votes.length === 0) {
return "skipped";
}
if (areSomePlayersDeadFromCurrentVotes) {
return "death";
}
if (doesGamePlayHaveCause(baseGame.currentPlay, "previous-votes-were-in-ties") || nominatedPlayers.length === 1) {
return "inconsequential";
}
return "tie";
}

private generateCurrentGameHistoryRecordPlayVotingToInsert(
baseGame: GameWithCurrentPlay,
newGame: Game,
gameHistoryRecordToInsert: GameHistoryRecordToInsert,
): GameHistoryRecordPlayVoting {
const nominatedPlayers = this.gamePlayVoteService.getNominatedPlayers(gameHistoryRecordToInsert.play.votes, baseGame);
const gameHistoryRecordPlayVoting: GameHistoryRecordPlayVoting = {
result: this.generateCurrentGameHistoryRecordPlayVotingResultToInsert(baseGame, newGame, nominatedPlayers, gameHistoryRecordToInsert),
nominatedPlayers: nominatedPlayers.length ? nominatedPlayers : undefined,
};

return createGameHistoryRecordPlayVoting(gameHistoryRecordPlayVoting);
}

private generateCurrentGameHistoryRecordPlaySourceToInsert(baseGame: GameWithCurrentPlay): GameHistoryRecordPlaySource {
return createGameHistoryRecordPlaySource(baseGame.currentPlay.source);
}
}
Loading

0 comments on commit 1c3da1e

Please sign in to comment.