Skip to content

Commit 7a12819

Browse files
games api with .net 6
1 parent 0598e9b commit 7a12819

22 files changed

+683
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<RootNamespace>Codebreaker</RootNamespace>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
12+
</ItemGroup>
13+
14+
</Project>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.6.33620.401
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GamesApiDotnet6", "GamesApiDotnet6.csproj", "{644E4DF2-9AFA-4365-AB88-BC3727FB58EA}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{644E4DF2-9AFA-4365-AB88-BC3727FB58EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{644E4DF2-9AFA-4365-AB88-BC3727FB58EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{644E4DF2-9AFA-4365-AB88-BC3727FB58EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{644E4DF2-9AFA-4365-AB88-BC3727FB58EA}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ExtensibilityGlobals) = postSolution
23+
SolutionGuid = {00DA19A3-CA22-48A5-960B-AD0F19DB37AB}
24+
EndGlobalSection
25+
EndGlobal
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
namespace Codebreaker.Models;
2+
3+
public enum GameType
4+
{
5+
Game6x4,
6+
Game6x4Simple,
7+
Game8x5,
8+
Game5x5x4
9+
}
10+
11+
public record CreateGameRequest(GameType GameType, string PlayerName);
12+
13+
public record InvalidGameRequest(string Message, string[] Information);
14+
15+
public record CreateGameResponse(Guid GameId, GameType GameType, string PlayerName, int Holes, int MaxMoves);
16+
17+
// depending on the game type, set ColorFields or ShapeAndColorFields
18+
public record SetMoveRequest(Guid GameId, GameType GameType, int MoveNumber)
19+
{
20+
public ICollection<ColorField>? ColorFields { get; set; }
21+
public ICollection<ShapeAndColorField>? ShapeAndColorFields { get; set; }
22+
}
23+
24+
public record SetMoveResponse(
25+
Guid GameId,
26+
GameType GameType,
27+
int MoveNumber,
28+
ColorResult? ColorResult = default,
29+
ShapeAndColorResult? ShapeResult = default,
30+
SimpleColorResult? SimpleResult = default);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace Codebreaker.Models;
4+
5+
public partial record ColorField(string Color);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace Codebreaker.Models;
4+
5+
public record ShapeAndColorField(string Shape, string Color)
6+
{
7+
public override string ToString() => $"{Shape};{Color}";
8+
9+
public static ShapeAndColorField Parse(string s, IFormatProvider? provider = default)
10+
{
11+
if (TryParse(s, provider, out ShapeAndColorField? shape))
12+
{
13+
return shape;
14+
}
15+
else
16+
{
17+
throw new ArgumentException($"Cannot parse value {s}", nameof(s));
18+
}
19+
}
20+
21+
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out ShapeAndColorField result)
22+
{
23+
result = null;
24+
if (s is null)
25+
{
26+
return false;
27+
}
28+
string[] parts = s.Split(';');
29+
if (parts.Length != 2)
30+
{
31+
return false;
32+
}
33+
result = new ShapeAndColorField(parts[0], parts[1]);
34+
return true;
35+
}
36+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
namespace Codebreaker.Models;
2+
3+
public abstract record class Game(Guid GameId, GameType GameType, string PlayerName, int Holes, int MaxMoves);
4+
5+
public record class Game<TField, TResult>(Guid GameId, GameType GameType, string PlayerName, int Holes, int MaxMoves)
6+
: Game(GameId, GameType, PlayerName, Holes, MaxMoves)
7+
{
8+
internal Game(Guid GameId, GameType GameType, string PlayerName, int Holes, int MaxMoves, TField[] codes)
9+
: this(GameId, GameType, PlayerName, Holes, MaxMoves)
10+
{
11+
Codes = codes;
12+
}
13+
14+
public ICollection<TField> Codes { get; } = new TField[Holes];
15+
internal readonly List<Move<TField, TResult>> _moves = new();
16+
public IEnumerable<Move<TField, TResult>> Moves => _moves;
17+
public int LastMove => _moves.Count;
18+
19+
internal void AddMove(Move<TField, TResult> move)
20+
{
21+
_moves.Add(move);
22+
}
23+
}
24+
25+
public abstract record class Move(Guid GameId, Guid MoveId, int MoveNumber);
26+
27+
public record class Move<TField, TResult>(Guid GameId, Guid MoveId, int MoveNumber)
28+
: Move(GameId, MoveId, MoveNumber)
29+
{
30+
public Move(Guid GameId, Guid MoveId, int MoveNumber, ICollection<TField> fields, TResult? results)
31+
: this(GameId, MoveId, MoveNumber)
32+
{
33+
Fields = fields;
34+
Results = results;
35+
}
36+
37+
public ICollection<TField>? Fields { get; private set; }
38+
public TResult? Results { get; init; }
39+
}
40+
41+
public static class GameExtensions
42+
{
43+
// calculate result - this is just a sample implementation returning dummy values
44+
public static ColorResult GetResult(this Game<ColorField, ColorResult> game, IEnumerable<ColorField> fields)
45+
{
46+
return game.GameType switch
47+
{
48+
GameType.Game6x4 => new ColorResult(1, 1),
49+
GameType.Game8x5 => new ColorResult(1, 2),
50+
_ => throw new InvalidOperationException()
51+
};
52+
}
53+
54+
public static ShapeAndColorResult GetResult(this Game<ShapeAndColorField, ShapeAndColorResult> game, IEnumerable<ShapeAndColorField> fields)
55+
{
56+
return new ShapeAndColorResult(2, 1, 0);
57+
}
58+
59+
public static SimpleColorResult GetResult(this Game<ColorField, SimpleColorResult> game, IEnumerable<ColorField> fields)
60+
{
61+
ResultInformation[] results = { ResultInformation.CorrectColor, ResultInformation.Incorrect, ResultInformation.Incorrect, ResultInformation.CorrectPositionAndColor };
62+
return new SimpleColorResult(results);
63+
}
64+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace Codebreaker.Models;
2+
3+
public readonly partial record struct ColorResult(
4+
byte Correct,
5+
byte WrongPosition);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace Codebreaker.Models;
2+
3+
public readonly partial record struct ShapeAndColorResult(byte Correct, byte WrongPosition, byte ColorOrShape);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace Codebreaker.Models;
4+
5+
public enum ResultInformation
6+
{
7+
Incorrect,
8+
CorrectPositionAndColor,
9+
CorrectColor
10+
}
11+
12+
public readonly record struct SimpleColorResult(ResultInformation[] Results);
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System.Text.Json.Serialization;
2+
3+
using Codebreaker.Utilities;
4+
5+
using Microsoft.AspNetCore.Http.Json;
6+
7+
var builder = WebApplication.CreateBuilder(args);
8+
9+
builder.Services.AddEndpointsApiExplorer();
10+
builder.Services.AddSwaggerGen();
11+
12+
builder.Services.Configure<JsonOptions>(options =>
13+
{
14+
options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault;
15+
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
16+
});
17+
18+
builder.Services.AddSingleton<IGamesRepository, InMemoryGamesRepository>();
19+
builder.Services.AddSingleton<GamesFactory>();
20+
builder.Services.AddTransient<IGamesService, GamesService>();
21+
22+
var app = builder.Build();
23+
24+
// Configure the HTTP request pipeline.
25+
if (app.Environment.IsDevelopment())
26+
{
27+
app.UseSwagger();
28+
app.UseSwaggerUI();
29+
}
30+
31+
app.UseHttpsRedirection();
32+
33+
app.MapGet("/games", async (IGamesService gamesService) =>
34+
{
35+
IEnumerable<Game> games = await gamesService.GetGamesAsync();
36+
return Results.Ok(games);
37+
})
38+
.WithName("GetGames")
39+
.Produces<IEnumerable<Game>>(StatusCodes.Status200OK)
40+
.WithTags("Info");
41+
42+
// Get game by id
43+
app.MapGet("/games/{gameId:guid}", async (Guid gameId, IGamesService gameService) =>
44+
{
45+
Game? game = await gameService.GetGameAsync(gameId);
46+
47+
if (game is null)
48+
return Results.NotFound();
49+
50+
return Results.Ok(game);
51+
})
52+
.WithName("GetGame")
53+
.Produces<Game>(StatusCodes.Status200OK)
54+
.Produces(StatusCodes.Status404NotFound)
55+
.WithTags("Info");
56+
57+
// Start a game - create a game object
58+
app.MapPost("/games", async (CreateGameRequest request, IGamesService gamesService) =>
59+
{
60+
Game? game = null;
61+
try
62+
{
63+
game = await gamesService.CreateGameAsync(request.GameType, request.PlayerName);
64+
}
65+
catch (GameException ex) when (ex.HResult == 4000)
66+
{
67+
app.Logger.LogError("Game Type not found {gametype}", request.GameType);
68+
69+
return Results.BadRequest();
70+
}
71+
72+
CreateGameResponse createGameResponse = new(game.GameId, game.GameType, game.PlayerName, game.Holes, game.MaxMoves);
73+
return Results.Created($"/{game.GameId}", createGameResponse);
74+
})
75+
.WithName("CreateGame")
76+
.Produces<CreateGameResponse>(StatusCodes.Status201Created)
77+
.Produces(StatusCodes.Status400BadRequest)
78+
.WithTags("Play");
79+
80+
// Create a move for a game
81+
app.MapPost("/games/{gameId:guid}/moves", async (Guid gameId, SetMoveRequest request, IGamesService gamesService) =>
82+
{
83+
if (gameId != request.GameId)
84+
{
85+
return Results.BadRequest();
86+
}
87+
88+
try
89+
{
90+
SetMoveResponse response = await gamesService.SetMoveAsync(request);
91+
return Results.Ok(response);
92+
}
93+
catch (GameException ex) when (ex.HResult is > 4200 and < 4300)
94+
{
95+
return Results.BadRequest();
96+
}
97+
catch (GameException ex) when (ex.HResult == 4400)
98+
{
99+
return Results.NotFound();
100+
}
101+
catch (Exception ex)
102+
{
103+
app.Logger.LogError(ex, "Unexpected error");
104+
return Results.StatusCode(StatusCodes.Status500InternalServerError);
105+
}
106+
})
107+
.WithName("SetMove")
108+
.Produces<SetMoveResponse>(StatusCodes.Status200OK)
109+
.Produces(StatusCodes.Status400BadRequest)
110+
.Produces(StatusCodes.Status404NotFound)
111+
.WithTags("Play");
112+
113+
app.Run();

0 commit comments

Comments
 (0)