From 7a532e52e13e760764174f95d6e27e197fcf838d Mon Sep 17 00:00:00 2001 From: jheaps Date: Fri, 4 Jul 2025 13:41:48 -0600 Subject: [PATCH] Create db and db access. Add json converter for board array. Add tests for json conversion --- .../JoshHeaps.Net.Tests.csproj | 28 ++++ .../JsonTests/JsonConversionTests.cs | 100 ++++++++++++++ JoshHeaps.Net.sln | 6 + JoshHeaps.Net/Controllers/ChessController.cs | 6 +- JoshHeaps.Net/DAL/ChessDbAccess.cs | 40 ++++++ JoshHeaps.Net/DAL/ChessDbContext.cs | 23 ++++ JoshHeaps.Net/Databases/chess.db | Bin 0 -> 4096 bytes JoshHeaps.Net/Databases/chess.db-shm | Bin 0 -> 32768 bytes JoshHeaps.Net/Databases/chess.db-wal | Bin 0 -> 28872 bytes JoshHeaps.Net/JoshHeaps.Net.csproj | 10 +- JoshHeaps.Net/Models/GameState.cs | 8 ++ JoshHeaps.Net/Models/GameStateEntity.cs | 8 ++ JoshHeaps.Net/Program.cs | 10 ++ .../Services/Implementations/Stockfish.cs | 129 +---------------- .../Utilities/ChessBoardConverter.cs | 80 +++++++++++ JoshHeaps.Net/Utilities/StockfishHelpers.cs | 130 ++++++++++++++++++ JoshHeaps.Net/appsettings.json | 5 +- Tests/Tests.csproj | 10 ++ 18 files changed, 461 insertions(+), 132 deletions(-) create mode 100644 JoshHeaps.Net.Tests/JoshHeaps.Net.Tests.csproj create mode 100644 JoshHeaps.Net.Tests/JsonTests/JsonConversionTests.cs create mode 100644 JoshHeaps.Net/DAL/ChessDbAccess.cs create mode 100644 JoshHeaps.Net/DAL/ChessDbContext.cs create mode 100644 JoshHeaps.Net/Databases/chess.db create mode 100644 JoshHeaps.Net/Databases/chess.db-shm create mode 100644 JoshHeaps.Net/Databases/chess.db-wal create mode 100644 JoshHeaps.Net/Models/GameStateEntity.cs create mode 100644 JoshHeaps.Net/Utilities/ChessBoardConverter.cs create mode 100644 JoshHeaps.Net/Utilities/StockfishHelpers.cs diff --git a/JoshHeaps.Net.Tests/JoshHeaps.Net.Tests.csproj b/JoshHeaps.Net.Tests/JoshHeaps.Net.Tests.csproj new file mode 100644 index 0000000..18e5875 --- /dev/null +++ b/JoshHeaps.Net.Tests/JoshHeaps.Net.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/JoshHeaps.Net.Tests/JsonTests/JsonConversionTests.cs b/JoshHeaps.Net.Tests/JsonTests/JsonConversionTests.cs new file mode 100644 index 0000000..dde03e0 --- /dev/null +++ b/JoshHeaps.Net.Tests/JsonTests/JsonConversionTests.cs @@ -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(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(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() } + }; +} diff --git a/JoshHeaps.Net.sln b/JoshHeaps.Net.sln index 1d468c4..05f0a75 100644 --- a/JoshHeaps.Net.sln +++ b/JoshHeaps.Net.sln @@ -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 diff --git a/JoshHeaps.Net/Controllers/ChessController.cs b/JoshHeaps.Net/Controllers/ChessController.cs index b19b3a0..e582ea2 100644 --- a/JoshHeaps.Net/Controllers/ChessController.cs +++ b/JoshHeaps.Net/Controllers/ChessController.cs @@ -9,7 +9,10 @@ namespace JoshHeaps.Net.Controllers; [ApiController] [Route("api/[controller]")] -public class ChessController(IChessService chessService, IHubContext chessHub, IBackgroundTaskQueue queue) : ControllerBase +public class ChessController( + IChessService chessService, + IHubContext chessHub, + IBackgroundTaskQueue queue) : ControllerBase { /// /// Store of ongoing games. @@ -31,6 +34,7 @@ public class ChessController(IChessService chessService, IHubContext 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; diff --git a/JoshHeaps.Net/DAL/ChessDbAccess.cs b/JoshHeaps.Net/DAL/ChessDbAccess.cs new file mode 100644 index 0000000..828bcd9 --- /dev/null +++ b/JoshHeaps.Net/DAL/ChessDbAccess.cs @@ -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 LoadAsync(Guid id, CancellationToken ct = default) + { + var e = await db.Games.FindAsync([id], ct); + return e is null + ? null + : JsonSerializer.Deserialize(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); + } + } +} diff --git a/JoshHeaps.Net/DAL/ChessDbContext.cs b/JoshHeaps.Net/DAL/ChessDbContext.cs new file mode 100644 index 0000000..49fad1a --- /dev/null +++ b/JoshHeaps.Net/DAL/ChessDbContext.cs @@ -0,0 +1,23 @@ +using JoshHeaps.Net.Models; +using Microsoft.EntityFrameworkCore; + +namespace JoshHeaps.Net.DAL; + +public class ChessDbContext : DbContext +{ + public ChessDbContext(DbContextOptions opts) : base(opts) { } + + public DbSet Games => Set(); + + protected override void OnModelCreating(ModelBuilder b) + { + // Primary key + b.Entity().HasKey(g => g.GameId); + + // Board + pieces serialized as JSON blobs + b.Entity() + .Property(g => g.SerializedState) + .HasColumnType("TEXT"); + } +} + diff --git a/JoshHeaps.Net/Databases/chess.db b/JoshHeaps.Net/Databases/chess.db new file mode 100644 index 0000000000000000000000000000000000000000..0de02ecf623141161c863ee065d9f7dd83cbe849 GIT binary patch literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|AVoG{WYDWB;*e#Z~}LT1Gs^;YiR2X4j@8YMDIe1RDzwD?*}g& z$C11D4e*NRGNkllWg$#P&H8+Jtf#ldW?b#d-KpBnujS>qn%#>p&!>N<3}d}osW)4n zcz4K~O6y*%^ro~Y1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk|0WYy%-Hu`1h@-o@ z=-$hq{(%tkHiYg9q8l#?EidX{h@kh(%{CpjOP$}BZJhJ&eP7P|*|~Y&Jo$H0~HN5s8{2G8PVtfoNpZj0I)A@Qw^6s`VdRD>=XD6{ayMhbj)&>}r?jg>`;uzaXoK_{z7(exOxfOW?8_~7+_t!7p{U?v0 zJa53>S13QPJ&&NJOL4m0GLOLRcK713xKB - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + @@ -17,6 +22,7 @@ + @@ -24,7 +30,7 @@ - + diff --git a/JoshHeaps.Net/Models/GameState.cs b/JoshHeaps.Net/Models/GameState.cs index 6e8f468..ca6b6d4 100644 --- a/JoshHeaps.Net/Models/GameState.cs +++ b/JoshHeaps.Net/Models/GameState.cs @@ -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 diff --git a/JoshHeaps.Net/Models/GameStateEntity.cs b/JoshHeaps.Net/Models/GameStateEntity.cs new file mode 100644 index 0000000..266f6f0 --- /dev/null +++ b/JoshHeaps.Net/Models/GameStateEntity.cs @@ -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; } +} diff --git a/JoshHeaps.Net/Program.cs b/JoshHeaps.Net/Program.cs index c430c45..16d31a0 100644 --- a/JoshHeaps.Net/Program.cs +++ b/JoshHeaps.Net/Program.cs @@ -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(); builder.Services.AddSingleton(); + var cs = builder.Configuration.GetConnectionString("ChessDatabase"); + builder.Services.AddDbContext(o => o.UseSqlite(cs)); var app = builder.Build(); + using (var scope = app.Services.CreateScope()) + { + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Database.Migrate(); + } + // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { diff --git a/JoshHeaps.Net/Services/Implementations/Stockfish.cs b/JoshHeaps.Net/Services/Implementations/Stockfish.cs index 0f68109..c50d563 100644 --- a/JoshHeaps.Net/Services/Implementations/Stockfish.cs +++ b/JoshHeaps.Net/Services/Implementations/Stockfish.cs @@ -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, - }; - } - - /// - /// 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. - /// - 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)) - }; } \ No newline at end of file diff --git a/JoshHeaps.Net/Utilities/ChessBoardConverter.cs b/JoshHeaps.Net/Utilities/ChessBoardConverter.cs new file mode 100644 index 0000000..354159d --- /dev/null +++ b/JoshHeaps.Net/Utilities/ChessBoardConverter.cs @@ -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 +{ + public override ChessPiece?[,] Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + List 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(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(); + } +} + diff --git a/JoshHeaps.Net/Utilities/StockfishHelpers.cs b/JoshHeaps.Net/Utilities/StockfishHelpers.cs new file mode 100644 index 0000000..ac1bb8f --- /dev/null +++ b/JoshHeaps.Net/Utilities/StockfishHelpers.cs @@ -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, + }; + } + + /// + /// 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. + /// + 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)) + }; +} diff --git a/JoshHeaps.Net/appsettings.json b/JoshHeaps.Net/appsettings.json index 10f68b8..d87f3fc 100644 --- a/JoshHeaps.Net/appsettings.json +++ b/JoshHeaps.Net/appsettings.json @@ -5,5 +5,8 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ConnectionStrings": { + "ChessDatabase": "Data Source=Databases/chess.db" + } } diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 2150e37..7fc6a17 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -7,4 +7,14 @@ enable + + + + + + + + + +