Skip to content

Commit

Permalink
feat(game-phase): phase tick (#976)
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinezanardi authored Apr 12, 2024
1 parent 57b4813 commit 28048cf
Show file tree
Hide file tree
Showing 51 changed files with 25,418 additions and 25,318 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const DEFAULT_GAME_OPTIONS: ReadonlyDeep<GameOptions> = {
isEnabled: true,
electedAt: {
turn: 1,
phase: "night",
phaseName: "night",
},
hasDoubledVote: true,
mustSettleTieInVotes: true,
Expand Down
16 changes: 16 additions & 0 deletions src/modules/game/constants/game-phase/game-phase.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { GamePhase } from "@/modules/game/schemas/game-phase/game-phase.schema";

const GAME_PHASE_NAMES = [
"night",
"day",
] as const;

const DEFAULT_GAME_PHASE = {
name: "night",
tick: 1,
} as const satisfies GamePhase;

export {
GAME_PHASE_NAMES,
DEFAULT_GAME_PHASE,
};
6 changes: 0 additions & 6 deletions src/modules/game/constants/game.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ import type { PlayerGroup } from "@/modules/game/types/player/player.types";
import { ROLE_NAMES } from "@/modules/role/constants/role.constants";
import type { RoleName } from "@/modules/role/types/role.types";

const GAME_PHASES = [
"night",
"day",
] as const;

const GAME_STATUSES = [
"playing",
"over",
Expand Down Expand Up @@ -228,7 +223,6 @@ const NIGHT_GAME_PLAYS_PRIORITY_LIST: ReadonlyDeep<GamePlay[]> = GAME_PLAYS_PRIO
const DAY_GAME_PLAYS_PRIORITY_LIST: ReadonlyDeep<GamePlay[]> = GAME_PLAYS_PRIORITY_LIST.filter(({ occurrence }) => occurrence === "on-days");

export {
GAME_PHASES,
GAME_STATUSES,
GAME_SOURCES,
GAME_PLAYS_PRIORITY_LIST,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { ApiPropertyOptions } from "@nestjs/swagger";
import { ApiProperty } from "@nestjs/swagger";
import { IsIn, IsInt, IsOptional, Min } from "class-validator";

import { GamePhase } from "@/modules/game/types/game.types";
import { GAME_PHASES } from "@/modules/game/constants/game.constants";
import { GamePhaseName } from "@/modules/game/types/game-phase/game-phase.types";
import { GAME_PHASE_NAMES } from "@/modules/game/constants/game-phase/game-phase.constants";
import { SHERIFF_ELECTION_GAME_OPTIONS_API_PROPERTIES, SHERIFF_ELECTION_GAME_OPTIONS_FIELDS_SPECS } from "@/modules/game/schemas/game-options/roles-game-options/sheriff-game-options/sheriff-election-game-options/sheriff-election-game-options.schema.constants";

class CreateSheriffElectionGameOptionsDto {
Expand All @@ -17,12 +17,12 @@ class CreateSheriffElectionGameOptionsDto {
public turn: number = SHERIFF_ELECTION_GAME_OPTIONS_FIELDS_SPECS.turn.default;

@ApiProperty({
...SHERIFF_ELECTION_GAME_OPTIONS_API_PROPERTIES.phase,
...SHERIFF_ELECTION_GAME_OPTIONS_API_PROPERTIES.phaseName,
required: false,
} as ApiPropertyOptions)
@IsOptional()
@IsIn(GAME_PHASES)
public phase: GamePhase = SHERIFF_ELECTION_GAME_OPTIONS_FIELDS_SPECS.phase.default;
@IsIn(GAME_PHASE_NAMES)
public phaseName: GamePhaseName = SHERIFF_ELECTION_GAME_OPTIONS_FIELDS_SPECS.phaseName.default;
}

export { CreateSheriffElectionGameOptionsDto };
11 changes: 5 additions & 6 deletions src/modules/game/dto/create-game/create-game.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ import { ApiHideProperty, ApiProperty } from "@nestjs/swagger";
import { Transform, Type } from "class-transformer";
import { ArrayMaxSize, Equals, IsArray, IsOptional, ValidateNested } from "class-validator";

import { GamePhase } from "@/modules/game/types/game.types";
import { CompositionGroupsSize } from "@/modules/game/dto/base/decorators/composition/composition-groups-size.decorator";
import { AdditionalCardsForActorRoles } from "@/modules/game/dto/base/decorators/additional-cards/additional-cards-for-actor-roles.decorator";
import { AdditionalCardsForActorSize } from "@/modules/game/dto/base/decorators/additional-cards/additional-cards-for-actor-size.decorator";
import { CompositionHasTwoGroupsWithPrejudicedManipulator } from "@/modules/game/dto/base/decorators/composition/composition-has-two-groups-with-prejudiced-manipulator.decorator";
import { CompositionGroupsPresence } from "@/modules/game/dto/base/decorators/composition/composition-groups-presence.decorator";
import { AdditionalCardsForThiefRoles } from "@/modules/game/dto/base/decorators/additional-cards/additional-cards-for-thief-roles.decorator";
import { AdditionalCardsForThiefSize } from "@/modules/game/dto/base/decorators/additional-cards/additional-cards-for-thief-size.decorator";
import { AdditionalCardsPresence } from "@/modules/game/dto/base/decorators/additional-cards/additional-cards-presence.decorator";
import { AdditionalCardsRolesMaxInGame } from "@/modules/game/dto/base/decorators/additional-cards/additional-cards-roles-max-in-game.decorator";
import { CompositionBounds } from "@/modules/game/dto/base/decorators/composition/composition-bounds.decorator";
import { CompositionGroupsPresence } from "@/modules/game/dto/base/decorators/composition/composition-groups-presence.decorator";
import { CompositionGroupsSize } from "@/modules/game/dto/base/decorators/composition/composition-groups-size.decorator";
import { CompositionHasTwoGroupsWithPrejudicedManipulator } from "@/modules/game/dto/base/decorators/composition/composition-has-two-groups-with-prejudiced-manipulator.decorator";
import { CompositionHasVillager } from "@/modules/game/dto/base/decorators/composition/composition-has-villager.decorator";
import { CompositionHasWerewolf } from "@/modules/game/dto/base/decorators/composition/composition-has-werewolf.decorator";
import { CompositionPositionsConsistency } from "@/modules/game/dto/base/decorators/composition/composition-positions-consistency.decorator";
Expand All @@ -24,6 +23,7 @@ import { gamePlayersPositionTransformer } from "@/modules/game/dto/base/transfor
import { CreateGameAdditionalCardDto } from "@/modules/game/dto/create-game/create-game-additional-card/create-game-additional-card.dto";
import { CreateGameOptionsDto } from "@/modules/game/dto/create-game/create-game-options/create-game-options.dto";
import { CreateGamePlayerDto } from "@/modules/game/dto/create-game/create-game-player/create-game-player.dto";
import { GamePhase } from "@/modules/game/schemas/game-phase/game-phase.schema";
import { GamePlay } from "@/modules/game/schemas/game-play/game-play.schema";
import { GAME_API_PROPERTIES, GAME_FIELDS_SPECS } from "@/modules/game/schemas/game.schema.constants";

Expand All @@ -35,8 +35,7 @@ class CreateGameDto {

@ApiHideProperty()
@IsOptional()
@Equals(GAME_FIELDS_SPECS.phase.default)
public phase: GamePhase = GAME_FIELDS_SPECS.phase.default;
public phase: GamePhase = new GamePhase();

@ApiProperty(GAME_API_PROPERTIES.players as ApiPropertyOptions)
@Transform(gamePlayersPositionTransformer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ function createCantVoteByScapegoatPlayerAttribute(game: Game, playerAttribute: P
source: "scapegoat",
remainingPhases: 1,
activeAt: {
turn: game.phase === "day" ? game.turn + 1 : game.turn,
phase: "day",
turn: game.phase.name === "day" ? game.turn + 1 : game.turn,
phaseName: "day",
},
...playerAttribute,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { PlayerAttributeName } from "@/modules/game/types/player/player-att

function isPlayerAttributeActive({ activeAt }: PlayerAttribute, game: Game): boolean {
return activeAt === undefined || activeAt.turn < game.turn ||
activeAt.turn === game.turn && (activeAt.phase === game.phase || game.phase === "day");
activeAt.turn === game.turn && (activeAt.phaseName === game.phase.name || game.phase.name === "day");
}

function getPlayerAttributeWithName({ attributes }: Player, attributeName: PlayerAttributeName): PlayerAttribute | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { InjectModel } from "@nestjs/mongoose";
import type { FilterQuery, QueryOptions, Types } from "mongoose";
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 { 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";
import { GameHistoryRecordDocument, GameHistoryRecordToInsert } from "@/modules/game/types/game-history-record/game-history-record.types";
import { GamePlayAction, WitchPotion } from "@/modules/game/types/game-play/game-play.types";
import { GamePhase } from "@/modules/game/types/game.types";

@Injectable()
export class GameHistoryRecordRepository {
Expand Down Expand Up @@ -144,8 +144,13 @@ export class GameHistoryRecordRepository {
return this.gameHistoryRecordModel.findOne(filter, undefined, { sort: { createdAt: -1 } });
}

public async getGameHistoryPhaseRecords(gameId: Types.ObjectId, turn: number, phase: GamePhase): Promise<GameHistoryRecord[]> {
return this.gameHistoryRecordModel.find({ gameId, turn, phase });
public async getGameHistoryPhaseRecords(gameId: Types.ObjectId, turn: number, phaseName: GamePhaseName): Promise<GameHistoryRecord[]> {
const filter: FilterQuery<GameHistoryRecord> = {
gameId,
turn,
"phase.name": phaseName,
};
return this.gameHistoryRecordModel.find(filter);
}

public async getGameHistoryGamePlayRecords(gameId: Types.ObjectId, gamePlay: GamePlay, options: QueryOptions<GameHistoryRecord> = {}): Promise<GameHistoryRecord[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Injectable } from "@nestjs/common";
import { plainToInstance } from "class-transformer";
import type { Types } from "mongoose";

import { GamePhase } from "@/modules/game/schemas/game-phase/game-phase.schema";
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 type { MakeGamePlayWithRelationsDto } from "@/modules/game/dto/make-game-play/make-game-play-with-relations.dto";
import { getAdditionalCardWithId, getNonexistentPlayer, getPlayerWithActiveAttributeName, getPlayerWithId } from "@/modules/game/helpers/game.helpers";
Expand All @@ -19,7 +21,6 @@ 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 { GamePlayAction, WitchPotion } from "@/modules/game/types/game-play/game-play.types";
import type { GameWithCurrentPlay } from "@/modules/game/types/game-with-current-play.types";
import { GamePhase } from "@/modules/game/types/game.types";

import { ApiResources } from "@/shared/api/enums/api.enums";
import { ResourceNotFoundReasons } from "@/shared/exception/enums/resource-not-found-error.enum";
Expand Down Expand Up @@ -77,7 +78,7 @@ export class GameHistoryRecordService {
return this.gameHistoryRecordRepository.getGameHistoryElderProtectedFromWerewolvesRecords(gameId, elderPlayerId);
}

public async getGameHistoryPhaseRecords(gameId: Types.ObjectId, turn: number, phase: GamePhase): Promise<GameHistoryRecord[]> {
public async getGameHistoryPhaseRecords(gameId: Types.ObjectId, turn: number, phase: GamePhaseName): Promise<GameHistoryRecord[]> {
return this.gameHistoryRecordRepository.getGameHistoryPhaseRecords(gameId, turn, phase);
}

Expand All @@ -92,7 +93,7 @@ export class GameHistoryRecordService {
const gameHistoryRecordToInsert: GameHistoryRecordToInsert = {
gameId: baseGame._id,
turn: baseGame.turn,
phase: baseGame.phase,
phase: toJSON(baseGame.phase) as GamePhase,
tick: baseGame.tick,
play: this.generateCurrentGameHistoryRecordPlayToInsert(baseGame as GameWithCurrentPlay, play),
revealedPlayers: this.generateCurrentGameHistoryRecordRevealedPlayersToInsert(baseGame, newGame),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ export class GamePhaseService {

public async switchPhaseAndAppendGamePhaseUpcomingPlays(game: Game): Promise<Game> {
const clonedGame = createGame(game);
clonedGame.phase = clonedGame.phase === "night" ? "day" : "night";
if (clonedGame.phase === "night") {
const { name: phaseName } = clonedGame.phase;
clonedGame.phase.name = phaseName === "night" ? "day" : "night";
clonedGame.phase.tick = 1;
if (clonedGame.phase.name === "night") {
clonedGame.turn++;
}
const phaseUpcomingPlays = await this.gamePlayService.getPhaseUpcomingPlays(clonedGame);
Expand All @@ -42,7 +44,7 @@ export class GamePhaseService {

public applyStartingGamePhaseOutcomes(game: Game): Game {
const clonedGame = createGame(game);
if (clonedGame.phase === "night") {
if (clonedGame.phase.name === "night") {
return this.applyStartingNightPlayerAttributesOutcomes(clonedGame);
}
return clonedGame;
Expand Down Expand Up @@ -84,7 +86,7 @@ export class GamePhaseService {
private async applyEndingGamePhasePlayerAttributesOutcomesToPlayer(player: Player, game: Game): Promise<Game> {
const clonedGame = createGame(game);
const clonedPlayer = createPlayer(player);
if (clonedGame.phase === "night") {
if (clonedGame.phase.name === "night") {
return this.applyEndingNightPlayerAttributesOutcomesToPlayer(clonedPlayer, clonedGame);
}
return this.applyEndingDayPlayerAttributesOutcomesToPlayer(clonedPlayer, clonedGame);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable } from "@nestjs/common";

import { GamePhaseName } from "@/modules/game/types/game-phase/game-phase.types";
import { DAY_GAME_PLAYS_PRIORITY_LIST, NIGHT_GAME_PLAYS_PRIORITY_LIST } from "@/modules/game/constants/game.constants";
import { CreateGamePlayerDto } from "@/modules/game/dto/create-game/create-game-player/create-game-player.dto";
import { CreateGameDto } from "@/modules/game/dto/create-game/create-game.dto";
Expand All @@ -15,7 +16,6 @@ import type { SheriffGameOptions } from "@/modules/game/schemas/game-options/rol
import type { GamePlay } from "@/modules/game/schemas/game-play/game-play.schema";
import type { Game } from "@/modules/game/schemas/game.schema";
import type { GameWithCurrentPlay } from "@/modules/game/types/game-with-current-play.types";
import { GamePhase } from "@/modules/game/types/game.types";
import { PlayerGroup } from "@/modules/game/types/player/player.types";
import { RoleName } from "@/modules/role/types/role.types";

Expand Down Expand Up @@ -71,8 +71,8 @@ export class GamePlayService {
}

public async getPhaseUpcomingPlays(game: CreateGameDto | Game): Promise<GamePlay[]> {
const isSheriffElectionTime = this.isSheriffElectionTime(game.options.roles.sheriff, game.turn, game.phase);
const phaseGamePlaysPriorityList = game.phase === "night" ? NIGHT_GAME_PLAYS_PRIORITY_LIST : DAY_GAME_PLAYS_PRIORITY_LIST;
const isSheriffElectionTime = this.isSheriffElectionTime(game.options.roles.sheriff, game.turn, game.phase.name);
const phaseGamePlaysPriorityList = game.phase.name === "night" ? NIGHT_GAME_PLAYS_PRIORITY_LIST : DAY_GAME_PLAYS_PRIORITY_LIST;
const suitabilityPromises = phaseGamePlaysPriorityList.map(async eligiblePlay => this.isGamePlaySuitableForCurrentPhase(game, eligiblePlay as GamePlay));
const suitabilityResults = await Promise.all(suitabilityPromises);
const upcomingNightPlays = phaseGamePlaysPriorityList
Expand Down Expand Up @@ -104,7 +104,7 @@ export class GamePlayService {
private async getNewUpcomingPlaysForCurrentPhase(game: Game): Promise<GamePlay[]> {
const { _id, turn, phase } = game;
const currentPhaseUpcomingPlays = await this.getPhaseUpcomingPlays(game);
const gameHistoryPhaseRecords = await this.gameHistoryRecordService.getGameHistoryPhaseRecords(_id, turn, phase);
const gameHistoryPhaseRecords = await this.gameHistoryRecordService.getGameHistoryPhaseRecords(_id, turn, phase.name);
return currentPhaseUpcomingPlays.filter(gamePlay => this.isUpcomingPlayNewForCurrentPhase(gamePlay, game, gameHistoryPhaseRecords));
}

Expand All @@ -127,9 +127,9 @@ export class GamePlayService {
});
}

private isSheriffElectionTime(sheriffGameOptions: SheriffGameOptions, currentTurn: number, currentPhase: GamePhase): boolean {
private isSheriffElectionTime(sheriffGameOptions: SheriffGameOptions, currentTurn: number, currentPhase: GamePhaseName): boolean {
const { electedAt, isEnabled } = sheriffGameOptions;
return isEnabled && electedAt.turn === currentTurn && electedAt.phase === currentPhase;
return isEnabled && electedAt.turn === currentTurn && electedAt.phaseName === currentPhase;
}

private async isLoversGamePlaySuitableForCurrentPhase(game: CreateGameDto | Game, gamePlay: GamePlay): Promise<boolean> {
Expand Down Expand Up @@ -180,7 +180,7 @@ export class GamePlayService {
const { doesGrowlOnWerewolvesSide } = game.options.roles.bearTamer;
const isBearTamerInfected = bearTamerPlayer.side.current === "werewolves";
const lastVoteGamePlay = await this.gameHistoryRecordService.getLastGameHistorySurvivorsVoteRecord(game._id);
const didGamePhaseHaveSurvivorsVote = lastVoteGamePlay?.turn === game.turn && lastVoteGamePlay.phase === game.phase;
const didGamePhaseHaveSurvivorsVote = lastVoteGamePlay?.turn === game.turn && lastVoteGamePlay.phase.name === game.phase.name;
return !didGamePhaseHaveSurvivorsVote && (doesGrowlOnWerewolvesSide && isBearTamerInfected || doesBearTamerHaveWerewolfSidedNeighbor);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class GameVictoryService {
return false;
}
const { cause: deathCause } = angelPlayer.death;
return deathCause === "eaten" || deathCause === "vote" && phase === "night";
return deathCause === "eaten" || deathCause === "vote" && phase.name === "night";
}

private doesPrejudicedManipulatorWin(game: Game): boolean {
Expand Down
1 change: 1 addition & 0 deletions src/modules/game/providers/services/game.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export class GameService {
clonedGame = await this.gamePlayService.refreshUpcomingPlays(clonedGame);
clonedGame = this.gamePlayService.proceedToNextGamePlay(clonedGame);
clonedGame.tick++;
clonedGame.phase.tick++;
if (isGamePhaseOver(clonedGame)) {
clonedGame = await this.handleGamePhaseCompletion(clonedGame);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ApiPropertyOptions } from "@nestjs/swagger";
import { SchemaTypes } from "mongoose";
import type { ReadonlyDeep } from "type-fest";

import { GAME_PHASES } from "@/modules/game/constants/game.constants";
import { GAME_PHASE_SCHEMA } from "@/modules/game/schemas/game-phase/game-phase.schema";
import { DEAD_PLAYER_SCHEMA } from "@/modules/game/schemas/player/dead-player.schema";
import { PLAYER_SCHEMA } from "@/modules/game/schemas/player/player.schema";
import { GAME_HISTORY_RECORD_PLAY_SCHEMA } from "@/modules/game/schemas/game-history-record/game-history-record-play/game-history-record-play.schema";
Expand All @@ -23,7 +23,7 @@ const GAME_HISTORY_RECORD_FIELDS_SPECS = {
},
phase: {
required: true,
enum: GAME_PHASES,
type: GAME_PHASE_SCHEMA,
},
tick: {
required: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ApiProperty } from "@nestjs/swagger";
import { Expose, Transform, Type } from "class-transformer";
import { Types } from "mongoose";

import { GamePhase } from "@/modules/game/types/game.types";
import { GamePhase } from "@/modules/game/schemas/game-phase/game-phase.schema";
import { DeadPlayer } from "@/modules/game/schemas/player/dead-player.schema";
import { GAME_HISTORY_RECORD_API_PROPERTIES, GAME_HISTORY_RECORD_FIELDS_SPECS } from "@/modules/game/schemas/game-history-record/game-history-record.schema.constants";
import { GameHistoryRecordPlay } from "@/modules/game/schemas/game-history-record/game-history-record-play/game-history-record-play.schema";
Expand Down Expand Up @@ -38,6 +38,7 @@ class GameHistoryRecord {

@ApiProperty(GAME_HISTORY_RECORD_API_PROPERTIES.phase as ApiPropertyOptions)
@Prop(GAME_HISTORY_RECORD_FIELDS_SPECS.phase)
@Type(() => GamePhase)
@Expose()
public phase: GamePhase;

Expand Down
Loading

0 comments on commit 28048cf

Please sign in to comment.