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
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{82206AD3-19DF-4DDA-9647-02B691B78CE8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JoshHeaps.Net.Tests", "JoshHeaps.Net.Tests\JoshHeaps.Net.Tests.csproj", "{769155E1-1686-402C-9618-272D20DC3F96}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -9,7 +9,10 @@ namespace JoshHeaps.Net.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[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>
|
||||
/// Store of ongoing games.
|
||||
@@ -31,6 +34,7 @@ public class ChessController(IChessService chessService, IHubContext<ChessHub> c
|
||||
gameState.IsVsComputer = true;
|
||||
gameState.WhiteJoined = true;
|
||||
gameState.BlackJoined = true;
|
||||
gameState.ComputerDifficulty = difficulty;
|
||||
Guid playerId = Guid.NewGuid();
|
||||
Guid computerId = Guid.NewGuid();
|
||||
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>
|
||||
|
||||
<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>
|
||||
@@ -17,6 +22,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Databases\" />
|
||||
<Folder Include="Resources\" />
|
||||
<Folder Include="wwwroot\css\debug\" />
|
||||
<Folder Include="wwwroot\images\Chess Images\" />
|
||||
@@ -24,7 +30,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="9.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
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;
|
||||
|
||||
@@ -8,6 +12,7 @@ public class GameState
|
||||
|
||||
// 8x8 board of references. Null if no piece is present.
|
||||
// row 0 at top -> row 7 at bottom (typical 0-based array).
|
||||
[JsonConverter(typeof(ChessBoardConverter))]
|
||||
public ChessPiece?[,] Board { get; set; }
|
||||
|
||||
// Whose turn is it?
|
||||
@@ -42,7 +47,10 @@ public class GameState
|
||||
public Guid BlackPlayerId { get; set; }
|
||||
|
||||
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; }
|
||||
|
||||
// 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.Services.Implementations;
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JoshHeaps.Net;
|
||||
|
||||
@@ -27,9 +29,17 @@ public class Program
|
||||
|
||||
builder.Services.AddSingleton<IChessService, ChessService>();
|
||||
builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
|
||||
var cs = builder.Configuration.GetConnectionString("ChessDatabase");
|
||||
builder.Services.AddDbContext<ChessDbContext>(o => o.UseSqlite(cs));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<ChessDbContext>();
|
||||
dbContext.Database.Migrate();
|
||||
}
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using JoshHeaps.Net.Hubs;
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
using JoshHeaps.Net.Utilities;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Channels;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"ChessDatabase": "Data Source=Databases/chess.db"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,14 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user