diff --git a/src/clients/Codebreaker.GameAPIs.Client/Models/CreateGameRequest.cs b/src/clients/Codebreaker.GameAPIs.Client/Models/CreateGameRequest.cs index 27fb5044..6c081da3 100644 --- a/src/clients/Codebreaker.GameAPIs.Client/Models/CreateGameRequest.cs +++ b/src/clients/Codebreaker.GameAPIs.Client/Models/CreateGameRequest.cs @@ -12,6 +12,7 @@ public enum GameType Game6x4Mini, Game8x5, Game5x5x4, + Game5x3, } /// diff --git a/src/services/bot/CodeBreaker.Bot/CodeBreakerAlgorithms.cs b/src/services/bot/CodeBreaker.Bot/CodeBreakerAlgorithms.cs index b1e508b5..8f6f9e89 100644 --- a/src/services/bot/CodeBreaker.Bot/CodeBreakerAlgorithms.cs +++ b/src/services/bot/CodeBreaker.Bot/CodeBreakerAlgorithms.cs @@ -21,6 +21,7 @@ public static string[] IntToColors(this int value, GameType gameType, Dictionary GameType.Game6x4 => IntToColors6x4(value, colorNames), GameType.Game8x5 => IntToColors8x5(value, colorNames), GameType.Game5x5x4 => IntToColors5x5x4(value, colorNames), + GameType.Game5x3 => IntToColors5x3(value, colorNames), _ => IntToColors6x4(value, colorNames) }; } @@ -81,6 +82,22 @@ private static string[] IntToColors5x5x4(int value, Dictionary? col return colorNamesArray; } + private static string[] IntToColors5x3(int value, Dictionary? colorNames) + { + int i1 = (value >> 0) & 0b111111; + int i2 = (value >> 6) & 0b111111; + int i3 = (value >> 12) & 0b111111; + + string[] colorNamesArray = + [ + colorNames?[i3] ?? $"Unknown{i3}", + colorNames?[i2] ?? $"Unknown{i2}", + colorNames?[i1] ?? $"Unknown{i1}" + ]; + + return colorNamesArray; + } + /// /// Reduces the possible values based on the black matches with the selection /// @@ -292,6 +309,7 @@ private static int GetFieldsCount(GameType gameType) => GameType.Game6x4 => 4, GameType.Game8x5 => 5, GameType.Game5x5x4 => 4, + GameType.Game5x3 => 3, _ => 4 }; @@ -301,6 +319,7 @@ private static int GetBitsPerField(GameType gameType) => GameType.Game6x4 => 6, // 6 bits for up to 64 values (using 6) GameType.Game8x5 => 6, // 6 bits for up to 64 values (using 8) GameType.Game5x5x4 => 6, // 6 bits for up to 64 values (using 25, but limited per position) + GameType.Game5x3 => 6, // 6 bits for up to 64 values (using 5) _ => 6 }; } \ No newline at end of file diff --git a/src/services/bot/CodeBreaker.BotWithString/StringCodeBreakerAlgorithms.cs b/src/services/bot/CodeBreaker.BotWithString/StringCodeBreakerAlgorithms.cs index 8b0159ae..b650f727 100644 --- a/src/services/bot/CodeBreaker.BotWithString/StringCodeBreakerAlgorithms.cs +++ b/src/services/bot/CodeBreaker.BotWithString/StringCodeBreakerAlgorithms.cs @@ -17,6 +17,7 @@ private static int GetFieldsCount(GameType gameType) => GameType.Game6x4 => 4, GameType.Game8x5 => 5, GameType.Game5x5x4 => 4, + GameType.Game5x3 => 3, _ => 4 }; diff --git a/src/services/common/Codebreaker.Data.Cosmos/Codebreaker.Data.Cosmos.csproj b/src/services/common/Codebreaker.Data.Cosmos/Codebreaker.Data.Cosmos.csproj index 320e2d65..df2b3232 100644 --- a/src/services/common/Codebreaker.Data.Cosmos/Codebreaker.Data.Cosmos.csproj +++ b/src/services/common/Codebreaker.Data.Cosmos/Codebreaker.Data.Cosmos.csproj @@ -18,8 +18,12 @@ - - + + + + + + diff --git a/src/services/common/Codebreaker.Data.SqlServer/Codebreaker.Data.SqlServer.csproj b/src/services/common/Codebreaker.Data.SqlServer/Codebreaker.Data.SqlServer.csproj index d9bb3333..26b3b3bd 100644 --- a/src/services/common/Codebreaker.Data.SqlServer/Codebreaker.Data.SqlServer.csproj +++ b/src/services/common/Codebreaker.Data.SqlServer/Codebreaker.Data.SqlServer.csproj @@ -19,8 +19,12 @@ - - + + + + + + diff --git a/src/services/common/Codebreaker.GameAPIs.Analyzers.Tests/Analyzers/ColorGame5x3AnalyzerTests.cs b/src/services/common/Codebreaker.GameAPIs.Analyzers.Tests/Analyzers/ColorGame5x3AnalyzerTests.cs new file mode 100644 index 00000000..41a463de --- /dev/null +++ b/src/services/common/Codebreaker.GameAPIs.Analyzers.Tests/Analyzers/ColorGame5x3AnalyzerTests.cs @@ -0,0 +1,168 @@ +using System.Collections; + +using static Codebreaker.GameAPIs.Models.Colors; + +namespace Codebreaker.GameAPIs.Analyzer.Tests; + +public class ColorGame5x3AnalyzerTests +{ + [Fact] + public void SetMove_ShouldReturnTwoWhite() + { + ColorResult expectedKeyPegs = new(0, 2); + ColorResult? resultKeyPegs = AnalyzeGame( + [Red, Green, Blue], + [Green, Blue, Yellow] + ); + + Assert.Equal(expectedKeyPegs, resultKeyPegs); + } + + [InlineData(2, 0, Red, Green, Red)] + [InlineData(3, 0, Red, Green, Blue)] + [Theory] + public void SetMove_UsingVariousData(int expectedBlack, int expectedWhite, params string[] guessValues) + { + string[] code = [Red, Green, Blue]; + ColorResult expectedKeyPegs = new(expectedBlack, expectedWhite); + ColorResult resultKeyPegs = AnalyzeGame(code, guessValues); + Assert.Equal(expectedKeyPegs, resultKeyPegs); + } + + [Theory] + [ClassData(typeof(TestData5x3))] + public void SetMove_UsingVariousDataUsingDataClass(string[] code, string[] guess, ColorResult expectedKeyPegs) + { + ColorResult actualKeyPegs = AnalyzeGame(code, guess); + Assert.Equal(expectedKeyPegs, actualKeyPegs); + } + + [Fact] + public void SetMove_ShouldThrowOnInvalidGuessCount() + { + Assert.Throws(() => + AnalyzeGame( + [Red, Green, Blue], + [Red] + )); + } + + [Fact] + public void SetMove_ShouldThrowOnInvalidGuessValues() + { + Assert.Throws(() => + AnalyzeGame( + [Red, Green, Blue], + [Red, "Invalid", Blue] + )); + } + + [Fact] + public void SetMove_ShouldThrowOnInvalidMoveNumber() + { + Assert.Throws(() => + AnalyzeGame( + [Red, Green, Blue], + [Green, Red, Blue], moveNumber: 2)); + } + + [Fact] + public void SetMove_ShouldNotIncrementMoveNumberOnInvalidMove() + { + IGame game = AnalyzeGameCatchingException( + [Red, Green, Blue], + [Green, Red, Blue], moveNumber: 2); + + Assert.Equal(0, game?.LastMoveNumber); + } + + [Fact] + public void SetMove_ShouldIncludeExpectedMoveNumberInErrorMessage() + { + var exception = Assert.Throws(() => + AnalyzeGame( + [Red, Green, Blue], + [Green, Red, Blue], moveNumber: 3)); + + Assert.Contains("received 3", exception.Message); + Assert.Contains("expected 1", exception.Message); + } + + private static MockColorGame CreateGame(string[] codes) => + new() + { + GameType = GameTypes.Game5x3, + NumberCodes = 3, + MaxMoves = 10, + IsVictory = false, + FieldValues = new Dictionary>() + { + [FieldCategories.Colors] = [.. TestData5x3.Colors5] + }, + Codes = codes + }; + + private static ColorResult AnalyzeGame(string[] codes, string[] guesses, int moveNumber = 1) + { + MockColorGame game = CreateGame(codes); + ColorGameGuessAnalyzer analyzer = new(game, [.. guesses.ToPegs()], moveNumber); + return analyzer.GetResult(); + } + + private static IGame AnalyzeGameCatchingException(string[] codes, string[] guesses, int moveNumber = 1) + { + MockColorGame game = CreateGame(codes); + + ColorGameGuessAnalyzer analyzer = new(game, guesses.ToPegs().ToArray(), moveNumber); + try + { + analyzer.GetResult(); + } + catch (ArgumentException) + { + + } + return game; + } +} + +public class TestData5x3 : IEnumerable +{ + public static readonly string[] Colors5 = [Red, Green, Blue, Yellow, Purple]; + + public IEnumerator GetEnumerator() + { + yield return new object[] + { + new string[] { Red, Green, Blue }, // code + new string[] { Red, Red, Yellow }, // inputdata + new ColorResult(1, 0) // expected + }; + yield return new object[] + { + new string[] { Red, Green, Blue }, + new string[] { Green, Blue, Red }, + new ColorResult(0, 3) + }; + yield return new object[] + { + new string[] { Yellow, Purple, Green }, + new string[] { Purple, Purple, Purple }, + new ColorResult(1, 0) + }; + yield return new object[] + { + new string[] { Red, Green, Blue }, + new string[] { Red, Green, Blue }, + new ColorResult(3, 0) + }; + yield return new object[] + { + new string[] { Yellow, Blue, Red }, + new string[] { Blue, Yellow, Yellow }, + new ColorResult(0, 2) + }; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/services/common/Codebreaker.GameAPIs.Analyzers/Models/GameTypes.cs b/src/services/common/Codebreaker.GameAPIs.Analyzers/Models/GameTypes.cs index 14aa2b8b..40f5eb68 100644 --- a/src/services/common/Codebreaker.GameAPIs.Analyzers/Models/GameTypes.cs +++ b/src/services/common/Codebreaker.GameAPIs.Analyzers/Models/GameTypes.cs @@ -6,4 +6,5 @@ public class GameTypes public const string Game6x4Mini = nameof(Game6x4Mini); public const string Game8x5 = nameof(Game8x5); public const string Game5x5x4 = nameof(Game5x5x4); + public const string Game5x3 = nameof(Game5x3); } diff --git a/src/services/common/Codebreaker.GameAPIs.Models/Codebreaker.GameAPIs.Models.csproj b/src/services/common/Codebreaker.GameAPIs.Models/Codebreaker.GameAPIs.Models.csproj index da974fe7..fa535ca8 100644 --- a/src/services/common/Codebreaker.GameAPIs.Models/Codebreaker.GameAPIs.Models.csproj +++ b/src/services/common/Codebreaker.GameAPIs.Models/Codebreaker.GameAPIs.Models.csproj @@ -19,7 +19,11 @@ - + + + + + diff --git a/src/services/gameapis/Codebreaker.GameAPIs.Tests/GamesFactoryTests.cs b/src/services/gameapis/Codebreaker.GameAPIs.Tests/GamesFactoryTests.cs index 927271a9..cc215916 100644 --- a/src/services/gameapis/Codebreaker.GameAPIs.Tests/GamesFactoryTests.cs +++ b/src/services/gameapis/Codebreaker.GameAPIs.Tests/GamesFactoryTests.cs @@ -37,6 +37,17 @@ public void Create5x5x4GameShouldCreate5x5x4Game() Assert.Equal(5, shapes); } + [Fact] + public void Create5x3GameShouldCreate5x3Game() + { + Game game = GamesFactory.CreateGame("Game5x3", "Test"); + + Assert.Equal("Test", game.PlayerName); + Assert.Equal(3, game.Codes.Length); + int colors = game.FieldValues["colors"].Count(); + Assert.Equal(5, colors); + } + [Fact] public void Apply6x4MoveShouldAddAMoveToTheGame() { @@ -72,4 +83,15 @@ public void Apply5x5x4MoveShouldAddAMoveToTheGame() Assert.Single(game.Moves); } + + [Fact] + public void Apply5x3MoveShouldAddAMoveToTheGame() + { + Game game = GamesFactory.CreateGame("Game5x3", "Test"); + var values = game.FieldValues["colors"]; + string[] guesses = Enumerable.Repeat(values.First(), 3).ToArray(); + game.ApplyMove(guesses, 1); + + Assert.Single(game.Moves); + } } diff --git a/src/services/gameapis/Codebreaker.GameAPIs/Codebreaker.GameAPIs.csproj b/src/services/gameapis/Codebreaker.GameAPIs/Codebreaker.GameAPIs.csproj index ef80dce8..338f16ed 100644 --- a/src/services/gameapis/Codebreaker.GameAPIs/Codebreaker.GameAPIs.csproj +++ b/src/services/gameapis/Codebreaker.GameAPIs/Codebreaker.GameAPIs.csproj @@ -25,7 +25,7 @@ - + @@ -35,11 +35,14 @@ - - + + + + + diff --git a/src/services/gameapis/Codebreaker.GameAPIs/Services/GamesFactory.cs b/src/services/gameapis/Codebreaker.GameAPIs/Services/GamesFactory.cs index ffae1305..e463af33 100644 --- a/src/services/gameapis/Codebreaker.GameAPIs/Services/GamesFactory.cs +++ b/src/services/gameapis/Codebreaker.GameAPIs/Services/GamesFactory.cs @@ -63,6 +63,16 @@ Game Create5x5x4Game() => .Select(item => string.Join(';', item.Shape, item.Color)) .ToArray() }; + + Game Create5x3Game() => + new(Guid.NewGuid(), gameType, playerName, DateTime.UtcNow, 3, 10) + { + FieldValues = new Dictionary>() + { + { FieldCategories.Colors, s_colors5 } + }, + Codes = Random.Shared.GetItems(s_colors5, 3) + }; return gameType switch { @@ -70,6 +80,7 @@ Game Create5x5x4Game() => GameTypes.Game6x4 => Create6x4Game(), GameTypes.Game8x5 => Create8x5Game(), GameTypes.Game5x5x4 => Create5x5x4Game(), + GameTypes.Game5x3 => Create5x3Game(), _ => throw new CodebreakerException("Invalid game type") { Code = CodebreakerExceptionCodes.InvalidGameType } }; } @@ -113,6 +124,7 @@ string[] GetShapeGameGuessAnalyzerResult() GameTypes.Game8x5 => GetColorGameGuessAnalyzerResult(), GameTypes.Game6x4Mini => GetSimpleGameGuessAnalyzerResult(), GameTypes.Game5x5x4 => GetShapeGameGuessAnalyzerResult(), + GameTypes.Game5x3 => GetColorGameGuessAnalyzerResult(), _ => throw new CodebreakerException("Invalid game type") { Code = CodebreakerExceptionCodes.InvalidGameType } };