KoogleApp/test/Koogle.Tests/Application/Games/TrainingGameLogicServiceTes...

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
}