Skip to content

Commit

Permalink
feat(game-options): skip votes options (#370)
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinezanardi authored Aug 3, 2023
1 parent 20a1e74 commit 9859940
Show file tree
Hide file tree
Showing 22 changed files with 36,124 additions and 34,134 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { GameOptions } from "../../schemas/game-options/game-options.schema

const defaultGameOptions: GameOptions = Object.freeze({
composition: { isHidden: false },
votes: { canBeSkipped: true },
roles: {
areRevealedOnDeath: true,
doSkipCallIfNoTarget: false,
Expand Down Expand Up @@ -49,6 +50,7 @@ const defaultGameOptions: GameOptions = Object.freeze({

const gameOptionsApiProperties: Record<keyof GameOptions, ApiPropertyOptions> = Object.freeze({
composition: { description: "Game's composition options" },
votes: { description: "Game's votes options" },
roles: { description: "Game's roles options" },
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { ApiPropertyOptions } from "@nestjs/swagger";
import type { VotesGameOptions } from "../../schemas/game-options/votes-game-options.schema";
import { defaultGameOptions } from "./game-options.constant";

const votesGameOptionsFieldsSpecs = Object.freeze({ canBeSkipped: { default: defaultGameOptions.votes.canBeSkipped } });

const votesGameOptionsApiProperties: Record<keyof VotesGameOptions, ApiPropertyOptions> = Object.freeze({
canBeSkipped: {
description: "If set to `true`, players are not obliged to vote. There won't be any death if votes are skipped. Sheriff election nor votes because of the angel presence can't be skipped",
...votesGameOptionsFieldsSpecs.canBeSkipped,
},
});

export {
votesGameOptionsApiProperties,
votesGameOptionsFieldsSpecs,
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { IsOptional, ValidateNested } from "class-validator";
import { gameOptionsApiProperties } from "../../../constants/game-options/game-options.constant";
import { CreateCompositionGameOptionsDto } from "./create-composition-game-options/create-composition-game-options.dto";
import { CreateRolesGameOptionsDto } from "./create-roles-game-options/create-roles-game-options.dto";
import { CreateVotesGameOptionsDto } from "./create-votes-game-options/create-votes-game-options.dto";

class CreateGameOptionsDto {
@ApiProperty({
Expand All @@ -15,6 +16,15 @@ class CreateGameOptionsDto {
@ValidateNested()
public composition: CreateCompositionGameOptionsDto = new CreateCompositionGameOptionsDto();

@ApiProperty({
...gameOptionsApiProperties.votes,
required: false,
})
@IsOptional()
@Type(() => CreateVotesGameOptionsDto)
@ValidateNested()
public votes: CreateVotesGameOptionsDto = new CreateVotesGameOptionsDto();

@ApiProperty({
...gameOptionsApiProperties.roles,
required: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsBoolean, IsOptional } from "class-validator";
import { votesGameOptionsApiProperties, votesGameOptionsFieldsSpecs } from "../../../../constants/game-options/votes-game-options.constant";

class CreateVotesGameOptionsDto {
@ApiProperty({
...votesGameOptionsApiProperties.canBeSkipped,
required: false,
})
@Type(() => Boolean)
@IsOptional()
@IsBoolean()
public canBeSkipped: boolean = votesGameOptionsFieldsSpecs.canBeSkipped.default;
}

export { CreateVotesGameOptionsDto };
1 change: 1 addition & 0 deletions src/modules/game/enums/game-history-record.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ enum GAME_HISTORY_RECORD_VOTING_RESULTS {
TIE = "tie",
DEATH = "death",
INCONSEQUENTIAL = "inconsequential",
SKIPPED = "skipped",
}

export { GAME_HISTORY_RECORD_VOTING_RESULTS };
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ export class GameHistoryRecordService {
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 (!gameHistoryRecordToInsert.play.votes || gameHistoryRecordToInsert.play.votes.length === 0) {
return GAME_HISTORY_RECORD_VOTING_RESULTS.SKIPPED;
}
if (areSomePlayersDeadFromCurrentVotes) {
return GAME_HISTORY_RECORD_VOTING_RESULTS.DEATH;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,20 +282,39 @@ export class GamePlayValidatorService {
throw new BadGamePlayPayloadException(BAD_GAME_PLAY_PAYLOAD_REASONS.BAD_VOTE_TARGET_FOR_TIE_BREAKER);
}
}

private validateGamePlayVotesWithRelationsDtoSourceAndTarget(playVotes: MakeGamePlayVoteWithRelationsDto[]): void {
if (playVotes.some(({ source }) => !source.isAlive || doesPlayerHaveAttribute(source, PLAYER_ATTRIBUTE_NAMES.CANT_VOTE))) {
throw new BadGamePlayPayloadException(BAD_GAME_PLAY_PAYLOAD_REASONS.BAD_VOTE_SOURCE);
}
if (playVotes.some(({ target }) => !target.isAlive)) {
throw new BadGamePlayPayloadException(BAD_GAME_PLAY_PAYLOAD_REASONS.BAD_VOTE_TARGET);
}
if (playVotes.some(({ source, target }) => source._id === target._id)) {
throw new BadGamePlayPayloadException(BAD_GAME_PLAY_PAYLOAD_REASONS.SAME_SOURCE_AND_TARGET_VOTE);
}
}

private validateUnsetGamePlayVotesWithRelationsDto(game: GameWithCurrentPlay): void {
const { action: currentPlayAction, cause: currentPlayCause } = game.currentPlay;
const { canBeSkipped: canVotesBeSkipped } = game.options.votes;
const isCurrentPlayVoteCauseOfAngelPresence = currentPlayAction === GAME_PLAY_ACTIONS.VOTE && currentPlayCause === GAME_PLAY_CAUSES.ANGEL_PRESENCE;
const isCurrentPlayVoteInevitable = currentPlayAction === GAME_PLAY_ACTIONS.ELECT_SHERIFF || isCurrentPlayVoteCauseOfAngelPresence;
const canSomePlayerVote = game.players.some(player => player.isAlive && !doesPlayerHaveAttribute(player, PLAYER_ATTRIBUTE_NAMES.CANT_VOTE));
if (canSomePlayerVote && (!canVotesBeSkipped && requiredVotesActions.includes(currentPlayAction) || canVotesBeSkipped && isCurrentPlayVoteInevitable)) {
throw new BadGamePlayPayloadException(BAD_GAME_PLAY_PAYLOAD_REASONS.REQUIRED_VOTES);
}
}

private async validateGamePlayVotesWithRelationsDto(playVotes: MakeGamePlayVoteWithRelationsDto[] | undefined, game: GameWithCurrentPlay): Promise<void> {
if (playVotes === undefined || playVotes.length === 0) {
if (requiredVotesActions.includes(game.currentPlay.action)) {
throw new BadGamePlayPayloadException(BAD_GAME_PLAY_PAYLOAD_REASONS.REQUIRED_VOTES);
}
if (!playVotes || playVotes.length === 0) {
this.validateUnsetGamePlayVotesWithRelationsDto(game);
return;
}
if (!requiredVotesActions.includes(game.currentPlay.action)) {
throw new BadGamePlayPayloadException(BAD_GAME_PLAY_PAYLOAD_REASONS.UNEXPECTED_VOTES);
}
if (playVotes.some(({ source, target }) => source._id === target._id)) {
throw new BadGamePlayPayloadException(BAD_GAME_PLAY_PAYLOAD_REASONS.SAME_SOURCE_AND_TARGET_VOTE);
}
this.validateGamePlayVotesWithRelationsDtoSourceAndTarget(playVotes);
if (game.currentPlay.cause === GAME_PLAY_CAUSES.PREVIOUS_VOTES_WERE_IN_TIES) {
await this.validateGamePlayVotesTieBreakerWithRelationsDto(playVotes, game);
}
Expand Down
10 changes: 10 additions & 0 deletions src/modules/game/schemas/game-options/game-options.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Expose, Type } from "class-transformer";
import { gameOptionsApiProperties } from "../../constants/game-options/game-options.constant";
import { CompositionGameOptions, CompositionGameOptionsSchema } from "./composition-game-options.schema";
import { RolesGameOptions, RolesGameOptionsSchema } from "./roles-game-options/roles-game-options.schema";
import { VotesGameOptions, VotesGameOptionsSchema } from "./votes-game-options.schema";

@Schema({
versionKey: false,
Expand All @@ -20,6 +21,15 @@ class GameOptions {
@Expose()
public composition: CompositionGameOptions;

@ApiProperty(gameOptionsApiProperties.votes)
@Prop({
type: VotesGameOptionsSchema,
default: () => ({}),
})
@Type(() => VotesGameOptions)
@Expose()
public votes: VotesGameOptions;

@ApiProperty(gameOptionsApiProperties.roles)
@Prop({
type: RolesGameOptionsSchema,
Expand Down
23 changes: 23 additions & 0 deletions src/modules/game/schemas/game-options/votes-game-options.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { ApiProperty } from "@nestjs/swagger";
import { Expose } from "class-transformer";
import { votesGameOptionsApiProperties, votesGameOptionsFieldsSpecs } from "../../constants/game-options/votes-game-options.constant";

@Schema({
versionKey: false,
id: false,
_id: false,
})
class VotesGameOptions {
@ApiProperty(votesGameOptionsApiProperties.canBeSkipped)
@Prop({ default: votesGameOptionsFieldsSpecs.canBeSkipped.default })
@Expose()
public canBeSkipped: boolean;
}

const VotesGameOptionsSchema = SchemaFactory.createForClass(VotesGameOptions);

export {
VotesGameOptions,
VotesGameOptionsSchema,
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ 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_SOURCE = "One source is not able to vote because he's dead or doesn't have the ability to do so",
BAD_VOTE_TARGET = "One target can't be voted because he's dead",
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",
Expand Down
Empty file removed tests/badges/.gitkeep
Empty file.
1 change: 0 additions & 1 deletion tests/badges/shields.json

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import { bulkCreateFakeCreateGamePlayerDto } from "../../../../../factories/game
import { createFakeCreateGameDto, createFakeCreateGameWithPlayersDto } from "../../../../../factories/game/dto/create-game/create-game.dto.factory";
import { createFakeMakeGamePlayDto } from "../../../../../factories/game/dto/make-game-play/make-game-play.dto.factory";
import { createFakeGameHistoryRecord } from "../../../../../factories/game/schemas/game-history-record/game-history-record.schema.factory";
import { createFakeCompositionGameOptions } from "../../../../../factories/game/schemas/game-options/composition-game-options.schema.factory";
import { createFakeGameOptions } from "../../../../../factories/game/schemas/game-options/game-options.schema.factory";
import { createFakeVotesGameOptions } from "../../../../../factories/game/schemas/game-options/votes-game-options.schema.factory";
import { createFakeGamePlayAllVote, createFakeGamePlaySeerLooks, createFakeGamePlayWerewolvesEat } from "../../../../../factories/game/schemas/game-play/game-play.schema.factory";
import { createFakeGame, createFakeGameWithCurrentPlay } from "../../../../../factories/game/schemas/game.schema.factory";
import { createFakeSeenBySeerPlayerAttribute } from "../../../../../factories/game/schemas/player/player-attribute/player-attribute.schema.factory";
Expand Down Expand Up @@ -485,7 +488,11 @@ describe("Game Controller", () => {
},
};
const payload = createFakeCreateGameWithPlayersDto({}, { options });
const expectedOptions = createFakeGameOptionsDto({ ...options, composition: { isHidden: defaultGameOptions.composition.isHidden } });
const expectedOptions = createFakeGameOptionsDto({
...options,
composition: createFakeCompositionGameOptions({ isHidden: defaultGameOptions.composition.isHidden }),
votes: createFakeVotesGameOptions({ canBeSkipped: defaultGameOptions.votes.canBeSkipped }),
});
const response = await app.inject({
method: "POST",
url: "/games",
Expand Down Expand Up @@ -641,13 +648,15 @@ describe("Game Controller", () => {
createFakeVillagerAlivePlayer(),
createFakeWerewolfAlivePlayer(),
]);
const options = createFakeGameOptions({ votes: createFakeVotesGameOptions({ canBeSkipped: false }) });
const game = createFakeGame({
status: GAME_STATUSES.PLAYING,
currentPlay: createFakeGamePlayAllVote(),
players,
options,
});
await models.game.create(game);
const payload = createFakeMakeGamePlayDto({ targets: [{ playerId: players[0]._id }] });
const payload = createFakeMakeGamePlayDto({});
const response = await app.inject({
method: "POST",
url: `/games/${game._id.toString()}/play`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { faker } from "@faker-js/faker";
import { plainToInstance } from "class-transformer";
import { CreateCompositionGameOptionsDto } from "../../../../../../../src/modules/game/dto/create-game/create-game-options/create-composition-game-options/create-composition-game-options.dto";
import { plainToInstanceDefaultOptions } from "../../../../../../../src/shared/validation/constants/validation.constant";

function createFakeCompositionGameOptionsDto(
createCompositionGameOptionsDto: Partial<CreateCompositionGameOptionsDto> = {},
override: object = {},
): CreateCompositionGameOptionsDto {
return plainToInstance(CreateCompositionGameOptionsDto, {
isHidden: createCompositionGameOptionsDto.isHidden ?? faker.datatype.boolean(),
...override,
}, plainToInstanceDefaultOptions);
}

export { createFakeCompositionGameOptionsDto };
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { faker } from "@faker-js/faker";
import { plainToInstance } from "class-transformer";
import { CreateGameOptionsDto } from "../../../../../../src/modules/game/dto/create-game/create-game-options/create-game-options.dto";
import { plainToInstanceDefaultOptions } from "../../../../../../src/shared/validation/constants/validation.constant";
import { createFakeCompositionGameOptionsDto } from "./create-composition-game-options/create-composition-game-options.dto.factory";
import { createFakeRolesGameOptionsDto } from "./create-roles-game-options/create-roles-game-options.dto.factory";
import { createFakeVotesGameOptionsDto } from "./create-votes-game-options/create-votes-game-options.dto.factory";

function createFakeGameOptionsDto(createGameOptionsDto: Partial<CreateGameOptionsDto> = {}, override: object = {}): CreateGameOptionsDto {
return plainToInstance(CreateGameOptionsDto, {
composition: { isHidden: createGameOptionsDto.composition?.isHidden ?? faker.datatype.boolean() },
composition: createFakeCompositionGameOptionsDto(createGameOptionsDto.composition),
votes: createFakeVotesGameOptionsDto(createGameOptionsDto.votes),
roles: createFakeRolesGameOptionsDto(createGameOptionsDto.roles),
...override,
}, plainToInstanceDefaultOptions);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { faker } from "@faker-js/faker";
import { plainToInstance } from "class-transformer";
import { CreateVotesGameOptionsDto } from "../../../../../../../src/modules/game/dto/create-game/create-game-options/create-votes-game-options/create-votes-game-options.dto";
import { plainToInstanceDefaultOptions } from "../../../../../../../src/shared/validation/constants/validation.constant";

function createFakeVotesGameOptionsDto(createVotesGameOptionsDto: Partial<CreateVotesGameOptionsDto> = {}, override: object = {}): CreateVotesGameOptionsDto {
return plainToInstance(CreateVotesGameOptionsDto, {
canBeSkipped: createVotesGameOptionsDto.canBeSkipped ?? faker.datatype.boolean(),
...override,
}, plainToInstanceDefaultOptions);
}

export { createFakeVotesGameOptionsDto };
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { faker } from "@faker-js/faker";
import { plainToInstance } from "class-transformer";
import { CompositionGameOptions } from "../../../../../src/modules/game/schemas/game-options/composition-game-options.schema";
import { plainToInstanceDefaultOptions } from "../../../../../src/shared/validation/constants/validation.constant";

function createFakeCompositionGameOptions(compositionGameOptions: Partial<CompositionGameOptions> = {}, override: object = {}): CompositionGameOptions {
return plainToInstance(CompositionGameOptions, {
isHidden: compositionGameOptions.isHidden ?? faker.datatype.boolean(),
...override,
}, plainToInstanceDefaultOptions);
}

export { createFakeCompositionGameOptions };
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { faker } from "@faker-js/faker";
import { plainToInstance } from "class-transformer";
import { GameOptions } from "../../../../../src/modules/game/schemas/game-options/game-options.schema";
import { plainToInstanceDefaultOptions } from "../../../../../src/shared/validation/constants/validation.constant";
import { createFakeCompositionGameOptions } from "./composition-game-options.schema.factory";
import { createFakeRolesGameOptions } from "./game-roles-options.schema.factory";
import { createFakeVotesGameOptions } from "./votes-game-options.schema.factory";

function createFakeGameOptions(gameOptions: Partial<GameOptions> = {}, override: object = {}): GameOptions {
return plainToInstance(GameOptions, {
composition: { isHidden: gameOptions.composition?.isHidden ?? faker.datatype.boolean() },
composition: createFakeCompositionGameOptions(gameOptions.composition),
votes: createFakeVotesGameOptions(gameOptions.votes),
roles: createFakeRolesGameOptions(gameOptions.roles),
...override,
}, plainToInstanceDefaultOptions);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { faker } from "@faker-js/faker";
import { plainToInstance } from "class-transformer";
import { VotesGameOptions } from "../../../../../src/modules/game/schemas/game-options/votes-game-options.schema";
import { plainToInstanceDefaultOptions } from "../../../../../src/shared/validation/constants/validation.constant";

function createFakeVotesGameOptions(createVotesGameOptions: Partial<VotesGameOptions> = {}, override: object = {}): VotesGameOptions {
return plainToInstance(VotesGameOptions, {
canBeSkipped: createVotesGameOptions.canBeSkipped ?? faker.datatype.boolean(),
...override,
}, plainToInstanceDefaultOptions);
}

export { createFakeVotesGameOptions };
Loading

0 comments on commit 9859940

Please sign in to comment.