Create db and db access. Add json converter for board array. Add tests for json conversion
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
|
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\JoshHeaps.Net\JoshHeaps.Net.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="NUnit.Framework" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using JoshHeaps.Net.Models;
|
||||||
|
using JoshHeaps.Net.Services.Implementations;
|
||||||
|
using JoshHeaps.Net.Utilities;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace JoshHeaps.Net.Tests.JsonTests;
|
||||||
|
|
||||||
|
internal class JsonConversionTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void ConvertBoard_WhenGivenStartingBoard_CanConvertToAndFromJson()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var gameState = new ChessService().CreateNewGame();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var serializedGameState = JsonSerializer.Serialize(gameState);
|
||||||
|
var deserializedGameState = JsonSerializer.Deserialize<GameState>(serializedGameState);
|
||||||
|
|
||||||
|
Assert.That(deserializedGameState, Is.Not.Null);
|
||||||
|
|
||||||
|
var originBoard = gameState.Board;
|
||||||
|
var convertedBoard = deserializedGameState!.Board;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
AssertBoardEquality(originBoard, convertedBoard);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ConvertBoard_WhenGivenValidBoard_CanConvertToAndFromJson()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
ChessPiece?[,] originBoard = new ChessPiece?[8,8];
|
||||||
|
|
||||||
|
originBoard[0, 0] = new ChessPiece(
|
||||||
|
"rook1",
|
||||||
|
PieceType.Rook,
|
||||||
|
PieceColor.White,
|
||||||
|
new Position(0, 0)
|
||||||
|
);
|
||||||
|
originBoard[4,4] = new ChessPiece(
|
||||||
|
"pawn1",
|
||||||
|
PieceType.Pawn,
|
||||||
|
PieceColor.Black,
|
||||||
|
new Position(4, 4)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var serializedBoard = JsonSerializer.Serialize(originBoard, _options);
|
||||||
|
var convertedBoard = JsonSerializer.Deserialize<ChessPiece?[,]>(serializedBoard, _options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
AssertBoardEquality(originBoard, convertedBoard!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertBoardEquality(ChessPiece?[,]? original, ChessPiece?[,]? converted)
|
||||||
|
{
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(original, Is.Not.Null);
|
||||||
|
Assert.That(converted, Is.Not.Null);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(original!.LongLength, Is.EqualTo(converted!.LongLength));
|
||||||
|
|
||||||
|
for (int i = 0; i < original.GetLength(0); i++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < original.GetLength(1); j++)
|
||||||
|
{
|
||||||
|
var originalPiece = original[i, j];
|
||||||
|
var convertedPiece = converted[i, j];
|
||||||
|
AssertPieceEquality(originalPiece, convertedPiece);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertPieceEquality(ChessPiece? original, ChessPiece? converted)
|
||||||
|
{
|
||||||
|
if (original is null)
|
||||||
|
{
|
||||||
|
Assert.That(converted, Is.Null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(converted, Is.Not.Null);
|
||||||
|
Assert.That(original!.Id, Is.EqualTo(converted!.Id));
|
||||||
|
Assert.That(original.Type, Is.EqualTo(converted.Type));
|
||||||
|
Assert.That(original.Color, Is.EqualTo(converted.Color));
|
||||||
|
Assert.That(original.Position.Row, Is.EqualTo(converted.Position.Row));
|
||||||
|
Assert.That(original.Position.Col, Is.EqualTo(converted.Position.Col));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions _options = new()
|
||||||
|
{
|
||||||
|
Converters = { new ChessBoardConverter() }
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JoshHeaps.Net", "JoshHeaps.
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{82206AD3-19DF-4DDA-9647-02B691B78CE8}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{82206AD3-19DF-4DDA-9647-02B691B78CE8}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JoshHeaps.Net.Tests", "JoshHeaps.Net.Tests\JoshHeaps.Net.Tests.csproj", "{769155E1-1686-402C-9618-272D20DC3F96}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -21,6 +23,10 @@ Global
|
|||||||
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Release|Any CPU.Build.0 = Release|Any CPU
|
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{769155E1-1686-402C-9618-272D20DC3F96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{769155E1-1686-402C-9618-272D20DC3F96}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{769155E1-1686-402C-9618-272D20DC3F96}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{769155E1-1686-402C-9618-272D20DC3F96}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ namespace JoshHeaps.Net.Controllers;
|
|||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class ChessController(IChessService chessService, IHubContext<ChessHub> chessHub, IBackgroundTaskQueue queue) : ControllerBase
|
public class ChessController(
|
||||||
|
IChessService chessService,
|
||||||
|
IHubContext<ChessHub> chessHub,
|
||||||
|
IBackgroundTaskQueue queue) : ControllerBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Store of ongoing games.
|
/// Store of ongoing games.
|
||||||
@@ -31,6 +34,7 @@ public class ChessController(IChessService chessService, IHubContext<ChessHub> c
|
|||||||
gameState.IsVsComputer = true;
|
gameState.IsVsComputer = true;
|
||||||
gameState.WhiteJoined = true;
|
gameState.WhiteJoined = true;
|
||||||
gameState.BlackJoined = true;
|
gameState.BlackJoined = true;
|
||||||
|
gameState.ComputerDifficulty = difficulty;
|
||||||
Guid playerId = Guid.NewGuid();
|
Guid playerId = Guid.NewGuid();
|
||||||
Guid computerId = Guid.NewGuid();
|
Guid computerId = Guid.NewGuid();
|
||||||
var isWhite = Random.Shared.Next(2) == 0;
|
var isWhite = Random.Shared.Next(2) == 0;
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using JoshHeaps.Net.Models;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace JoshHeaps.Net.DAL;
|
||||||
|
|
||||||
|
public class ChessDbAccess(ChessDbContext db)
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions opts =
|
||||||
|
new(JsonSerializerDefaults.Web) { WriteIndented = false };
|
||||||
|
|
||||||
|
public async Task SaveAsync(GameState state, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(state, opts);
|
||||||
|
var entity = await db.Games.FindAsync([state.GameId], ct)
|
||||||
|
?? new GameStateEntity { GameId = state.GameId };
|
||||||
|
|
||||||
|
entity.SerializedState = json;
|
||||||
|
entity.LastMoveUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
db.Update(entity);
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GameState?> LoadAsync(Guid id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var e = await db.Games.FindAsync([id], ct);
|
||||||
|
return e is null
|
||||||
|
? null
|
||||||
|
: JsonSerializer.Deserialize<GameState>(e.SerializedState, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(Guid id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (await db.Games.FindAsync([id], ct) is { } e)
|
||||||
|
{
|
||||||
|
db.Remove(e);
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using JoshHeaps.Net.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace JoshHeaps.Net.DAL;
|
||||||
|
|
||||||
|
public class ChessDbContext : DbContext
|
||||||
|
{
|
||||||
|
public ChessDbContext(DbContextOptions<ChessDbContext> opts) : base(opts) { }
|
||||||
|
|
||||||
|
public DbSet<GameStateEntity> Games => Set<GameStateEntity>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder b)
|
||||||
|
{
|
||||||
|
// Primary key
|
||||||
|
b.Entity<GameStateEntity>().HasKey(g => g.GameId);
|
||||||
|
|
||||||
|
// Board + pieces serialized as JSON blobs
|
||||||
|
b.Entity<GameStateEntity>()
|
||||||
|
.Property(g => g.SerializedState)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,12 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="System.Text.Json" Version="9.0.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="9.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -17,6 +22,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Folder Include="Databases\" />
|
||||||
<Folder Include="Resources\" />
|
<Folder Include="Resources\" />
|
||||||
<Folder Include="wwwroot\css\debug\" />
|
<Folder Include="wwwroot\css\debug\" />
|
||||||
<Folder Include="wwwroot\images\Chess Images\" />
|
<Folder Include="wwwroot\images\Chess Images\" />
|
||||||
@@ -24,7 +30,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="8.0.7" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="9.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
using JoshHeaps.Net.Services.Implementations;
|
using JoshHeaps.Net.Services.Implementations;
|
||||||
|
using JoshHeaps.Net.Utilities;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace JoshHeaps.Net.Models;
|
namespace JoshHeaps.Net.Models;
|
||||||
|
|
||||||
@@ -8,6 +12,7 @@ public class GameState
|
|||||||
|
|
||||||
// 8x8 board of references. Null if no piece is present.
|
// 8x8 board of references. Null if no piece is present.
|
||||||
// row 0 at top -> row 7 at bottom (typical 0-based array).
|
// row 0 at top -> row 7 at bottom (typical 0-based array).
|
||||||
|
[JsonConverter(typeof(ChessBoardConverter))]
|
||||||
public ChessPiece?[,] Board { get; set; }
|
public ChessPiece?[,] Board { get; set; }
|
||||||
|
|
||||||
// Whose turn is it?
|
// Whose turn is it?
|
||||||
@@ -42,7 +47,10 @@ public class GameState
|
|||||||
public Guid BlackPlayerId { get; set; }
|
public Guid BlackPlayerId { get; set; }
|
||||||
|
|
||||||
public bool IsVsComputer { get; set; } = false;
|
public bool IsVsComputer { get; set; } = false;
|
||||||
|
public int ComputerDifficulty { get; set; } = 20;
|
||||||
|
|
||||||
|
[JsonIgnore] // <-- exclude from JSON
|
||||||
|
[NotMapped] // <-- EF won't try to persist it either
|
||||||
public Stockfish? Computer { get; set; }
|
public Stockfish? Computer { get; set; }
|
||||||
|
|
||||||
// optional: convenience
|
// optional: convenience
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace JoshHeaps.Net.Models;
|
||||||
|
|
||||||
|
public class GameStateEntity
|
||||||
|
{
|
||||||
|
public Guid GameId { get; set; }
|
||||||
|
public string SerializedState { get; set; } = ""; // JSON
|
||||||
|
public DateTime LastMoveUtc { get; set; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
using JoshHeaps.Net.DAL;
|
||||||
using JoshHeaps.Net.Hubs;
|
using JoshHeaps.Net.Hubs;
|
||||||
using JoshHeaps.Net.Services.Implementations;
|
using JoshHeaps.Net.Services.Implementations;
|
||||||
using JoshHeaps.Net.Services.Interfaces;
|
using JoshHeaps.Net.Services.Interfaces;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace JoshHeaps.Net;
|
namespace JoshHeaps.Net;
|
||||||
|
|
||||||
@@ -27,9 +29,17 @@ public class Program
|
|||||||
|
|
||||||
builder.Services.AddSingleton<IChessService, ChessService>();
|
builder.Services.AddSingleton<IChessService, ChessService>();
|
||||||
builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
|
builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
|
||||||
|
var cs = builder.Configuration.GetConnectionString("ChessDatabase");
|
||||||
|
builder.Services.AddDbContext<ChessDbContext>(o => o.UseSqlite(cs));
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<ChessDbContext>();
|
||||||
|
dbContext.Database.Migrate();
|
||||||
|
}
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
using JoshHeaps.Net.Hubs;
|
using JoshHeaps.Net.Hubs;
|
||||||
using JoshHeaps.Net.Models;
|
using JoshHeaps.Net.Models;
|
||||||
using JoshHeaps.Net.Services.Interfaces;
|
using JoshHeaps.Net.Services.Interfaces;
|
||||||
|
using JoshHeaps.Net.Utilities;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
|
|
||||||
namespace JoshHeaps.Net.Services.Implementations;
|
namespace JoshHeaps.Net.Services.Implementations;
|
||||||
@@ -124,130 +123,4 @@ public sealed class Stockfish : IAsyncDisposable
|
|||||||
|
|
||||||
await chessHub.Clients.Group(state.GameId.ToString()).SendAsync("ReceiveMoveUpdate", state.GameId.ToString(), moveDto, result);
|
await chessHub.Clients.Group(state.GameId.ToString()).SendAsync("ReceiveMoveUpdate", state.GameId.ToString(), moveDto, result);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static class StockfishHelpers
|
|
||||||
{
|
|
||||||
public static MoveDto ToMoveDto(
|
|
||||||
this string uci,
|
|
||||||
GameState gameState,
|
|
||||||
Guid playerId)
|
|
||||||
{
|
|
||||||
int fCol = uci[0] - 'a', fRow = 7 - (uci[1] - '1');
|
|
||||||
int tCol = uci[2] - 'a', tRow = 7 - (uci[3] - '1');
|
|
||||||
|
|
||||||
var piece = gameState.Board[fRow, fCol]
|
|
||||||
?? throw new Exception("No piece at source square");
|
|
||||||
|
|
||||||
PieceType? promo = uci.Length == 5 ? uci[4] switch
|
|
||||||
{
|
|
||||||
'q' => PieceType.Queen,
|
|
||||||
'r' => PieceType.Rook,
|
|
||||||
'b' => PieceType.Bishop,
|
|
||||||
'n' => PieceType.Knight,
|
|
||||||
_ => null
|
|
||||||
} : null;
|
|
||||||
|
|
||||||
return new MoveDto
|
|
||||||
{
|
|
||||||
GameId = gameState.GameId,
|
|
||||||
PlayerId = playerId,
|
|
||||||
PieceId = piece.Id,
|
|
||||||
TargetRow = tRow,
|
|
||||||
TargetCol = tCol,
|
|
||||||
PromotionChoice = promo,
|
|
||||||
SourceCol = fCol,
|
|
||||||
SourceRow = fRow,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a 2-D board array (rank 8 = row 0, file a = col 0) to a FEN string.
|
|
||||||
/// Only piece placement + active colour + castling are computed; the rest use
|
|
||||||
/// safe defaults (-, 0, 1). That is all Stockfish needs.
|
|
||||||
/// </summary>
|
|
||||||
public static string ToFen(this GameState gs)
|
|
||||||
{
|
|
||||||
var sb = new StringBuilder(64);
|
|
||||||
|
|
||||||
/* 1) piece placement */
|
|
||||||
for (int row = 0; row < 8; row++)
|
|
||||||
{
|
|
||||||
int empty = 0;
|
|
||||||
|
|
||||||
for (int col = 0; col < 8; col++)
|
|
||||||
{
|
|
||||||
var p = gs.Board[row, col];
|
|
||||||
|
|
||||||
if (p is null)
|
|
||||||
{
|
|
||||||
empty++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (empty > 0) { sb.Append(empty); empty = 0; }
|
|
||||||
sb.Append(ToFenChar(p)); // ← unchanged helper
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty > 0) sb.Append(empty);
|
|
||||||
if (row < 7) sb.Append('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 2) active colour */
|
|
||||||
sb.Append(gs.CurrentPlayer == PieceColor.White ? " w " : " b ");
|
|
||||||
|
|
||||||
/* 3) castling rights (from GameState flags) */
|
|
||||||
sb.Append(GetCastlingFlags(gs));
|
|
||||||
|
|
||||||
/* 4) en-passant target square */
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(gs.EnPassantTarget.HasValue
|
|
||||||
? Alg(gs.EnPassantTarget.Value)
|
|
||||||
: "-");
|
|
||||||
|
|
||||||
/* 5-6) half-move clock + full-move number */
|
|
||||||
int fullMoves = gs.MoveHistory.Count / 2 + 1;
|
|
||||||
sb.Append(" 0 ").Append(fullMoves);
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- helpers ---------- */
|
|
||||||
|
|
||||||
private static string GetCastlingFlags(GameState gs)
|
|
||||||
{
|
|
||||||
var flags = new StringBuilder(4);
|
|
||||||
|
|
||||||
if (gs.WhiteCanCastleKingside) flags.Append('K');
|
|
||||||
if (gs.WhiteCanCastleQueenside) flags.Append('Q');
|
|
||||||
if (gs.BlackCanCastleKingside) flags.Append('k');
|
|
||||||
if (gs.BlackCanCastleQueenside) flags.Append('q');
|
|
||||||
|
|
||||||
return flags.Length == 0 ? "-" : flags.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Alg(Position p)
|
|
||||||
{
|
|
||||||
char file = (char)('a' + p.Col);
|
|
||||||
int rank = 8 - p.Row;
|
|
||||||
return $"{file}{rank}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static char ToFenChar(ChessPiece p) => p switch
|
|
||||||
{
|
|
||||||
{ Type: PieceType.Pawn, Color: PieceColor.White } => 'P',
|
|
||||||
{ Type: PieceType.Pawn, Color: PieceColor.Black } => 'p',
|
|
||||||
{ Type: PieceType.Knight, Color: PieceColor.White } => 'N',
|
|
||||||
{ Type: PieceType.Knight, Color: PieceColor.Black } => 'n',
|
|
||||||
{ Type: PieceType.Bishop, Color: PieceColor.White } => 'B',
|
|
||||||
{ Type: PieceType.Bishop, Color: PieceColor.Black } => 'b',
|
|
||||||
{ Type: PieceType.Rook, Color: PieceColor.White } => 'R',
|
|
||||||
{ Type: PieceType.Rook, Color: PieceColor.Black } => 'r',
|
|
||||||
{ Type: PieceType.Queen, Color: PieceColor.White } => 'Q',
|
|
||||||
{ Type: PieceType.Queen, Color: PieceColor.Black } => 'q',
|
|
||||||
{ Type: PieceType.King, Color: PieceColor.White } => 'K',
|
|
||||||
{ Type: PieceType.King, Color: PieceColor.Black } => 'k',
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(p))
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using JoshHeaps.Net.Models;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace JoshHeaps.Net.Utilities;
|
||||||
|
|
||||||
|
public sealed class ChessBoardConverter
|
||||||
|
: JsonConverter<ChessPiece?[,]>
|
||||||
|
{
|
||||||
|
public override ChessPiece?[,] Read(
|
||||||
|
ref Utf8JsonReader reader,
|
||||||
|
Type typeToConvert,
|
||||||
|
JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
List<ChessPiece?> pieces = [];
|
||||||
|
|
||||||
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
|
throw new JsonException($"Expected start of an array, instead received {reader.TokenType}");
|
||||||
|
|
||||||
|
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
{
|
||||||
|
pieces.Add(null);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TokenType != JsonTokenType.StartObject)
|
||||||
|
throw new JsonException($"Expected start of an object, instead received {reader.TokenType}");
|
||||||
|
|
||||||
|
var piece = JsonSerializer.Deserialize<ChessPiece>(ref reader, options) ?? throw new JsonException("Deserialized piece was null");
|
||||||
|
pieces.Add(piece);
|
||||||
|
|
||||||
|
if (reader.TokenType != JsonTokenType.EndObject)
|
||||||
|
throw new JsonException($"Expected end of an object, instead received {reader.TokenType}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TokenType != JsonTokenType.EndArray)
|
||||||
|
throw new JsonException($"Expected end of an array, instead received {reader.TokenType}");
|
||||||
|
|
||||||
|
var boardSize = Math.Sqrt(pieces.Count);
|
||||||
|
|
||||||
|
if (boardSize % 1 != 0)
|
||||||
|
throw new JsonException($"Expected a square number of pieces, instead received {pieces.Count}");
|
||||||
|
|
||||||
|
ChessPiece?[,] board = new ChessPiece?[(int)boardSize, (int)boardSize];
|
||||||
|
|
||||||
|
foreach (var piece in pieces)
|
||||||
|
{
|
||||||
|
if (piece is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
board[piece.Position.Row, piece.Position.Col] = piece;
|
||||||
|
}
|
||||||
|
|
||||||
|
return board;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(
|
||||||
|
Utf8JsonWriter writer,
|
||||||
|
ChessPiece?[,] value,
|
||||||
|
JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStartArray();
|
||||||
|
|
||||||
|
foreach (var piece in value)
|
||||||
|
{
|
||||||
|
if (piece is null)
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonSerializer.Serialize(writer, piece);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteEndArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
using JoshHeaps.Net.Models;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace JoshHeaps.Net.Utilities;
|
||||||
|
|
||||||
|
public static class StockfishHelpers
|
||||||
|
{
|
||||||
|
public static MoveDto ToMoveDto(
|
||||||
|
this string uci,
|
||||||
|
GameState gameState,
|
||||||
|
Guid playerId)
|
||||||
|
{
|
||||||
|
int fCol = uci[0] - 'a', fRow = 7 - (uci[1] - '1');
|
||||||
|
int tCol = uci[2] - 'a', tRow = 7 - (uci[3] - '1');
|
||||||
|
|
||||||
|
var piece = gameState.Board[fRow, fCol]
|
||||||
|
?? throw new Exception("No piece at source square");
|
||||||
|
|
||||||
|
PieceType? promo = uci.Length == 5 ? uci[4] switch
|
||||||
|
{
|
||||||
|
'q' => PieceType.Queen,
|
||||||
|
'r' => PieceType.Rook,
|
||||||
|
'b' => PieceType.Bishop,
|
||||||
|
'n' => PieceType.Knight,
|
||||||
|
_ => null
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
return new MoveDto
|
||||||
|
{
|
||||||
|
GameId = gameState.GameId,
|
||||||
|
PlayerId = playerId,
|
||||||
|
PieceId = piece.Id,
|
||||||
|
TargetRow = tRow,
|
||||||
|
TargetCol = tCol,
|
||||||
|
PromotionChoice = promo,
|
||||||
|
SourceCol = fCol,
|
||||||
|
SourceRow = fRow,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a 2-D board array (rank 8 = row 0, file a = col 0) to a FEN string.
|
||||||
|
/// Only piece placement + active colour + castling are computed; the rest use
|
||||||
|
/// safe defaults (-, 0, 1). That is all Stockfish needs.
|
||||||
|
/// </summary>
|
||||||
|
public static string ToFen(this GameState gs)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(64);
|
||||||
|
|
||||||
|
/* 1) piece placement */
|
||||||
|
for (int row = 0; row < 8; row++)
|
||||||
|
{
|
||||||
|
int empty = 0;
|
||||||
|
|
||||||
|
for (int col = 0; col < 8; col++)
|
||||||
|
{
|
||||||
|
var p = gs.Board[row, col];
|
||||||
|
|
||||||
|
if (p is null)
|
||||||
|
{
|
||||||
|
empty++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (empty > 0) { sb.Append(empty); empty = 0; }
|
||||||
|
sb.Append(ToFenChar(p)); // ← unchanged helper
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty > 0) sb.Append(empty);
|
||||||
|
if (row < 7) sb.Append('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2) active colour */
|
||||||
|
sb.Append(gs.CurrentPlayer == PieceColor.White ? " w " : " b ");
|
||||||
|
|
||||||
|
/* 3) castling rights (from GameState flags) */
|
||||||
|
sb.Append(GetCastlingFlags(gs));
|
||||||
|
|
||||||
|
/* 4) en-passant target square */
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(gs.EnPassantTarget.HasValue
|
||||||
|
? Alg(gs.EnPassantTarget.Value)
|
||||||
|
: "-");
|
||||||
|
|
||||||
|
/* 5-6) half-move clock + full-move number */
|
||||||
|
int fullMoves = gs.MoveHistory.Count / 2 + 1;
|
||||||
|
sb.Append(" 0 ").Append(fullMoves);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- helpers ---------- */
|
||||||
|
|
||||||
|
private static string GetCastlingFlags(GameState gs)
|
||||||
|
{
|
||||||
|
var flags = new StringBuilder(4);
|
||||||
|
|
||||||
|
if (gs.WhiteCanCastleKingside) flags.Append('K');
|
||||||
|
if (gs.WhiteCanCastleQueenside) flags.Append('Q');
|
||||||
|
if (gs.BlackCanCastleKingside) flags.Append('k');
|
||||||
|
if (gs.BlackCanCastleQueenside) flags.Append('q');
|
||||||
|
|
||||||
|
return flags.Length == 0 ? "-" : flags.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Alg(Position p)
|
||||||
|
{
|
||||||
|
char file = (char)('a' + p.Col);
|
||||||
|
int rank = 8 - p.Row;
|
||||||
|
return $"{file}{rank}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static char ToFenChar(ChessPiece p) => p switch
|
||||||
|
{
|
||||||
|
{ Type: PieceType.Pawn, Color: PieceColor.White } => 'P',
|
||||||
|
{ Type: PieceType.Pawn, Color: PieceColor.Black } => 'p',
|
||||||
|
{ Type: PieceType.Knight, Color: PieceColor.White } => 'N',
|
||||||
|
{ Type: PieceType.Knight, Color: PieceColor.Black } => 'n',
|
||||||
|
{ Type: PieceType.Bishop, Color: PieceColor.White } => 'B',
|
||||||
|
{ Type: PieceType.Bishop, Color: PieceColor.Black } => 'b',
|
||||||
|
{ Type: PieceType.Rook, Color: PieceColor.White } => 'R',
|
||||||
|
{ Type: PieceType.Rook, Color: PieceColor.Black } => 'r',
|
||||||
|
{ Type: PieceType.Queen, Color: PieceColor.White } => 'Q',
|
||||||
|
{ Type: PieceType.Queen, Color: PieceColor.Black } => 'q',
|
||||||
|
{ Type: PieceType.King, Color: PieceColor.White } => 'K',
|
||||||
|
{ Type: PieceType.King, Color: PieceColor.Black } => 'k',
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(p))
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,5 +5,8 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*",
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"ChessDatabase": "Data Source=Databases/chess.db"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,14 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
|
||||||
|
<PackageReference Include="NUnit" Version="4.3.2" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="9.0.6" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\JoshHeaps.Net\JoshHeaps.Net.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user