Skip to content

Commit c049af8

Browse files
Complete CodeBreaker.BotWithString implementation with game runner, comprehensive tests, and documentation
Co-authored-by: christiannagel <1908285+christiannagel@users.noreply.github.com>
1 parent 3ff55be commit c049af8

6 files changed

Lines changed: 375 additions & 8 deletions

File tree

src/services/bot/CodeBreaker.BotWithString.Tests/CodeBreaker.BotWithString.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
</PropertyGroup>
88
<ItemGroup>
99
<PackageReference Include="Microsoft.NET.Test.Sdk" />
10+
<PackageReference Include="Moq" />
1011
<PackageReference Include="xunit" />
1112
<PackageReference Include="xunit.runner.visualstudio">
1213
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
using Codebreaker.GameAPIs.Client;
2+
using Codebreaker.GameAPIs.Client.Models;
3+
using Moq;
4+
5+
namespace CodeBreaker.BotWithString.Tests;
6+
7+
public class StringBotGameRunnerTests
8+
{
9+
[Fact]
10+
public async Task PlayGameAsync_Should_StartGameAndMakeMove()
11+
{
12+
// Arrange
13+
var mockClient = new Mock<IGamesClient>();
14+
var gameId = Guid.NewGuid();
15+
var fieldValues = new Dictionary<string, string[]>
16+
{
17+
["Colors"] = new[] { "Red", "Blue", "Green", "Yellow" }
18+
};
19+
20+
mockClient.Setup(x => x.StartGameAsync(GameType.Game6x4, "TestPlayer", It.IsAny<CancellationToken>()))
21+
.ReturnsAsync((gameId, 4, 12, fieldValues));
22+
23+
mockClient.Setup(x => x.SetMoveAsync(gameId, "TestPlayer", GameType.Game6x4, 1,
24+
It.IsAny<string[]>(), It.IsAny<CancellationToken>()))
25+
.ReturnsAsync((new[] { "4", "0" }, true, true)); // 4 black hits = win
26+
27+
var runner = new StringBotGameRunner(mockClient.Object);
28+
29+
// Act
30+
var result = await runner.PlayGameAsync(GameType.Game6x4, "TestPlayer");
31+
32+
// Assert
33+
Assert.Equal(gameId, result.GameId);
34+
Assert.Equal(GameType.Game6x4, result.GameType);
35+
Assert.Equal("TestPlayer", result.PlayerName);
36+
Assert.True(result.GameWon);
37+
Assert.True(result.GameEnded);
38+
Assert.Equal(1, result.MovesUsed);
39+
Assert.NotNull(result.WinningCombination);
40+
Assert.Equal(4, result.WinningCombination.Length);
41+
}
42+
43+
[Fact]
44+
public async Task PlayGameAsync_Should_HandleMultipleMoves()
45+
{
46+
// Arrange
47+
var mockClient = new Mock<IGamesClient>();
48+
var gameId = Guid.NewGuid();
49+
var fieldValues = new Dictionary<string, string[]>
50+
{
51+
["Colors"] = new[] { "Red", "Blue" }
52+
};
53+
54+
mockClient.Setup(x => x.StartGameAsync(GameType.Game6x4, "TestPlayer", It.IsAny<CancellationToken>()))
55+
.ReturnsAsync((gameId, 4, 12, fieldValues));
56+
57+
// First move: no matches
58+
mockClient.Setup(x => x.SetMoveAsync(gameId, "TestPlayer", GameType.Game6x4, 1,
59+
It.IsAny<string[]>(), It.IsAny<CancellationToken>()))
60+
.ReturnsAsync((new[] { "0", "0" }, false, false));
61+
62+
// Second move: win
63+
mockClient.Setup(x => x.SetMoveAsync(gameId, "TestPlayer", GameType.Game6x4, 2,
64+
It.IsAny<string[]>(), It.IsAny<CancellationToken>()))
65+
.ReturnsAsync((new[] { "4", "0" }, true, true));
66+
67+
var runner = new StringBotGameRunner(mockClient.Object);
68+
69+
// Act
70+
var result = await runner.PlayGameAsync(GameType.Game6x4, "TestPlayer");
71+
72+
// Assert
73+
Assert.Equal(gameId, result.GameId);
74+
Assert.True(result.GameWon);
75+
Assert.True(result.GameEnded);
76+
Assert.Equal(2, result.MovesUsed);
77+
78+
// Verify both moves were called
79+
mockClient.Verify(x => x.SetMoveAsync(gameId, "TestPlayer", GameType.Game6x4, 1,
80+
It.IsAny<string[]>(), It.IsAny<CancellationToken>()), Times.Once);
81+
mockClient.Verify(x => x.SetMoveAsync(gameId, "TestPlayer", GameType.Game6x4, 2,
82+
It.IsAny<string[]>(), It.IsAny<CancellationToken>()), Times.Once);
83+
}
84+
85+
[Fact]
86+
public async Task PlayGameAsync_Should_HandleGameLoss()
87+
{
88+
// Arrange
89+
var mockClient = new Mock<IGamesClient>();
90+
var gameId = Guid.NewGuid();
91+
var fieldValues = new Dictionary<string, string[]>
92+
{
93+
["Colors"] = new[] { "Red", "Blue" }
94+
};
95+
96+
mockClient.Setup(x => x.StartGameAsync(GameType.Game6x4, "TestPlayer", It.IsAny<CancellationToken>()))
97+
.ReturnsAsync((gameId, 4, 2, fieldValues)); // Only 2 max moves
98+
99+
// Both moves: no matches, game ends after max moves
100+
mockClient.Setup(x => x.SetMoveAsync(gameId, "TestPlayer", GameType.Game6x4, It.IsAny<int>(),
101+
It.IsAny<string[]>(), It.IsAny<CancellationToken>()))
102+
.ReturnsAsync((new[] { "0", "0" }, false, false));
103+
104+
var runner = new StringBotGameRunner(mockClient.Object);
105+
106+
// Act
107+
var result = await runner.PlayGameAsync(GameType.Game6x4, "TestPlayer");
108+
109+
// Assert
110+
Assert.Equal(gameId, result.GameId);
111+
Assert.False(result.GameWon);
112+
Assert.False(result.GameEnded); // Game didn't officially end, just reached max moves
113+
Assert.Equal(2, result.MovesUsed);
114+
Assert.Null(result.WinningCombination);
115+
}
116+
117+
[Fact]
118+
public async Task PlayGameAsync_Should_HandleGame8x5()
119+
{
120+
// Arrange
121+
var mockClient = new Mock<IGamesClient>();
122+
var gameId = Guid.NewGuid();
123+
var fieldValues = new Dictionary<string, string[]>
124+
{
125+
["Colors"] = new[] { "Red", "Blue", "Green", "Yellow", "Orange" }
126+
};
127+
128+
mockClient.Setup(x => x.StartGameAsync(GameType.Game8x5, "TestPlayer", It.IsAny<CancellationToken>()))
129+
.ReturnsAsync((gameId, 5, 14, fieldValues));
130+
131+
mockClient.Setup(x => x.SetMoveAsync(gameId, "TestPlayer", GameType.Game8x5, 1,
132+
It.IsAny<string[]>(), It.IsAny<CancellationToken>()))
133+
.ReturnsAsync((new[] { "5", "0" }, true, true)); // 5 black hits = win
134+
135+
var runner = new StringBotGameRunner(mockClient.Object);
136+
137+
// Act
138+
var result = await runner.PlayGameAsync(GameType.Game8x5, "TestPlayer");
139+
140+
// Assert
141+
Assert.Equal(GameType.Game8x5, result.GameType);
142+
Assert.True(result.GameWon);
143+
Assert.NotNull(result.WinningCombination);
144+
Assert.Equal(5, result.WinningCombination.Length);
145+
}
146+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# CodeBreaker.BotWithString
2+
3+
A string-based implementation of the Codebreaker bot that works with the Games API using string arrays instead of binary data.
4+
5+
## Overview
6+
7+
This project provides a string-based alternative to the original `CodeBreaker.Bot` project. Instead of using binary data and bit manipulation, this implementation works directly with string arrays representing colors, making it easier to understand and integrate with string-based APIs.
8+
9+
## Key Features
10+
11+
- **String-based algorithms**: All core algorithms work with `string[]` arrays instead of `int` binary representations
12+
- **GameAPIs Client compatibility**: Works seamlessly with the `IGamesClient` interface
13+
- **Support for all game types**: Game6x4, Game8x5, and Game5x5x4
14+
- **Comprehensive algorithm set**:
15+
- `HandleBlackMatches`: Filters by exact position and color matches
16+
- `HandleWhiteMatches`: Filters by correct color, wrong position matches
17+
- `HandleBlueMatches`: Handles partial matches for Game5x5x4
18+
- `HandleNoMatches`: Removes combinations containing selection colors
19+
- `SelectPeg`: Gets specific peg from string array
20+
- `GenerateAllPossibleCombinations`: Creates all possible game combinations
21+
22+
## Core Components
23+
24+
### StringCodeBreakerAlgorithms
25+
26+
The main class containing all string-based algorithms:
27+
28+
```csharp
29+
// Example usage
30+
var possibleCombinations = StringCodeBreakerAlgorithms.GenerateAllPossibleCombinations(
31+
GameType.Game6x4,
32+
new[] { "Red", "Blue", "Green", "Yellow" });
33+
34+
// Filter based on black matches (exact position and color)
35+
var filtered = possibleCombinations.HandleBlackMatches(
36+
GameType.Game6x4,
37+
2, // number of black hits
38+
new[] { "Red", "Blue", "Green", "Yellow" }); // the guess
39+
```
40+
41+
### StringBotGameRunner
42+
43+
A demonstration class showing how to use the string-based algorithms with the GameAPIs client:
44+
45+
```csharp
46+
var runner = new StringBotGameRunner(gamesClient);
47+
var result = await runner.PlayGameAsync(GameType.Game6x4, "PlayerName");
48+
```
49+
50+
## Key Differences from Binary Version
51+
52+
| Aspect | Binary Version | String Version |
53+
|--------|----------------|----------------|
54+
| Data representation | `int` with bit manipulation | `string[]` arrays |
55+
| Color handling | Bit masks and shifts | Direct string comparison |
56+
| Algorithm complexity | Bit operations | Simple array operations |
57+
| API compatibility | Requires conversion | Direct compatibility |
58+
| Readability | Lower (bit manipulation) | Higher (string operations) |
59+
60+
## Testing
61+
62+
The project includes comprehensive unit tests covering:
63+
64+
- All algorithm methods for different game types
65+
- Edge cases and error conditions
66+
- Game runner functionality with mocked clients
67+
- Parameter validation
68+
69+
Run tests with:
70+
```bash
71+
dotnet test CodeBreaker.BotWithString.Tests.csproj
72+
```
73+
74+
## Usage in Benchmarks
75+
76+
This string-based implementation is designed to be integrated into bot benchmark projects to compare performance and behavior with the binary version. The simplified string operations may have different performance characteristics compared to bit manipulation, making it valuable for performance analysis.
77+
78+
## API Compatibility
79+
80+
The string-based algorithms are fully compatible with the `IGamesClient` interface, which uses string-based methods:
81+
82+
- `StartGameAsync` returns field values as `string[]`
83+
- `SetMoveAsync` accepts guesses as `string[]`
84+
- Results are returned as `string[]`
85+
86+
This makes integration straightforward without requiring data conversion layers.
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
using Codebreaker.GameAPIs.Client;
2+
using Codebreaker.GameAPIs.Client.Models;
3+
4+
namespace CodeBreaker.BotWithString;
5+
6+
/// <summary>
7+
/// Demonstrates how to use the string-based algorithms with the GameAPIs client
8+
/// </summary>
9+
public class StringBotGameRunner
10+
{
11+
private readonly IGamesClient _gamesClient;
12+
13+
public StringBotGameRunner(IGamesClient gamesClient)
14+
{
15+
_gamesClient = gamesClient;
16+
}
17+
18+
/// <summary>
19+
/// Plays a simple game using string-based algorithms
20+
/// </summary>
21+
/// <param name="gameType">The type of game to play</param>
22+
/// <param name="playerName">The name of the player</param>
23+
/// <param name="cancellationToken">Cancellation token</param>
24+
/// <returns>Game result information</returns>
25+
public async Task<GameResult> PlayGameAsync(GameType gameType, string playerName, CancellationToken cancellationToken = default)
26+
{
27+
// Start a new game
28+
var (gameId, numberCodes, maxMoves, fieldValues) = await _gamesClient.StartGameAsync(gameType, playerName, cancellationToken);
29+
30+
// Get available colors/values for this game type
31+
string[] availableValues = fieldValues.ContainsKey("Colors")
32+
? fieldValues["Colors"].ToArray()
33+
: fieldValues.Values.First().ToArray();
34+
35+
// Generate all possible combinations for this game type
36+
List<string[]> possibleCombinations = StringCodeBreakerAlgorithms.GenerateAllPossibleCombinations(gameType, availableValues);
37+
38+
int moveNumber = 1;
39+
bool gameWon = false;
40+
bool gameEnded = false;
41+
string[]? winningCombination = null;
42+
int actualMovesUsed = 0;
43+
44+
while (!gameEnded && moveNumber <= maxMoves && possibleCombinations.Count > 0)
45+
{
46+
// For this simple implementation, just pick the first possible combination
47+
string[] guess = possibleCombinations[0];
48+
49+
// Make the move
50+
var (results, ended, isVictory) = await _gamesClient.SetMoveAsync(
51+
gameId, playerName, gameType, moveNumber, guess, cancellationToken);
52+
53+
actualMovesUsed = moveNumber; // Track actual moves used
54+
gameEnded = ended;
55+
gameWon = isVictory;
56+
57+
if (isVictory)
58+
{
59+
winningCombination = guess;
60+
break;
61+
}
62+
63+
// Parse results and filter possible combinations
64+
if (results.Length >= 2)
65+
{
66+
int blackHits = 0;
67+
int whiteHits = 0;
68+
int blueHits = 0;
69+
70+
// Assuming results format: [black_hits, white_hits, ...]
71+
if (int.TryParse(results[0], out blackHits))
72+
{
73+
possibleCombinations = possibleCombinations.HandleBlackMatches(gameType, blackHits, guess);
74+
}
75+
76+
if (results.Length > 1 && int.TryParse(results[1], out whiteHits))
77+
{
78+
possibleCombinations = possibleCombinations.HandleWhiteMatches(gameType, whiteHits, guess);
79+
}
80+
81+
if (results.Length > 2 && int.TryParse(results[2], out blueHits))
82+
{
83+
possibleCombinations = possibleCombinations.HandleBlueMatches(gameType, blueHits, guess);
84+
}
85+
86+
// If no matches at all
87+
if (blackHits == 0 && whiteHits == 0 && (results.Length <= 2 || blueHits == 0))
88+
{
89+
possibleCombinations = possibleCombinations.HandleNoMatches(gameType, guess);
90+
}
91+
}
92+
93+
moveNumber++;
94+
}
95+
96+
return new GameResult
97+
{
98+
GameId = gameId,
99+
GameType = gameType,
100+
PlayerName = playerName,
101+
MovesUsed = actualMovesUsed,
102+
MaxMoves = maxMoves,
103+
GameWon = gameWon,
104+
GameEnded = gameEnded,
105+
WinningCombination = winningCombination,
106+
RemainingPossibilities = possibleCombinations.Count
107+
};
108+
}
109+
}
110+
111+
/// <summary>
112+
/// Result of a game played by the string-based bot
113+
/// </summary>
114+
public record GameResult
115+
{
116+
public required Guid GameId { get; init; }
117+
public required GameType GameType { get; init; }
118+
public required string PlayerName { get; init; }
119+
public required int MovesUsed { get; init; }
120+
public required int MaxMoves { get; init; }
121+
public required bool GameWon { get; init; }
122+
public required bool GameEnded { get; init; }
123+
public required string[]? WinningCombination { get; init; }
124+
public required int RemainingPossibilities { get; init; }
125+
}

0 commit comments

Comments
 (0)