408 lines
12 KiB
C#
408 lines
12 KiB
C#
using FluentAssertions;
|
|
using Koogle.Application.Games;
|
|
using Koogle.Application.Games.Training;
|
|
using Koogle.Domain.Enums;
|
|
using Xunit;
|
|
|
|
namespace Koogle.Tests.Application.Games;
|
|
|
|
/// <summary>
|
|
/// Unit tests for TrainingGameLogicService.
|
|
/// </summary>
|
|
public class TrainingGameLogicServiceTests
|
|
{
|
|
private readonly TrainingGameLogicService _sut;
|
|
|
|
public TrainingGameLogicServiceTests()
|
|
{
|
|
_sut = new TrainingGameLogicService();
|
|
}
|
|
|
|
#region CreateInitialModel Tests
|
|
|
|
[Fact]
|
|
public void CreateInitialModel_CreatesPlayerStats_ForAllPlayers()
|
|
{
|
|
// Arrange
|
|
var playerIds = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
|
|
|
|
// Act
|
|
var result = _sut.CreateInitialModel(playerIds, null);
|
|
|
|
// Assert
|
|
result.Should().BeOfType<TrainingGameModel>();
|
|
var model = (TrainingGameModel)result;
|
|
model.PlayerStatistics.Should().HaveCount(3);
|
|
model.PlayerStatistics.Keys.Should().BeEquivalentTo(playerIds);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateInitialModel_InitializesStatsToZero()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
|
|
// Act
|
|
var result = _sut.CreateInitialModel([playerId], null);
|
|
|
|
// Assert
|
|
var model = (TrainingGameModel)result;
|
|
var stats = model.PlayerStatistics[playerId];
|
|
stats.ThrowCount.Should().Be(0);
|
|
stats.PinCount.Should().Be(0);
|
|
stats.CircleCount.Should().Be(0);
|
|
stats.StrikeCount.Should().Be(0);
|
|
stats.GutterCount.Should().Be(0);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ProcessThrow Tests - Pin Count
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(1)]
|
|
[InlineData(5)]
|
|
[InlineData(8)]
|
|
[InlineData(9)]
|
|
public void ProcessThrow_UpdatesPinCount_Correctly(int pinsKnocked)
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var model = (TrainingGameModel)_sut.CreateInitialModel([playerId], null);
|
|
var afterThrow = CreateAfterThrowState(playerId, pinsKnocked);
|
|
|
|
// Act
|
|
var (updatedModel, _) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (TrainingGameModel)updatedModel;
|
|
updated.PlayerStatistics[playerId].PinCount.Should().Be(pinsKnocked);
|
|
updated.PlayerStatistics[playerId].ThrowCount.Should().Be(1);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessThrow_AccumulatesPins_AcrossMultipleThrows()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var model = (TrainingGameModel)_sut.CreateInitialModel([playerId], null);
|
|
|
|
// Act - 3 throws
|
|
var (m1, _) = _sut.ProcessThrow(model, CreateAfterThrowState(playerId, 5));
|
|
var (m2, _) = _sut.ProcessThrow(m1, CreateAfterThrowState(playerId, 3));
|
|
var (m3, _) = _sut.ProcessThrow(m2, CreateAfterThrowState(playerId, 7));
|
|
|
|
// Assert
|
|
var updated = (TrainingGameModel)m3;
|
|
updated.PlayerStatistics[playerId].PinCount.Should().Be(15);
|
|
updated.PlayerStatistics[playerId].ThrowCount.Should().Be(3);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ProcessThrow Tests - Circle Detection (Kranz)
|
|
|
|
[Fact]
|
|
public void ProcessThrow_DetectsCircle_WhenIsCircleTrue()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var model = (TrainingGameModel)_sut.CreateInitialModel([playerId], null);
|
|
var afterThrow = CreateAfterThrowState(playerId, 8, isCircle: true);
|
|
|
|
// Act
|
|
var (updatedModel, _) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (TrainingGameModel)updatedModel;
|
|
updated.PlayerStatistics[playerId].CircleCount.Should().Be(1);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessThrow_AccumulatesCircles()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var model = (TrainingGameModel)_sut.CreateInitialModel([playerId], null);
|
|
|
|
// Act - 3 circles
|
|
var (m1, _) = _sut.ProcessThrow(model, CreateAfterThrowState(playerId, 8, isCircle: true));
|
|
var (m2, _) = _sut.ProcessThrow(m1, CreateAfterThrowState(playerId, 8, isCircle: true));
|
|
var (m3, _) = _sut.ProcessThrow(m2, CreateAfterThrowState(playerId, 8, isCircle: true));
|
|
|
|
// Assert
|
|
var updated = (TrainingGameModel)m3;
|
|
updated.PlayerStatistics[playerId].CircleCount.Should().Be(3);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ProcessThrow Tests - Strike Detection
|
|
|
|
[Fact]
|
|
public void ProcessThrow_DetectsStrike_WhenAllNinePinsKnocked()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var model = (TrainingGameModel)_sut.CreateInitialModel([playerId], null);
|
|
var afterThrow = CreateAfterThrowState(playerId, 9, isStrike: true);
|
|
|
|
// Act
|
|
var (updatedModel, _) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (TrainingGameModel)updatedModel;
|
|
updated.PlayerStatistics[playerId].StrikeCount.Should().Be(1);
|
|
updated.PlayerStatistics[playerId].PinCount.Should().Be(9);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ProcessThrow Tests - Gutter Detection (Rinne)
|
|
|
|
[Fact]
|
|
public void ProcessThrow_DetectsGutter_WhenNoPinsKnocked()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var model = (TrainingGameModel)_sut.CreateInitialModel([playerId], null);
|
|
var afterThrow = CreateAfterThrowState(playerId, 0, isGutter: true);
|
|
|
|
// Act
|
|
var (updatedModel, _) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
var updated = (TrainingGameModel)updatedModel;
|
|
updated.PlayerStatistics[playerId].GutterCount.Should().Be(1);
|
|
updated.PlayerStatistics[playerId].PinCount.Should().Be(0);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region CheckGameEnd Tests
|
|
|
|
[Fact]
|
|
public void CheckGameEnd_NeverEndsAutomatically()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var model = (TrainingGameModel)_sut.CreateInitialModel([playerId], null);
|
|
|
|
// Simulate many throws
|
|
for (var i = 0; i < 100; i++)
|
|
{
|
|
var (updated, _) = _sut.ProcessThrow(model, CreateAfterThrowState(playerId, 9, isStrike: true));
|
|
model = (TrainingGameModel)updated;
|
|
}
|
|
|
|
// Act
|
|
var (isGameOver, winnerId) = _sut.CheckGameEnd(model);
|
|
|
|
// Assert
|
|
isGameOver.Should().BeFalse();
|
|
winnerId.Should().BeNull();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetPlayerStats Tests
|
|
|
|
[Fact]
|
|
public void GetPlayerStats_ReturnsCorrectSummary()
|
|
{
|
|
// Arrange
|
|
var player1 = Guid.NewGuid();
|
|
var player2 = Guid.NewGuid();
|
|
var model = (TrainingGameModel)_sut.CreateInitialModel([player1, player2], null);
|
|
|
|
// Player1: 3 throws, 15 pins, 1 circle
|
|
var (m1, _) = _sut.ProcessThrow(model, CreateAfterThrowState(player1, 5));
|
|
var (m2, _) = _sut.ProcessThrow(m1, CreateAfterThrowState(player1, 8, isCircle: true));
|
|
var (m3, _) = _sut.ProcessThrow(m2, CreateAfterThrowState(player1, 2));
|
|
|
|
// Player2: 2 throws, 18 pins, 1 strike
|
|
var (m4, _) = _sut.ProcessThrow(m3, CreateAfterThrowState(player2, 9, isStrike: true));
|
|
var (m5, _) = _sut.ProcessThrow(m4, CreateAfterThrowState(player2, 9, isStrike: true));
|
|
|
|
// Act
|
|
var stats = _sut.GetPlayerStats(m5);
|
|
|
|
// Assert
|
|
stats.Should().HaveCount(2);
|
|
|
|
stats[player1].ThrowCount.Should().Be(3);
|
|
stats[player1].PinCount.Should().Be(15);
|
|
stats[player1].CircleCount.Should().Be(1);
|
|
stats[player1].Average.Should().Be(5.0);
|
|
|
|
stats[player2].ThrowCount.Should().Be(2);
|
|
stats[player2].PinCount.Should().Be(18);
|
|
stats[player2].StrikeCount.Should().Be(2);
|
|
stats[player2].Average.Should().Be(9.0);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ValidateSetup Tests
|
|
|
|
[Fact]
|
|
public void ValidateSetup_AcceptsNullOptions()
|
|
{
|
|
// Act
|
|
var result = _sut.ValidateSetup(null);
|
|
|
|
// Assert
|
|
result.IsValid.Should().BeTrue();
|
|
result.Errors.Should().BeEmpty();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(3)]
|
|
[InlineData(5)]
|
|
public void ValidateSetup_AcceptsValidThrowsPerRound(int throwsPerRound)
|
|
{
|
|
// Arrange
|
|
var setup = new TrainingGameSetup { ThrowsPerRound = throwsPerRound };
|
|
|
|
// Act
|
|
var result = _sut.ValidateSetup(setup);
|
|
|
|
// Assert
|
|
result.IsValid.Should().BeTrue();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(-1)]
|
|
[InlineData(6)]
|
|
[InlineData(10)]
|
|
public void ValidateSetup_RejectsInvalidThrowsPerRound(int throwsPerRound)
|
|
{
|
|
// Arrange
|
|
var setup = new TrainingGameSetup { ThrowsPerRound = throwsPerRound };
|
|
|
|
// Act
|
|
var result = _sut.ValidateSetup(setup);
|
|
|
|
// Assert
|
|
result.IsValid.Should().BeFalse();
|
|
result.Errors.Should().NotBeEmpty();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetAvailableActions Tests
|
|
|
|
[Fact]
|
|
public void GetAvailableActions_ReturnsEmptyList()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var model = _sut.CreateInitialModel([playerId], null);
|
|
|
|
// Act
|
|
var actions = _sut.GetAvailableActions(model, playerId);
|
|
|
|
// Assert
|
|
actions.Should().BeEmpty();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Player Rotation Tests
|
|
|
|
[Fact]
|
|
public void ProcessThrow_ShouldRotate_WhenRoundComplete()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var model = (TrainingGameModel)_sut.CreateInitialModel([playerId], null);
|
|
var afterThrow = CreateAfterThrowState(
|
|
playerId, 5,
|
|
throwCounterPerRound: 3,
|
|
throwsPerRound: 3);
|
|
|
|
// Act
|
|
var (_, result) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
result.ShouldRotatePlayer.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void ProcessThrow_ShouldNotRotate_WhenRoundIncomplete()
|
|
{
|
|
// Arrange
|
|
var playerId = Guid.NewGuid();
|
|
var model = (TrainingGameModel)_sut.CreateInitialModel([playerId], null);
|
|
var afterThrow = CreateAfterThrowState(
|
|
playerId, 5,
|
|
throwCounterPerRound: 1,
|
|
throwsPerRound: 3);
|
|
|
|
// Act
|
|
var (_, result) = _sut.ProcessThrow(model, afterThrow);
|
|
|
|
// Assert
|
|
result.ShouldRotatePlayer.Should().BeFalse();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private static AfterThrowState CreateAfterThrowState(
|
|
Guid playerId,
|
|
int pinsKnocked,
|
|
bool isCircle = false,
|
|
bool isStrike = false,
|
|
bool isGutter = false,
|
|
bool isCleared = false,
|
|
int throwCounterPerRound = 1,
|
|
int throwsPerRound = 3)
|
|
{
|
|
// Create pin array based on what's knocked
|
|
var pins = new PinStatus[9];
|
|
for (var i = 0; i < 9; i++)
|
|
{
|
|
pins[i] = i < pinsKnocked ? PinStatus.Fallen : PinStatus.Standing;
|
|
}
|
|
|
|
// Adjust for circle (8 outer pins fallen, center standing)
|
|
if (isCircle)
|
|
{
|
|
pins = new[]
|
|
{
|
|
PinStatus.Fallen, PinStatus.Fallen, PinStatus.Fallen,
|
|
PinStatus.Fallen, PinStatus.Standing, PinStatus.Fallen,
|
|
PinStatus.Fallen, PinStatus.Fallen, PinStatus.Fallen
|
|
};
|
|
}
|
|
|
|
var throwPanel = new ThrowPanelSnapshot
|
|
{
|
|
Pins = pins,
|
|
ThrowsPerRound = throwsPerRound,
|
|
ThrowCounterPerRound = throwCounterPerRound,
|
|
TotalThrowCounter = 1,
|
|
ThrowMode = ThrowMode.Reposition,
|
|
BellValue = false
|
|
};
|
|
|
|
return new AfterThrowState(
|
|
ThrowPanel: throwPanel,
|
|
CurrentPlayerId: playerId,
|
|
PinsKnocked: pinsKnocked,
|
|
IsCircle: isCircle,
|
|
IsStrike: isStrike,
|
|
IsGutter: isGutter,
|
|
IsCleared: isCleared,
|
|
IsBell: false
|
|
);
|
|
}
|
|
|
|
#endregion
|
|
}
|