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 0000000..0de02ec
Binary files /dev/null and b/JoshHeaps.Net/Databases/chess.db differ
diff --git a/JoshHeaps.Net/Databases/chess.db-shm b/JoshHeaps.Net/Databases/chess.db-shm
new file mode 100644
index 0000000..740aa39
Binary files /dev/null and b/JoshHeaps.Net/Databases/chess.db-shm differ
diff --git a/JoshHeaps.Net/Databases/chess.db-wal b/JoshHeaps.Net/Databases/chess.db-wal
new file mode 100644
index 0000000..7640837
Binary files /dev/null and b/JoshHeaps.Net/Databases/chess.db-wal differ
diff --git a/JoshHeaps.Net/JoshHeaps.Net.csproj b/JoshHeaps.Net/JoshHeaps.Net.csproj
index 52e2186..2d5e377 100644
--- a/JoshHeaps.Net/JoshHeaps.Net.csproj
+++ b/JoshHeaps.Net/JoshHeaps.Net.csproj
@@ -7,7 +7,12 @@
-
+
+ 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
+
+
+
+
+
+
+
+
+
+