Skip to content

Commit

Permalink
test(acceptance): bear tamer acceptance tests (#503)
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinezanardi committed Sep 19, 2023
1 parent 1f13ed2 commit 4800d60
Show file tree
Hide file tree
Showing 9 changed files with 39,963 additions and 38,024 deletions.
20 changes: 14 additions & 6 deletions src/modules/game/helpers/game.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,10 @@ function getFoxSniffedPlayers(sniffedTargetId: Types.ObjectId, game: Game): Play
}, []);
}

function getNearestAliveNeighbor(playerId: Types.ObjectId, game: Game, options: GetNearestPlayerOptions): Player | undefined {
const alivePlayers = getAlivePlayers(game);
alivePlayers.sort((a, b) => a.position - b.position);
const cantFindPlayerException = createCantFindPlayerUnexpectedException("getNearestAliveNeighbor", { gameId: game._id, playerId });
const player = getPlayerWithIdOrThrow(playerId, game, cantFindPlayerException);
function getNearestAliveNeighborInSortedAlivePlayers(player: Player, alivePlayers: Player[], options: GetNearestPlayerOptions): Player | undefined {
const indexHeading = options.direction === "left" ? -1 : 1;
let currentIndex = player.position + indexHeading;
const playerIndex = alivePlayers.findIndex(({ _id }) => _id.equals(player._id));
let currentIndex = playerIndex + indexHeading;
let count = 0;
while (count < alivePlayers.length) {
if (currentIndex < 0) {
Expand All @@ -175,6 +172,17 @@ function getNearestAliveNeighbor(playerId: Types.ObjectId, game: Game, options:
}
}

function getNearestAliveNeighbor(playerId: Types.ObjectId, game: Game, options: GetNearestPlayerOptions): Player | undefined {
const alivePlayers = getAlivePlayers(game);
alivePlayers.sort((a, b) => a.position - b.position);
const cantFindPlayerException = createCantFindPlayerUnexpectedException("getNearestAliveNeighbor", { gameId: game._id, playerId });
const player = getPlayerWithIdOrThrow(playerId, game, cantFindPlayerException);
if (!player.isAlive) {
return undefined;
}
return getNearestAliveNeighborInSortedAlivePlayers(player, alivePlayers, options);
}

function getExpectedPlayersToPlay(game: Game): Player[] {
const { currentPlay } = game;
const mustIncludeDeadPlayersGamePlayActions = [GamePlayActions.SHOOT, GamePlayActions.BAN_VOTING, GamePlayActions.DELEGATE];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@ import { Injectable } from "@nestjs/common";
import { GamePhases } from "@/modules/game/enums/game.enum";
import { PlayerAttributeNames } from "@/modules/game/enums/player.enum";
import { createGame } from "@/modules/game/helpers/game.factory";
import { getNearestAliveNeighbor, getPlayerWithIdOrThrow } from "@/modules/game/helpers/game.helper";
import { addPlayerAttributeInGame } from "@/modules/game/helpers/game.mutator";
import { createGrowledByBearTamerPlayerAttribute } from "@/modules/game/helpers/player/player-attribute/player-attribute.factory";
import { doesPlayerHaveActiveAttributeWithName, getActivePlayerAttributeWithName } from "@/modules/game/helpers/player/player-attribute/player-attribute.helper";
import { createPlayer } from "@/modules/game/helpers/player/player.factory";
import { isPlayerAliveAndPowerful } from "@/modules/game/helpers/player/player.helper";
import { GamePlayService } from "@/modules/game/providers/services/game-play/game-play.service";
import { PlayerAttributeService } from "@/modules/game/providers/services/player/player-attribute.service";
import type { Game } from "@/modules/game/schemas/game.schema";
import type { Player } from "@/modules/game/schemas/player/player.schema";
import { RoleNames, RoleSides } from "@/modules/role/enums/role.enum";

import { createCantFindPlayerUnexpectedException } from "@/shared/exception/helpers/unexpected-exception.factory";

@Injectable()
export class GamePhaseService {
Expand All @@ -17,11 +24,9 @@ export class GamePhaseService {
private readonly gamePlayService: GamePlayService,
) {}

public async applyEndingGamePhasePlayerAttributesOutcomesToPlayers(game: Game): Promise<Game> {
public async applyEndingGamePhaseOutcomes(game: Game): Promise<Game> {
let clonedGame = createGame(game);
for (const player of clonedGame.players) {
clonedGame = await this.applyEndingGamePhasePlayerAttributesOutcomesToPlayer(player, clonedGame);
}
clonedGame = await this.applyEndingGamePhasePlayerAttributesOutcomesToPlayers(clonedGame);
return clonedGame;
}

Expand All @@ -38,6 +43,22 @@ export class GamePhaseService {
return clonedGame;
}

public applyStartingGamePhaseOutcomes(game: Game): Game {
let clonedGame = createGame(game);
if (clonedGame.phase === GamePhases.DAY) {
clonedGame = this.applyStartingDayPlayerRoleOutcomesToPlayers(clonedGame);
}
return clonedGame;
}

private async applyEndingGamePhasePlayerAttributesOutcomesToPlayers(game: Game): Promise<Game> {
let clonedGame = createGame(game);
for (const player of clonedGame.players) {
clonedGame = await this.applyEndingGamePhasePlayerAttributesOutcomesToPlayer(player, clonedGame);
}
return clonedGame;
}

private async applyEndingDayPlayerAttributesOutcomesToPlayer(player: Player, game: Game): Promise<Game> {
let clonedGame = createGame(game);
const clonedPlayer = createPlayer(player);
Expand All @@ -49,11 +70,14 @@ export class GamePhaseService {

private async applyEndingNightPlayerAttributesOutcomesToPlayer(player: Player, game: Game): Promise<Game> {
let clonedGame = createGame(game);
const clonedPlayer = createPlayer(player);
let clonedPlayer = createPlayer(player);
const eatenAttribute = getActivePlayerAttributeWithName(clonedPlayer, PlayerAttributeNames.EATEN, clonedGame);
const notFoundPlayerExceptionInterpolations = { gameId: clonedGame._id, playerId: clonedPlayer._id };
const notFoundPlayerException = createCantFindPlayerUnexpectedException("applyEndingNightPlayerAttributesOutcomesToPlayer", notFoundPlayerExceptionInterpolations);
if (eatenAttribute) {
clonedGame = await this.playerAttributeService.applyEatenAttributeOutcomes(clonedPlayer, clonedGame, eatenAttribute);
}
clonedPlayer = getPlayerWithIdOrThrow(clonedPlayer._id, clonedGame, notFoundPlayerException);
if (doesPlayerHaveActiveAttributeWithName(clonedPlayer, PlayerAttributeNames.DRANK_DEATH_POTION, clonedGame)) {
clonedGame = await this.playerAttributeService.applyDrankDeathPotionAttributeOutcomes(clonedPlayer, clonedGame);
}
Expand All @@ -68,4 +92,28 @@ export class GamePhaseService {
}
return this.applyEndingDayPlayerAttributesOutcomesToPlayer(clonedPlayer, clonedGame);
}

private applyStartingDayBearTamerRoleOutcomes(bearTamerPlayer: Player, game: Game): Game {
const clonedGame = createGame(game);
const leftAliveNeighbor = getNearestAliveNeighbor(bearTamerPlayer._id, clonedGame, { direction: "left" });
const rightAliveNeighbor = getNearestAliveNeighbor(bearTamerPlayer._id, clonedGame, { direction: "right" });
const doesBearTamerHaveWerewolfSidedNeighbor = leftAliveNeighbor?.side.current === RoleSides.WEREWOLVES || rightAliveNeighbor?.side.current === RoleSides.WEREWOLVES;
const { doesGrowlIfInfected } = clonedGame.options.roles.bearTamer;
const isBearTamerInfected = bearTamerPlayer.side.current === RoleSides.WEREWOLVES;
if (doesGrowlIfInfected && isBearTamerInfected || doesBearTamerHaveWerewolfSidedNeighbor) {
const growledByBearTamerPlayerAttribute = createGrowledByBearTamerPlayerAttribute();
return addPlayerAttributeInGame(bearTamerPlayer._id, clonedGame, growledByBearTamerPlayerAttribute);
}
return clonedGame;
}

private applyStartingDayPlayerRoleOutcomesToPlayers(game: Game): Game {
let clonedGame = createGame(game);
for (const player of clonedGame.players) {
if (player.role.current === RoleNames.BEAR_TAMER && isPlayerAliveAndPowerful(player, clonedGame)) {
clonedGame = this.applyStartingDayBearTamerRoleOutcomes(player, clonedGame);
}
}
return clonedGame;
}
}
3 changes: 2 additions & 1 deletion src/modules/game/providers/services/game.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,10 @@ export class GameService {

private async handleGamePhaseCompletion(game: Game): Promise<Game> {
let clonedGame = createGameFromFactory(game);
clonedGame = await this.gamePhaseService.applyEndingGamePhasePlayerAttributesOutcomesToPlayers(clonedGame);
clonedGame = await this.gamePhaseService.applyEndingGamePhaseOutcomes(clonedGame);
clonedGame = this.playerAttributeService.decreaseRemainingPhasesAndRemoveObsoletePlayerAttributes(clonedGame);
clonedGame = await this.gamePhaseService.switchPhaseAndAppendGamePhaseUpcomingPlays(clonedGame);
clonedGame = this.gamePhaseService.applyStartingGamePhaseOutcomes(clonedGame);
return this.gamePlayService.proceedToNextGamePlay(clonedGame);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"roles": {
"bearTamer": {
"doesGrowlIfInfected": false
}
}
}
60 changes: 60 additions & 0 deletions tests/acceptance/features/game/features/role/bear-tamer.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
@bear-tamer-role

Feature: 馃惢 Bear Tamer role

Scenario: 馃惢 Bear Tamer's bear growls when one of his neighbor is a werewolf

Given a created game with options described in file no-sheriff-option.json and with the following players
| name | role |
| Olivia | villager |
| Antoine | bear-tamer |
| JB | villager |
| Thomas | werewolf |
| Doudou | villager |
Then the game's current play should be werewolves to eat

When the werewolves eat the player named Olivia
Then the player named Olivia should be murdered by werewolves from eaten
And the player named Antoine should not have the active growled from bear-tamer attribute
And the game's current play should be survivors to vote

When the player or group skips his turn
Then the game's current play should be werewolves to eat

When the werewolves eat the player named Doudou
Then the player named Doudou should be murdered by werewolves from eaten
And the player named Antoine should have the active growled from bear-tamer attribute

Scenario: 馃惢 Bear Tamer's bear growls when he is infected even if any of his neighbor is a werewolf

Given a created game with options described in file no-sheriff-option.json and with the following players
| name | role |
| Olivia | villager |
| Antoine | bear-tamer |
| JB | villager |
| Thomas | vile-father-of-wolves |
| Doudou | villager |
Then the game's current play should be werewolves to eat

When the vile father of wolves infects the player named Antoine
Then the player named Antoine should be alive
And the player named Antoine should be on werewolves current side and originally be on villagers side
And the player named Antoine should have the active growled from bear-tamer attribute
And the game's phase should be day

Scenario: 馃惢 Bear Tamer's bear doesn't growl when he is infected even if any of his neighbor is a werewolf with the right option

Given a created game with options described in file no-sheriff-option.json, bear-tamer-bear-doesnt-growl-if-infected-option.json and with the following players
| name | role |
| Olivia | villager |
| Antoine | bear-tamer |
| JB | villager |
| Thomas | vile-father-of-wolves |
| Doudou | villager |
Then the game's current play should be werewolves to eat

When the vile father of wolves infects the player named Antoine
Then the player named Antoine should be alive
And the player named Antoine should be on werewolves current side and originally be on villagers side
And the player named Antoine should not have the active growled from bear-tamer attribute
And the game's phase should be day

0 comments on commit 4800d60

Please sign in to comment.