Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) [year] [fullname]
Copyright (c) 2026 ChessRealms contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
24 changes: 17 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ search tool.

It does not provide an AI/search engine, UCI, SAN/PGN, Chess960 or tournament
services such as clocks and draw agreements. Dead-position recognition is
deliberately incomplete; see the [game API guide](docs/game-rules-api.md) for
the exact boundary.
deliberately incomplete; see the
[game API guide](https://github.com/ChessRealms/Engine/blob/main/docs/game-rules-api.md)
for the exact boundary.

## Build and test

Expand All @@ -21,10 +22,12 @@ root:
dotnet restore ChessRealms.Engine.slnx --locked-mode
dotnet build ChessRealms.Engine.slnx -c Release --no-restore
dotnet test ChessRealms.Engine.slnx -c Release --no-build --filter "TestCategory!=Deep"
dotnet pack src/ChessRealms.Engine/ChessRealms.Engine.csproj -c Release --no-build
```

The `Deep` test category contains slower perft cases and is opt-in. See
[CONTRIBUTING.md](CONTRIBUTING.md) for contributor checks.
The `Deep` test category contains slower perft cases and is opt-in. See the
[contribution guide](https://github.com/ChessRealms/Engine/blob/main/CONTRIBUTING.md)
for contributor checks.

## Public API

Expand All @@ -34,16 +37,23 @@ using ChessRealms.Engine;

var game = new ChessGame();
var branch = game.Clone();
var result = branch.MakeMove(AlgebraicMove.Parse("e2e4"));
var result = branch.MakeMove(CoordinateMove.Parse("e2e4"));

if (result != MoveResult.None)
{
Console.WriteLine(branch.ToFen());
ChessPiece piece = branch.GetPiece(Square.Parse("e4"));
branch.UndoMove();
}
```

`ChessGame` is mutable: use `Clone()` for an independent branch. Coordinate
promotions require a suffix such as `a7a8q`. The
[game API guide](docs/game-rules-api.md) describes ownership, FEN validation,
draw claims, repetition and supported rule boundaries.
[game API guide](https://github.com/ChessRealms/Engine/blob/main/docs/game-rules-api.md)
describes ownership, FEN validation, draw claims, repetition and supported rule
boundaries.

The package intentionally exposes only the high-level types in the
`ChessRealms.Engine` namespace. Bitboards, encoded moves, magic tables, raw
positions and repository tools are implementation details and may change
without becoming package contracts.
25 changes: 14 additions & 11 deletions docs/game-rules-api.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Game API and rule boundaries

`ChessGame` is a mutable, sealed standard-chess game. A new game starts at the
usual position; `TryCreateFromFen` starts from a supplied position. The game
generates legal moves, applies them, records successful moves and classifies
endings. Move generation uses bitboards and magic attack tables; pseudolegal
moves are filtered for king safety.
usual position; `FromFen` and `TryCreateFromFen` start from a supplied position.
The game generates legal moves, applies them, records successful moves and
classifies endings. Move generation uses bitboards and magic attack tables;
pseudolegal moves are filtered for king safety.

## Moves, state and ownership

- `AlgebraicMove.Parse` accepts lowercase coordinate moves such as `e2e4` and
- `CoordinateMove.Parse` accepts lowercase coordinate moves such as `e2e4` and
promotion moves ending in `q`, `r`, `b` or `n`. Parsing checks syntax; the game
checks legality. Promotion has no default queen: `a7a8` is rejected if a
promotion is required. `Parse` throws on malformed input; `TryParse` returns
Expand All @@ -19,7 +19,8 @@ moves are filtered for king safety.
finished game without changing state; successful moves switch the side to
move, including a checkmating move. Use `Outcome.Winner`, not the side to
move, to identify the winner.
- `Position` is a value snapshot. `History` is a read-only snapshot of successful
- `GetPiece(Square)` reads one square. `CopyBoardTo(Span<ChessPiece>)` copies all
64 squares in a1-to-h8 order. `History` is a read-only snapshot of successful
moves with the move, FEN before/after and move flags. `UndoMove()` restores
the previous position, repetition count and outcome, including after a
terminal move or a draw claim made after that move. It returns false if there
Expand All @@ -32,8 +33,10 @@ moves are filtered for king safety.

`MoveResult` describes a successful move (`Move`, `Capture`, `Check`,
`Checkmate`, `Stalemate`); `Outcome` describes the game result and finish
reason. Loaded positions are classified immediately. `GetBoardToSpan` requires
64 entries in a1-to-h8 order, with `ChessPiece.Empty` for vacant squares.
reason. Loaded positions are classified immediately. `CopyBoardTo` requires a
destination of at least 64 entries, with `ChessPiece.Empty` for vacant squares.
The raw bitboard position is deliberately not exposed; use FEN, board inspection,
legal moves, history and `Clone()` for consumer scenarios.

## FEN validation

Expand All @@ -52,9 +55,9 @@ target does not require an adjacent capturing pawn. Export records a target
after every double push. Validation does not prove historical reachability.

Invalid `TryCreateFromFen` input returns false with a null game; it never
substitutes the starting position. `new ChessGame(position)` and
`FenStrings.Format(position)` throw `ArgumentException` for invalid positions.
A FEN import begins with one occurrence and no move history.
substitutes the starting position. `FromFen` throws `FormatException` for invalid
input. A FEN import begins with one occurrence and no move history. The standard
starting FEN is available as `ChessGame.StartingFen`.

## Draws and repetition

Expand Down
24 changes: 11 additions & 13 deletions src/ChessRealms.Engine.Console/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using ChessRealms.Engine;
using ChessRealms.Engine.Common;
using ChessRealms.Engine.Core.Math;

MoveResult lastMoveResult = MoveResult.None;
ChessGame chessGame = new();
Expand Down Expand Up @@ -30,19 +28,19 @@
{
DrawClaim reason = command[0] == "claim3" ? DrawClaim.ThreefoldRepetition : DrawClaim.FiftyMoveRule;
bool claimed = command.Length == 1 ? chessGame.ClaimDraw(reason)
: command.Length == 2 && AlgebraicMove.TryParse(command[1], out var intended)
: command.Length == 2 && CoordinateMove.TryParse(command[1], out var intended)
&& chessGame.ClaimDraw(reason, intended);
Console.WriteLine(claimed ? "Draw claimed." : "Draw claim unavailable.");
continue;
}
bool success = AlgebraicMove.TryParse(input, out var move)
bool success = CoordinateMove.TryParse(input, out var move)
&& (lastMoveResult = chessGame.MakeMove(move)) != MoveResult.None;
if (!success) Console.WriteLine("Invalid move or game already finished.");
}
static void PrintBoard(ChessGame chessGame)
{
Span<ChessPiece> pieceSpan = stackalloc ChessPiece[64];
chessGame.GetBoardToSpan(pieceSpan);
chessGame.CopyBoardTo(pieceSpan);

Console.WriteLine(" a b c d e f g h");

Expand All @@ -52,9 +50,9 @@ static void PrintBoard(ChessGame chessGame)

for (int f = 0; f < 8; ++f)
{
int square = SquareOps.FromFileRank(f, r);
int square = r * 8 + f;

if (pieceSpan[square].IsEmpty())
if (pieceSpan[square].IsEmpty)
{
Console.Write('.');
}
Expand All @@ -74,12 +72,12 @@ static char PieceToString(ref ChessPiece piece)
{
char p = piece.Value switch
{
PieceValue.Pawn => PieceCharsets.Ascii.Pawn,
PieceValue.Knight => PieceCharsets.Ascii.Knight,
PieceValue.Bishop => PieceCharsets.Ascii.Bishop,
PieceValue.Rook => PieceCharsets.Ascii.Rook,
PieceValue.Queen => PieceCharsets.Ascii.Queen,
PieceValue.King => PieceCharsets.Ascii.King,
PieceValue.Pawn => 'p',
PieceValue.Knight => 'n',
PieceValue.Bishop => 'b',
PieceValue.Rook => 'r',
PieceValue.Queen => 'q',
PieceValue.King => 'k',
_ => '\0'
};

Expand Down
5 changes: 5 additions & 0 deletions src/ChessRealms.Engine.Perft/ChessRealms.Engine.Perft.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@
<ProjectReference Include="..\ChessRealms.Engine\ChessRealms.Engine.csproj" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="ChessRealms.Engine.Tests" />
<InternalsVisibleTo Include="ChessRealms.Engine.Benchmark" />
</ItemGroup>

</Project>
6 changes: 3 additions & 3 deletions src/ChessRealms.Engine.Perft/PerftDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

namespace Perft
{
public static class PerftDriver
internal static class PerftDriver
{
public struct PerftResult
internal struct PerftResult
{
public ulong Nodes;
public int Captures;
Expand All @@ -27,7 +27,7 @@ public override readonly string ToString()
}
}

public static PerftResult Test(Position pos, int depth, bool upper = true)
internal static PerftResult Test(Position pos, int depth, bool upper = true)
{
Position tmpPos = new();
Span<int> moves = stackalloc int[MoveGen.MaxMoves];
Expand Down
34 changes: 17 additions & 17 deletions src/ChessRealms.Engine.Tests/ChessGameTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ internal class ChessGameTests
private static ChessPiece[] Board(ChessGame game)
{
var board = new ChessPiece[64];
game.GetBoardToSpan(board);
game.CopyBoardTo(board);
return board;
}

private static void AssertPiece(ChessPiece[] board, string square, PieceColor color, PieceValue value)
=> Assert.That(board[AlgebraicNotation.ParseSquare(square)], Is.EqualTo(new ChessPiece(color, value)));
=> Assert.That(board[Square.Parse(square).Index], Is.EqualTo(new ChessPiece(color, value)));

[Test]
public void NewGame_HasCompleteInitialBoardAndWhiteToMove()
Expand All @@ -24,9 +24,9 @@ public void NewGame_HasCompleteInitialBoardAndWhiteToMove()
Assert.Multiple(() =>
{
Assert.That(game!.CurrentColor, Is.EqualTo(PieceColor.White));
Assert.That(game.EnemyColor, Is.EqualTo(PieceColor.Black));
Assert.That(game.OpponentColor, Is.EqualTo(PieceColor.Black));
Assert.That(game.IsFinished, Is.False);
Assert.That(game.HasMoves(), Is.True);
Assert.That(game.HasLegalMoves, Is.True);
for (int file = 0; file < 8; file++)
{
Assert.That(board[file], Is.EqualTo(new ChessPiece(PieceColor.White, backRank[file])));
Expand All @@ -46,7 +46,7 @@ public void CreateFromFen_PreservesBoardAndBlackToMove()
Assert.Multiple(() =>
{
Assert.That(game!.CurrentColor, Is.EqualTo(PieceColor.Black));
Assert.That(board.Count(piece => !piece.IsEmpty()), Is.EqualTo(3));
Assert.That(board.Count(piece => !piece.IsEmpty), Is.EqualTo(3));
AssertPiece(board, "e8", PieceColor.Black, PieceValue.King);
AssertPiece(board, "e1", PieceColor.White, PieceValue.King);
AssertPiece(board, "e2", PieceColor.White, PieceValue.Pawn);
Expand All @@ -57,15 +57,15 @@ public void CreateFromFen_PreservesBoardAndBlackToMove()
public void OrdinaryMoves_UpdateBoardAndAlternateSides()
{
ChessGame game = new();
Assert.That(game.MakeMove(AlgebraicMove.Parse("e2e4")), Is.EqualTo(MoveResult.Move));
Assert.That(game.MakeMove(CoordinateMove.Parse("e2e4")), Is.EqualTo(MoveResult.Move));
Assert.That(game!.CurrentColor, Is.EqualTo(PieceColor.Black));
var expected = Board(new ChessGame());
expected[AlgebraicNotation.ParseSquare("e2")] = ChessPiece.Empty;
expected[AlgebraicNotation.ParseSquare("e4")] = new(PieceColor.White, PieceValue.Pawn);
expected[Square.Parse("e2").Index] = ChessPiece.Empty;
expected[Square.Parse("e4").Index] = new(PieceColor.White, PieceValue.Pawn);
Assert.That(Board(game), Is.EqualTo(expected));
Assert.That(game.MakeMove(AlgebraicMove.Parse("e7e5")), Is.EqualTo(MoveResult.Move));
expected[AlgebraicNotation.ParseSquare("e7")] = ChessPiece.Empty;
expected[AlgebraicNotation.ParseSquare("e5")] = new(PieceColor.Black, PieceValue.Pawn);
Assert.That(game.MakeMove(CoordinateMove.Parse("e7e5")), Is.EqualTo(MoveResult.Move));
expected[Square.Parse("e7").Index] = ChessPiece.Empty;
expected[Square.Parse("e5").Index] = new(PieceColor.Black, PieceValue.Pawn);
Assert.That(Board(game), Is.EqualTo(expected));
Assert.That(game!.CurrentColor, Is.EqualTo(PieceColor.White));
}
Expand All @@ -74,12 +74,12 @@ public void OrdinaryMoves_UpdateBoardAndAlternateSides()
public void Capture_RemovesEnemyAndMovesAttacker()
{
ChessGame game = new();
Assert.That(game.MakeMove(AlgebraicMove.Parse("e2e4")), Is.EqualTo(MoveResult.Move));
Assert.That(game.MakeMove(AlgebraicMove.Parse("d7d5")), Is.EqualTo(MoveResult.Move));
Assert.That(game.MakeMove(CoordinateMove.Parse("e2e4")), Is.EqualTo(MoveResult.Move));
Assert.That(game.MakeMove(CoordinateMove.Parse("d7d5")), Is.EqualTo(MoveResult.Move));
var expected = Board(game!);
expected[AlgebraicNotation.ParseSquare("e4")] = ChessPiece.Empty;
expected[AlgebraicNotation.ParseSquare("d5")] = new(PieceColor.White, PieceValue.Pawn);
Assert.That(game.MakeMove(AlgebraicMove.Parse("e4d5")), Is.EqualTo(MoveResult.Move | MoveResult.Capture));
expected[Square.Parse("e4").Index] = ChessPiece.Empty;
expected[Square.Parse("d5").Index] = new(PieceColor.White, PieceValue.Pawn);
Assert.That(game.MakeMove(CoordinateMove.Parse("e4d5")), Is.EqualTo(MoveResult.Move | MoveResult.Capture));
Assert.That(Board(game), Is.EqualTo(expected));
Assert.That(game!.CurrentColor, Is.EqualTo(PieceColor.Black));
}
Expand All @@ -94,7 +94,7 @@ public void IllegalMove_PreservesBoardTurnAndFinishedState(string fen, string mo
var before = Board(game!);
var color = game!.CurrentColor;
var finished = game.IsFinished;
Assert.That(game.MakeMove(AlgebraicMove.Parse(move)), Is.EqualTo(MoveResult.None));
Assert.That(game.MakeMove(CoordinateMove.Parse(move)), Is.EqualTo(MoveResult.None));
Assert.Multiple(() =>
{
Assert.That(Board(game), Is.EqualTo(before));
Expand Down
Loading
Loading