Skip to content

Commit 4b81d18

Browse files
Enhance activity logging and add game reveal functionality
Updated `ActivityExtensions.cs` to replace `ActivityTagsCollection` with an array of `KeyValuePair<string, object?>` for event tags. Introduced a new `GameRevealedEvent` method for logging game reveal events. Added `RevealGameAsync` method in `GamesClient.cs` to cancel a game and retrieve its details, with improved error handling and logging. Updated event logging for game moves to include tags. Expanded `IGamesClient.cs` with XML documentation for the new method and marked `CancelGameAsync` as obsolete. Enhanced logging capabilities in `LogExtensions.cs` with a new method for revealing game errors. Updated `readme.md` to reflect new functionality and provide usage examples for `RevealGameAsync`.
1 parent 1f785cb commit 4b81d18

5 files changed

Lines changed: 166 additions & 11 deletions

File tree

src/clients/Codebreaker.GameAPIs.Client/ActivityExtensions.cs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,39 @@ internal static class ActivityExtensions
44
{
55
public static void ErrorEvent(this Activity? activity, string message)
66
{
7-
activity?.AddEvent(new ActivityEvent("Error", tags: new ActivityTagsCollection()
8-
{
7+
activity?.AddEvent(new ActivityEvent("Error", tags:
8+
[
99
new KeyValuePair<string, object?>("otel.status_Code", "Error"),
1010
new KeyValuePair<string, object?>("otel.status_description", message)
11-
}));
11+
]));
1212
}
1313

1414
public static void GameCreatedEvent(this Activity? activity, string gameId, string gameType)
1515
{
1616
activity?.SetBaggage("gameId", gameId);
1717
activity?.AddEvent(
1818
new ActivityEvent("GameCreated",
19-
tags: new ActivityTagsCollection()
20-
{
19+
tags:
20+
[
2121
new KeyValuePair<string, object?>("gameType", gameType)
22-
}));
22+
]));
2323
}
2424

2525
public static void GameCanceledEvent(this Activity? activity, string gameId)
2626
{
2727
activity?.SetBaggage("gameId", gameId);
2828
activity?.AddEvent(new ActivityEvent("GameCanceled"));
2929
}
30+
31+
public static void GameRevealedEvent(this Activity? activity, string gameId, string gameType)
32+
{
33+
activity?.SetBaggage("gameId", gameId);
34+
activity?.AddEvent(
35+
new ActivityEvent("GameRevealed",
36+
tags:
37+
[
38+
new KeyValuePair<string, object?>("gameId", gameId),
39+
new KeyValuePair<string, object?>("gameType", gameType)
40+
]));
41+
}
3042
}

src/clients/Codebreaker.GameAPIs.Client/GamesClient.cs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ public class GamesClient(HttpClient httpClient, ILogger<GamesClient> logger) : I
4646
}
4747
}
4848

49-
5049
/// <summary>
5150
/// Cancels a game.
5251
/// </summary>
@@ -74,6 +73,44 @@ public async Task CancelGameAsync(Guid id, string playerName, GameType gameType,
7473
}
7574
}
7675

76+
/// <summary>
77+
/// Cancels and reveals the details of a game based on the specified parameters.
78+
/// </summary>
79+
/// <param name="id">The unique identifier of the game to reveal.</param>
80+
/// <param name="playerName">The name of the player requesting the game details. Cannot be null or empty.</param>
81+
/// <param name="gameType">The type of the game to reveal.</param>
82+
/// <param name="cancellationToken">A token to monitor for cancellation requests. Optional.</param>
83+
/// <returns>A <see cref="GameInfo"/> object containing the details of the revealed game.</returns>
84+
/// <exception cref="HttpRequestException"></exception>
85+
/// <exception cref="InvalidOperationException"></exception>
86+
public async Task<GameInfo> RevealGameAsync(Guid id, string playerName, GameType gameType, CancellationToken cancellationToken = default)
87+
{
88+
using Activity? activity = ActivitySource.StartActivity("RevealGameAsync", ActivityKind.Client);
89+
try
90+
{
91+
// First cancel/end the game
92+
var request = new UpdateGameRequest(id, gameType, playerName, 0, true);
93+
var cancelResponse = await httpClient.PatchAsJsonAsync($"/games/{id}", request, s_jsonOptions, cancellationToken);
94+
cancelResponse.EnsureSuccessStatusCode();
95+
96+
// Then get the full game details
97+
var gameResponse = await httpClient.GetFromJsonAsync<GameInfo>($"/games/{id}", s_jsonOptions, cancellationToken)
98+
?? throw new InvalidOperationException($"Could not retrieve game with ID {id}");
99+
100+
int moveCount = gameResponse.Moves?.Count ?? 0;
101+
logger.GameRevealed(id, moveCount);
102+
activity?.GameRevealedEvent(id.ToString(), gameType.ToString());
103+
104+
return gameResponse;
105+
}
106+
catch (HttpRequestException ex)
107+
{
108+
logger.RevealGameError(ex.Message, ex);
109+
activity?.ErrorEvent(ex.Message);
110+
throw;
111+
}
112+
}
113+
77114
/// <summary>
78115
/// Set a game move by supplying guess pegs. This method returns the results of the move (the key pegs), and whether the game ended, and whether the game was won.
79116
/// </summary>
@@ -101,7 +138,11 @@ public async Task CancelGameAsync(Guid id, string playerName, GameType gameType,
101138
?? throw new InvalidOperationException();
102139

103140
logger.MoveSet(id, moveResponse.MoveNumber);
104-
activity?.AddEvent(new ActivityEvent("GameCreated"));
141+
activity?.AddEvent(new ActivityEvent("MoveSet",
142+
tags: [
143+
new KeyValuePair<string, object?>("gameId", id.ToString()),
144+
new KeyValuePair<string, object?>("moveNumber", moveResponse.MoveNumber)
145+
]));
105146

106147
(_, _, _, bool ended, bool isVictory, string[] results) = moveResponse;
107148
return (results, ended, isVictory);
@@ -157,8 +198,17 @@ public async Task<IEnumerable<GameInfo>> GetGamesAsync(GamesQuery query, Cancell
157198
try
158199
{
159200
string urlQuery = query.AsUrlQuery();
160-
IEnumerable<GameInfo> games = (await httpClient.GetFromJsonAsync<IEnumerable<GameInfo>>($"/games/{urlQuery}", s_jsonOptions, cancellationToken)) ?? Enumerable.Empty<GameInfo>();
161-
logger.GamesReceived(urlQuery, games.Count());
201+
IEnumerable<GameInfo> games = (await httpClient.GetFromJsonAsync<IEnumerable<GameInfo>>($"/games{urlQuery}", s_jsonOptions, cancellationToken)) ?? [];
202+
203+
int gameCount = games.Count();
204+
logger.GamesReceived(urlQuery, gameCount);
205+
206+
activity?.AddEvent(new ActivityEvent("GamesReceived",
207+
tags: [
208+
new KeyValuePair<string, object?>("count", gameCount),
209+
new KeyValuePair<string, object?>("query", urlQuery)
210+
]));
211+
162212
return games;
163213
}
164214
catch (HttpRequestException ex)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,89 @@
11
namespace Codebreaker.GameAPIs.Client;
22

3+
/// <summary>
4+
/// Defines methods for interacting with games, including retrieving game information, managing game state, and
5+
/// performing player actions.
6+
/// </summary>
7+
/// <remarks>This interface provides functionality for querying game details, starting new games, making moves,
8+
/// and canceling games. It is designed to support various game types and player interactions.</remarks>
39
public interface IGamesClient
410
{
11+
/// <summary>
12+
/// Retrieves information about a game by its unique identifier.
13+
/// </summary>
14+
/// <remarks>Use this method to retrieve detailed information about a specific game. If the game does not
15+
/// exist, the method returns <see langword="null"/>. Ensure that the <paramref name="id"/> parameter is valid
16+
/// before calling this method.</remarks>
17+
/// <param name="id">The unique identifier of the game to retrieve.</param>
18+
/// <param name="cancellationToken">A token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
19+
/// <returns>A <see cref="Task{TResult}"/> representing the asynchronous operation. The task result contains a <see
20+
/// cref="GameInfo"/> object with details about the game if found; otherwise, <see langword="null"/>.</returns>
521
Task<GameInfo?> GetGameAsync(Guid id, CancellationToken cancellationToken = default);
22+
23+
/// <summary>
24+
/// Asynchronously retrieves a collection of games based on the specified query criteria.
25+
/// </summary>
26+
/// <remarks>Use this method to retrieve game information based on specific filters, such as genre,
27+
/// release date, or platform. The query object defines the criteria for filtering the games.</remarks>
28+
/// <param name="query">The query parameters used to filter and retrieve the games. Cannot be null.</param>
29+
/// <param name="cancellationToken">A token to monitor for cancellation requests. The operation will be canceled if the token is triggered.</param>
30+
/// <returns>A task that represents the asynchronous operation. The task result contains an enumerable collection of <see
31+
/// cref="GameInfo"/> objects that match the query criteria. If no games match, the collection will be empty.</returns>
632
Task<IEnumerable<GameInfo>> GetGamesAsync(GamesQuery query, CancellationToken cancellationToken = default);
33+
34+
/// <summary>
35+
/// Processes a player's move in the specified game and returns the results of the move.
36+
/// </summary>
37+
/// <param name="id">The unique identifier of the game session.</param>
38+
/// <param name="playerName">The name of the player making the move.</param>
39+
/// <param name="gameType">The type of game being played.</param>
40+
/// <param name="moveNumber">The sequential number of the move being made.</param>
41+
/// <param name="guessPegs">An array of strings representing the player's guess for the current move.</param>
42+
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
43+
/// <returns>A task that represents the asynchronous operation. The task result is a tuple containing: <list type="bullet">
44+
/// <item><description>An array of strings representing the results of the move.</description></item>
45+
/// <item><description>A boolean indicating whether the game has ended.</description></item> <item><description>A
46+
/// boolean indicating whether the move resulted in a victory.</description></item> </list></returns>
747
Task<(string[] Results, bool Ended, bool IsVictory)> SetMoveAsync(Guid id, string playerName, GameType gameType, int moveNumber, string[] guessPegs, CancellationToken cancellationToken = default);
48+
49+
/// <summary>
50+
/// Starts a new game asynchronously and returns the game details.
51+
/// </summary>
52+
/// <remarks>Use this method to initialize a new game session. The returned game details include the
53+
/// configuration and constraints for the game, which can be used to guide gameplay.</remarks>
54+
/// <param name="gameType">The type of game to start. This determines the rules and configuration of the game.</param>
55+
/// <param name="playerName">The name of the player starting the game. Cannot be null or empty.</param>
56+
/// <param name="cancellationToken">A token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
57+
/// <returns>A task that represents the asynchronous operation. The task result is a tuple containing: <list type="bullet">
58+
/// <item><description><see cref="Guid"/> Id: The unique identifier for the newly created game.</description></item>
59+
/// <item><description><see cref="int"/> NumberCodes: The number of codes required to solve the
60+
/// game.</description></item> <item><description><see cref="int"/> MaxMoves: The maximum number of moves allowed in
61+
/// the game.</description></item> <item><description><see cref="IDictionary{TKey, TValue}"/> FieldValues: A
62+
/// dictionary containing field names as keys and their possible values as arrays of strings.</description></item>
63+
/// </list></returns>
864
Task<(Guid Id, int NumberCodes, int MaxMoves, IDictionary<string, string[]> FieldValues)> StartGameAsync(GameType gameType, string playerName, CancellationToken cancellationToken = default);
65+
66+
/// <summary>
67+
/// Cancels an ongoing game session.
68+
/// </summary>
69+
/// <remarks>This method cancels the specified game session and may notify other players or systems
70+
/// depending on the game type. Ensure the <paramref name="id"/> corresponds to an active game session and that the
71+
/// caller has the necessary permissions to cancel the game.</remarks>
72+
/// <param name="id">The unique identifier of the game session to cancel.</param>
73+
/// <param name="playerName">The name of the player requesting the cancellation. Must not be null or empty.</param>
74+
/// <param name="gameType">The type of game being canceled.</param>
75+
/// <param name="cancellationToken">A token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
76+
/// <returns>A task that represents the asynchronous operation.</returns>
77+
[Obsolete(message: "Use RevealGameAsync instead", error: false)]
978
Task CancelGameAsync(Guid id, string playerName, GameType gameType, CancellationToken cancellationToken = default);
79+
80+
/// <summary>
81+
/// Cancels and reveals the details of a game based on the specified parameters.
82+
/// </summary>
83+
/// <param name="id">The unique identifier of the game to reveal.</param>
84+
/// <param name="playerName">The name of the player requesting the game details. Cannot be null or empty.</param>
85+
/// <param name="gameType">The type of the game to reveal.</param>
86+
/// <param name="cancellationToken">A token to monitor for cancellation requests. Optional.</param>
87+
/// <returns>A <see cref="GameInfo"/> object containing the details of the revealed game.</returns>
88+
Task<GameInfo> RevealGameAsync(Guid id, string playerName, GameType gameType, CancellationToken cancellationToken = default);
1089
}

src/clients/Codebreaker.GameAPIs.Client/LogExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ internal static partial class LogExtensions
1919

2020
[LoggerMessage(5005, LogLevel.Error, "CancelGameAsync error {ErrorMessage}", EventName = "CancelGameError")]
2121
public static partial void CancelGameError(this ILogger logger, string errorMessage, Exception ex);
22+
23+
[LoggerMessage(5006, LogLevel.Error, "RevealGameAsync error {ErrorMessage}", EventName = "RevealGameError")]
24+
public static partial void RevealGameError(this ILogger logger, string errorMessage, Exception ex);
2225

2326
[LoggerMessage(8001, LogLevel.Information, "Game {GameId} created", EventName = "GameCreated")]
2427
public static partial void GameCreated(this ILogger logger,Guid gameId);
@@ -34,4 +37,7 @@ internal static partial class LogExtensions
3437

3538
[LoggerMessage(8005, LogLevel.Information, "Game {GameId} canceled", EventName = "GameCanceled")]
3639
public static partial void GameCanceled(this ILogger logger, Guid gameId);
40+
41+
[LoggerMessage(8006, LogLevel.Information, "Game {GameId} revealed with {MoveCount} moves", EventName = "GameRevealed")]
42+
public static partial void GameRevealed(this ILogger logger, Guid gameId, int moveCount);
3743
}

src/clients/Codebreaker.GameAPIs.Client/docs/readme.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The `IGamesClient` class is the main contract to be used for communication to pl
1616
| `SetMoveAsync` | Set guesses for a game move |
1717
| `GetGameAsync` | Get a game by id with all details and moves |
1818
| `GetGamesAsync` | Get a list of games with all details and moves (use the `GamesQuery` class to define the filter) |
19-
19+
| `RevealGameAsync` | Ends a game and returns the correct answer |
2020

2121
The `GamesClient` class implements the `IGamesClient` interface. In the constructor, inject the `HttpClient` class. You can use `Microsoft.Extensions.Http` to configure the `HttpClient` class.
2222

@@ -50,8 +50,16 @@ Start a game:
5050
(Guid id, int numberCodes, int maxMoves, IDictionary<string, string[]> fieldValues) = await gamesClient.StartGameAsync("Game6x4", "player1");
5151
```
5252

53+
The returned fieldValues contains an array of possible values for code fields, with the key being "colors". With a Game5x5x4 game, the returned fieldValues contains a key "shapes", and a key "colors".
54+
5355
Set a move:
5456

5557
```csharp
5658
(string[] result, bool ended, bool isVictory) = await gameClient.SetMoveAsync(id, "Game6x4", [ "Red", "Green", "Blue", "Yellow" ]);
5759
```
60+
61+
With a Game5x5x4 game, you can set a move like this:
62+
63+
```csharp
64+
(string[] result, bool ended, bool isVictory) = await gameClient.SetMoveAsync(id, "Game5x5x4", [ "Circle;Red", "Rectangle;Green", "Triangle;Blue", "Circle;Yellow" ]);
65+
```

0 commit comments

Comments
 (0)