746 lines
23 KiB
C#
746 lines
23 KiB
C#
using FluentAssertions;
|
|
using Koogle.Application.Games;
|
|
using Koogle.Application.Games.DeathBox;
|
|
using Koogle.Domain.Enums;
|
|
using Xunit;
|
|
|
|
namespace Koogle.Tests.Application.Games;
|
|
|
|
/// <summary>
|
|
/// Unit tests for DeathBoxGameLogicService.
|
|
/// </summary>
|
|
public class DeathBoxGameLogicServiceTests
|
|
{
|
|
private readonly DeathBoxGameLogicService _sut;
|
|
|
|
public DeathBoxGameLogicServiceTests()
|
|
{
|
|
_sut = new DeathBoxGameLogicService();
|
|
}
|
|
|
|
#region CreateInitialModel Tests
|
|
|
|
[Fact]
|
|
public void CreateInitialModel_CreatesPlayerStates_ForAllPlayers()
|
|
{
|
|
// Arrange
|
|
var playerIds = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
|
|
var setup = new DeathBoxGameSetup { CoffinSize = 12 };
|
|
|
|
// Act
|
|
var result = _sut.CreateInitialModel(playerIds, setup);
|
|
|
|
// Assert
|
|
result.Should().BeOfType<DeathBoxGameModel>();
|
|
var model = (DeathBoxGameModel)result;
|
|
model.PlayerStates.Should().HaveCount(3);
|
|
model.PlayerStates.Keys.Should().BeEquivalentTo(playerIds);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateInitialModel_InitializesPlayersWithZeroMarks()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: 10);
|
|
|
|
// Act
|
|
var result = _sut.CreateInitialModel([playerId], setup);
|
|
|
|
// Assert
|
|
var model = (DeathBoxGameModel)result;
|
|
model.PlayerStates[playerId].Marks.Should().Be(0);
|
|
model.PlayerStates[playerId].XCount.Should().Be(1);
|
|
model.PlayerStates[playerId].EggCount.Should().Be(0);
|
|
model.PlayerStates[playerId].IsEliminated.Should().BeFalse();
|
|
model.CoffinSize.Should().Be(10);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(6)]
|
|
[InlineData(9)]
|
|
[InlineData(12)]
|
|
public void CreateInitialModel_SetsCoffinSize(int coffinSize)
|
|
{
|
|
// Arrange
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: coffinSize);
|
|
|
|
// Act
|
|
var result = _sut.CreateInitialModel([Guid.NewGuid()], setup);
|
|
|
|
// Assert
|
|
var model = (DeathBoxGameModel)result;
|
|
model.CoffinSize.Should().Be(coffinSize);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateInitialModel_SetsPlayerOrder()
|
|
{
|
|
// Arrange
|
|
var playerIds = new[] { Guid.NewGuid(), Guid.NewGuid() };
|
|
var setup = DeathBoxGameSetup.Create(randomizePlayerOrder: false);
|
|
|
|
// Act
|
|
var result = _sut.CreateInitialModel(playerIds, setup);
|
|
|
|
// Assert
|
|
var model = (DeathBoxGameModel)result;
|
|
model.PlayerOrder.Should().HaveCount(2);
|
|
model.CurrentPlayerIndex.Should().Be(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateInitialModel_StartsNotGameOver()
|
|
{
|
|
// Act
|
|
var result = _sut.CreateInitialModel([Guid.NewGuid()], null);
|
|
|
|
// Assert
|
|
var model = (DeathBoxGameModel)result;
|
|
model.IsGameOver.Should().BeFalse();
|
|
model.WinnerId.Should().BeNull();
|
|
model.EliminatedPlayers.Should().BeEmpty();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ProcessThrow Tests - New Round (9 pins)
|
|
|
|
[Fact]
|
|
public void ProcessThrow_NewRound_CollectsX()
|
|
{
|
|
// Arrange
|
|
var playerId1 = Guid.NewGuid();
|
|
var playerId2 = Guid.NewGuid();
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: 12);
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel([playerId1, playerId2], setup);
|
|
|
|
// New round with 9 pins knocked (>= 3) -> Board cleared
|
|
var afterThrow = CreateAfterThrowState(model.PlayerOrder.First(), 9, isNewRound: true, isCleared:true);
|
|
|
|
// Act
|
|
var (updatedModel, _) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (DeathBoxGameModel)updatedModel;
|
|
updated.PlayerStates[model.PlayerOrder.Last()].XCount.Should().Be(1);
|
|
updated.LastThrow!.EarnedX.Should().BeTrue();
|
|
updated.LastThrow.WasPenalty.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessThrow_NewRound_WithLessThan3Pins_AddsMark()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: 12);
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel([playerId], setup);
|
|
|
|
// New round with 2 pins knocked (< 3)
|
|
var afterThrow = CreateAfterThrowState(playerId, 2, isNewRound: true);
|
|
|
|
// Act
|
|
var (updatedModel, _) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (DeathBoxGameModel)updatedModel;
|
|
updated.PlayerStates[playerId].XCount.Should().Be(1);
|
|
updated.PlayerStates[playerId].Marks.Should().Be(1);
|
|
updated.LastThrow!.WasPenalty.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessThrow_ThreeXs_ConvertToOneMark()
|
|
{
|
|
// Arrange
|
|
var playerId1 = Guid.NewGuid();
|
|
var playerId2 = Guid.NewGuid();
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: 12);
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel([playerId1, playerId2], setup);
|
|
|
|
playerId1 = model.PlayerOrder.First();
|
|
playerId2 = model.PlayerOrder.Last();
|
|
// Set player to have 2 Xs already
|
|
model.PlayerStates[playerId2].XCount = 2;
|
|
|
|
// New round - will get 3rd X
|
|
// player1 clears the pins -> player2 will have to start new round and gets a X
|
|
var afterThrow = CreateAfterThrowState(playerId1, 9, isNewRound: true, isCleared:true);
|
|
|
|
// Act
|
|
var (updatedModel, _) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (DeathBoxGameModel)updatedModel;
|
|
updated.PlayerStates[playerId2].XCount.Should().Be(0);
|
|
updated.PlayerStates[playerId2].Marks.Should().Be(1);
|
|
updated.LastThrow!.ConvertedXsToMark.Should().BeTrue();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ProcessThrow Tests - Gutter/NoWood
|
|
|
|
[Fact]
|
|
public void ProcessThrow_Gutter_AddsMark()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: 12);
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel([playerId], setup);
|
|
|
|
var afterThrow = CreateAfterThrowState(playerId, 0, isGutter: true);
|
|
|
|
// Act
|
|
var (updatedModel, result) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (DeathBoxGameModel)updatedModel;
|
|
updated.PlayerStates[playerId].Marks.Should().Be(1);
|
|
updated.LastThrow!.WasGutter.Should().BeTrue();
|
|
result.Triggers.Should().ContainSingle(t => t.TriggerType == "Gutter");
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessThrow_NoWood_AddsMark()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: 12);
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel([playerId], setup);
|
|
|
|
var afterThrow = CreateAfterThrowState(playerId, 0, isGutter: false, isNoWood: true);
|
|
|
|
// Act
|
|
var (updatedModel, result) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (DeathBoxGameModel)updatedModel;
|
|
updated.PlayerStates[playerId].Marks.Should().Be(1);
|
|
updated.LastThrow!.WasNoWood.Should().BeTrue();
|
|
result.Triggers.Should().ContainSingle(t => t.TriggerType == "NoWood");
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessThrow_NewRoundWithZeroPins_OnlyOneMark()
|
|
{
|
|
// Arrange - new round with 0 pins should NOT double-penalize
|
|
var playerId = Guid.NewGuid();
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: 12);
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel([playerId], setup);
|
|
|
|
var afterThrow = CreateAfterThrowState(playerId, 0, isNewRound: true, isGutter: true);
|
|
|
|
// Act
|
|
var (updatedModel, _) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (DeathBoxGameModel)updatedModel;
|
|
// Only one mark from the <3 pins rule, not double from gutter
|
|
updated.PlayerStates[playerId].Marks.Should().Be(1);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ProcessThrow Tests - Cleared (Eggs)
|
|
|
|
[Fact]
|
|
public void ProcessThrow_Cleared_CollectsEgg()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var previousPlayer = Guid.NewGuid();
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: 12);
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel([previousPlayer, playerId], setup);
|
|
model = model with { PreviousPlayerId = previousPlayer };
|
|
|
|
var afterThrow = CreateAfterThrowState(playerId, 4, isCleared: true);
|
|
|
|
// Act
|
|
var (updatedModel, _) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (DeathBoxGameModel)updatedModel;
|
|
updated.PlayerStates[playerId].EggCount.Should().Be(1);
|
|
updated.LastThrow!.EarnedEgg.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessThrow_Cleared_PreviousPlayerGetsMark()
|
|
{
|
|
// Arrange
|
|
var currentPlayer = Guid.NewGuid();
|
|
var previousPlayer = Guid.NewGuid();
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: 12);
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel([previousPlayer, currentPlayer], setup);
|
|
model = model with { PreviousPlayerId = previousPlayer };
|
|
|
|
var afterThrow = CreateAfterThrowState(currentPlayer, 4, isCleared: true);
|
|
|
|
// Act
|
|
var (updatedModel, _) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (DeathBoxGameModel)updatedModel;
|
|
updated.PlayerStates[previousPlayer].Marks.Should().Be(1);
|
|
updated.LastThrow!.PreviousPlayerGotMark.Should().BeTrue();
|
|
updated.LastThrow.PreviousPlayerPenalizedId.Should().Be(previousPlayer);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessThrow_ThreeEggs_RemovesOneMark()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var previousPlayer = Guid.NewGuid();
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: 12);
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel([previousPlayer, playerId], setup);
|
|
model = model with { PreviousPlayerId = previousPlayer };
|
|
|
|
// Set player to have 2 eggs and 1 mark
|
|
model.PlayerStates[playerId].EggCount = 2;
|
|
model.PlayerStates[playerId].Marks = 1;
|
|
|
|
var afterThrow = CreateAfterThrowState(playerId, 4, isCleared: true);
|
|
|
|
// Act
|
|
var (updatedModel, _) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (DeathBoxGameModel)updatedModel;
|
|
updated.PlayerStates[playerId].EggCount.Should().Be(0);
|
|
updated.PlayerStates[playerId].Marks.Should().Be(0);
|
|
updated.LastThrow!.ConvertedEggsToRemoveMark.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessThrow_ThreeEggsWithNoMarks_EggsExpire()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var previousPlayer = Guid.NewGuid();
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: 12);
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel([previousPlayer, playerId], setup);
|
|
model = model with { PreviousPlayerId = previousPlayer };
|
|
|
|
// Set player to have 2 eggs but 0 marks
|
|
model.PlayerStates[playerId].EggCount = 2;
|
|
model.PlayerStates[playerId].Marks = 0;
|
|
|
|
var afterThrow = CreateAfterThrowState(playerId, 4, isCleared: true);
|
|
|
|
// Act
|
|
var (updatedModel, _) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (DeathBoxGameModel)updatedModel;
|
|
updated.PlayerStates[playerId].EggCount.Should().Be(0);
|
|
updated.PlayerStates[playerId].Marks.Should().Be(0);
|
|
updated.LastThrow!.ConvertedEggsToRemoveMark.Should().BeFalse();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Elimination Tests
|
|
|
|
[Fact]
|
|
public void ProcessThrow_PlayerReachesCoffinSize_IsEliminated()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var otherPlayer = Guid.NewGuid();
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: 3);
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel([playerId, otherPlayer], setup);
|
|
|
|
// Set player to have 2 marks (one away from elimination)
|
|
model.PlayerStates[playerId].Marks = 2;
|
|
|
|
var afterThrow = CreateAfterThrowState(playerId, 0, isGutter: true);
|
|
|
|
// Act
|
|
var (updatedModel, result) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (DeathBoxGameModel)updatedModel;
|
|
updated.PlayerStates[playerId].IsEliminated.Should().BeTrue();
|
|
updated.EliminatedPlayers.Should().Contain(playerId);
|
|
updated.LastThrow!.PlayerEliminated.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessThrow_Elimination_TriggersExpensePoint()
|
|
{
|
|
// Arrange
|
|
var eliminated = Guid.NewGuid();
|
|
var survivor1 = Guid.NewGuid();
|
|
var survivor2 = Guid.NewGuid();
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: 3);
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel([eliminated, survivor1, survivor2], setup);
|
|
|
|
model.PlayerStates[eliminated].Marks = 2;
|
|
|
|
var afterThrow = CreateAfterThrowState(eliminated, 0, isGutter: true);
|
|
|
|
// Act
|
|
var (_, result) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert - remaining players = 2, so multiplier = 2
|
|
result.Triggers.Should().Contain(t =>
|
|
t.TriggerType == "ExpensePoint" &&
|
|
t.PersonId == eliminated &&
|
|
t.Multiplier == 2);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessThrow_PreviousPlayerEliminated_ByClear()
|
|
{
|
|
// Arrange
|
|
var currentPlayer = Guid.NewGuid();
|
|
var previousPlayer = Guid.NewGuid();
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: 3);
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel([previousPlayer, currentPlayer], setup);
|
|
model = model with { PreviousPlayerId = previousPlayer };
|
|
|
|
// Previous player at 2 marks
|
|
model.PlayerStates[previousPlayer].Marks = 2;
|
|
|
|
var afterThrow = CreateAfterThrowState(currentPlayer, 4, isCleared: true);
|
|
|
|
// Act
|
|
var (updatedModel, result) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (DeathBoxGameModel)updatedModel;
|
|
updated.PlayerStates[previousPlayer].IsEliminated.Should().BeTrue();
|
|
updated.LastThrow!.PreviousPlayerEliminated.Should().BeTrue();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Game End Tests
|
|
|
|
[Fact]
|
|
public void ProcessThrow_LastPlayerStanding_WinsGame()
|
|
{
|
|
// Arrange
|
|
var player1 = Guid.NewGuid();
|
|
var player2 = Guid.NewGuid();
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: 3);
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel([player1, player2], setup);
|
|
|
|
// Player1 already eliminated
|
|
model.PlayerStates[player1].IsEliminated = true;
|
|
model.PlayerStates[player1].Marks = 3;
|
|
model = model with { EliminatedPlayers = [player1] };
|
|
|
|
// Player2 throws normally
|
|
var afterThrow = CreateAfterThrowState(player2, 5);
|
|
|
|
// Act
|
|
var (updatedModel, result) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (DeathBoxGameModel)updatedModel;
|
|
updated.IsGameOver.Should().BeTrue();
|
|
updated.WinnerId.Should().Be(player2);
|
|
result.IsGameOver.Should().BeTrue();
|
|
result.WinnerId.Should().Be(player2);
|
|
}
|
|
|
|
[Fact]
|
|
public void CheckGameEnd_ReturnsFalse_WhenMultiplePlayersActive()
|
|
{
|
|
// Arrange
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel(
|
|
[Guid.NewGuid(), Guid.NewGuid()], null);
|
|
|
|
// Act
|
|
var (isGameOver, winnerId) = _sut.CheckGameEnd(model);
|
|
|
|
// Assert
|
|
isGameOver.Should().BeFalse();
|
|
winnerId.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void CheckGameEnd_ReturnsTrue_WhenGameOver()
|
|
{
|
|
// Arrange
|
|
var winner = Guid.NewGuid();
|
|
var model = new DeathBoxGameModel
|
|
{
|
|
IsGameOver = true,
|
|
WinnerId = winner,
|
|
PlayerStates = new Dictionary<Guid, DeathBoxPlayerState>
|
|
{
|
|
[winner] = new DeathBoxPlayerState()
|
|
}
|
|
};
|
|
|
|
// Act
|
|
var (isGameOver, winnerId) = _sut.CheckGameEnd(model);
|
|
|
|
// Assert
|
|
isGameOver.Should().BeTrue();
|
|
winnerId.Should().Be(winner);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ValidateSetup Tests
|
|
|
|
[Fact]
|
|
public void ValidateSetup_AcceptsNullOptions()
|
|
{
|
|
// Act
|
|
var result = _sut.ValidateSetup(null);
|
|
|
|
// Assert
|
|
result.IsValid.Should().BeTrue();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(6)]
|
|
[InlineData(9)]
|
|
[InlineData(12)]
|
|
public void ValidateSetup_AcceptsValidCoffinSize(int coffinSize)
|
|
{
|
|
// Arrange
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: coffinSize);
|
|
|
|
// Act
|
|
var result = _sut.ValidateSetup(setup);
|
|
|
|
// Assert
|
|
result.IsValid.Should().BeTrue();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(5)]
|
|
[InlineData(13)]
|
|
[InlineData(100)]
|
|
public void ValidateSetup_RejectsInvalidCoffinSize(int coffinSize)
|
|
{
|
|
// Arrange
|
|
var setup = new DeathBoxGameSetup { CoffinSize = coffinSize };
|
|
|
|
// Act
|
|
var result = _sut.ValidateSetup(setup);
|
|
|
|
// Assert
|
|
result.IsValid.Should().BeFalse();
|
|
result.Errors.Should().Contain(e => e.Contains("Sarggröße"));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetPlayerStats Tests
|
|
|
|
[Fact]
|
|
public void GetPlayerStats_ReturnsMarksAsCurrentPoints()
|
|
{
|
|
// Arrange
|
|
var player1 = Guid.NewGuid();
|
|
var player2 = Guid.NewGuid();
|
|
var model = new DeathBoxGameModel
|
|
{
|
|
CoffinSize = 12,
|
|
PlayerStates = new Dictionary<Guid, DeathBoxPlayerState>
|
|
{
|
|
[player1] = new DeathBoxPlayerState { Marks = 3, XCount = 1, EggCount = 2 },
|
|
[player2] = new DeathBoxPlayerState { Marks = 7, XCount = 0, EggCount = 0 }
|
|
}
|
|
};
|
|
|
|
// Act
|
|
var stats = _sut.GetPlayerStats(model);
|
|
|
|
// Assert
|
|
stats[player1].CurrentPoints.Should().Be(3);
|
|
stats[player1].CustomStats["XCount"].Should().Be(1);
|
|
stats[player1].CustomStats["EggCount"].Should().Be(2);
|
|
stats[player2].CurrentPoints.Should().Be(7);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetPlayerStats_IndicatesWinnerAndEliminated()
|
|
{
|
|
// Arrange
|
|
var winner = Guid.NewGuid();
|
|
var loser = Guid.NewGuid();
|
|
var model = new DeathBoxGameModel
|
|
{
|
|
CoffinSize = 12,
|
|
IsGameOver = true,
|
|
WinnerId = winner,
|
|
EliminatedPlayers = [loser],
|
|
PlayerStates = new Dictionary<Guid, DeathBoxPlayerState>
|
|
{
|
|
[winner] = new DeathBoxPlayerState { Marks = 5, IsEliminated = false },
|
|
[loser] = new DeathBoxPlayerState { Marks = 12, IsEliminated = true }
|
|
}
|
|
};
|
|
|
|
// Act
|
|
var stats = _sut.GetPlayerStats(model);
|
|
|
|
// Assert
|
|
stats[winner].CustomStats["IsWinner"].Should().Be(true);
|
|
stats[winner].CustomStats["IsEliminated"].Should().Be(false);
|
|
stats[loser].CustomStats["IsWinner"].Should().Be(false);
|
|
stats[loser].CustomStats["IsEliminated"].Should().Be(true);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetAvailableActions Tests
|
|
|
|
[Fact]
|
|
public void GetAvailableActions_ReturnsEmpty()
|
|
{
|
|
// Arrange - DeathBox has no custom actions
|
|
var playerId = Guid.NewGuid();
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel([playerId], null);
|
|
|
|
// Act
|
|
var actions = _sut.GetAvailableActions(model, playerId);
|
|
|
|
// Assert
|
|
actions.Should().BeEmpty();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Edge Cases
|
|
|
|
[Fact]
|
|
public void ProcessThrow_DoesNothing_WhenGameAlreadyOver()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var model = new DeathBoxGameModel
|
|
{
|
|
IsGameOver = true,
|
|
WinnerId = Guid.NewGuid(),
|
|
CoffinSize = 12,
|
|
PlayerStates = new Dictionary<Guid, DeathBoxPlayerState>
|
|
{
|
|
[playerId] = new DeathBoxPlayerState { Marks = 5 }
|
|
},
|
|
PlayerOrder = [playerId]
|
|
};
|
|
|
|
var afterThrow = CreateAfterThrowState(playerId, 0, isGutter: true);
|
|
|
|
// Act
|
|
var (updatedModel, result) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (DeathBoxGameModel)updatedModel;
|
|
updated.PlayerStates[playerId].Marks.Should().Be(5); // Unchanged
|
|
result.IsGameOver.Should().BeTrue();
|
|
result.ShouldRotatePlayer.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessThrow_SkipsEliminatedPlayers_WhenRotating()
|
|
{
|
|
// Arrange
|
|
var player1 = Guid.NewGuid();
|
|
var player2 = Guid.NewGuid();
|
|
var player3 = Guid.NewGuid();
|
|
var setup = DeathBoxGameSetup.Create(coffinSize: 12, randomizePlayerOrder: false);
|
|
var model = (DeathBoxGameModel)_sut.CreateInitialModel([player1, player2, player3], setup);
|
|
|
|
// Player2 is eliminated
|
|
model.PlayerStates[player2].IsEliminated = true;
|
|
model = model with
|
|
{
|
|
CurrentPlayerIndex = 0,
|
|
EliminatedPlayers = [player2]
|
|
};
|
|
|
|
var afterThrow = CreateAfterThrowState(player1, 5);
|
|
|
|
// Act
|
|
var (updatedModel, result) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (DeathBoxGameModel)updatedModel;
|
|
// Should skip player2 (index 1) and go to player3 (index 2)
|
|
result.Overrides!.NextPlayerId.Should().Be(player3);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private static AfterThrowState CreateAfterThrowState(
|
|
Guid playerId,
|
|
int pinsKnocked,
|
|
bool isNewRound = false,
|
|
bool isGutter = false,
|
|
bool isNoWood = false,
|
|
bool isCleared = false)
|
|
{
|
|
var pins = new PinStatus[9];
|
|
|
|
if (isCleared)
|
|
{
|
|
// All pins are now fallen after this throw (cleared)
|
|
for (var i = 0; i < 9; i++)
|
|
{
|
|
pins[i] = PinStatus.Fallen;
|
|
}
|
|
}
|
|
else if (isGutter || isNoWood)
|
|
{
|
|
// All pins still standing
|
|
for (var i = 0; i < 9; i++)
|
|
{
|
|
pins[i] = PinStatus.Standing;
|
|
}
|
|
}
|
|
else if (isNewRound)
|
|
{
|
|
// New round: 9 pins were standing, pinsKnocked were knocked down
|
|
// After throw: 9 - pinsKnocked are still standing
|
|
for (var i = 0; i < 9; i++)
|
|
{
|
|
pins[i] = i < (9 - pinsKnocked) ? PinStatus.Standing : PinStatus.Fallen;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Normal throw: some pins knocked
|
|
for (var i = 0; i < 9; i++)
|
|
{
|
|
pins[i] = i < pinsKnocked ? PinStatus.Fallen : PinStatus.Standing;
|
|
}
|
|
}
|
|
|
|
var throwPanel = new ThrowPanelSnapshot
|
|
{
|
|
Pins = pins,
|
|
ThrowsPerRound = 1,
|
|
ThrowCounterPerRound = 1,
|
|
TotalThrowCounter = 1,
|
|
ThrowMode = ThrowMode.Decrease,
|
|
BellValue = false
|
|
};
|
|
|
|
return new AfterThrowState(
|
|
ThrowPanel: throwPanel,
|
|
CurrentPlayerId: playerId,
|
|
PinsKnocked: pinsKnocked,
|
|
IsCircle: false,
|
|
IsStrike: pinsKnocked == 9 && isNewRound,
|
|
IsGutter: isGutter,
|
|
IsCleared: isCleared
|
|
);
|
|
}
|
|
|
|
#endregion
|
|
}
|