├── resources ├── icon.ico └── lynx.png ├── src ├── Lynx │ ├── Model │ │ ├── Side.cs │ │ ├── Piece.cs │ │ ├── PlyStackEntry.cs │ │ ├── PawnTableElement.cs │ │ ├── TranspositionTableFactory.cs │ │ ├── CastlingRights.cs │ │ ├── PVTable.cs │ │ ├── CastlingData.cs │ │ ├── TTProbeResult.cs │ │ ├── EvaluationContext.cs │ │ ├── SearchConstraints.cs │ │ ├── BoardSquare.cs │ │ ├── ParseFENResult.cs │ │ ├── GameState.cs │ │ ├── TablebaseResult.cs │ │ ├── SearchResult.cs │ │ └── TranspositionTableElement.cs │ ├── UCI │ │ └── Commands │ │ │ ├── GUI │ │ │ ├── QuitCommand.cs │ │ │ ├── StopCommand.cs │ │ │ ├── PonderHitCommand.cs │ │ │ ├── UCICommand.cs │ │ │ ├── IsReadyCommand.cs │ │ │ ├── UCINewGameCommand.cs │ │ │ ├── SetOptionCommand.cs │ │ │ ├── DebugCommand.cs │ │ │ ├── PositionCommand.cs │ │ │ └── RegisterCommand.cs │ │ │ └── Engine │ │ │ ├── UciOKCommand.cs │ │ │ ├── ReadyOKCommand.cs │ │ │ ├── CopyProtectionCommand.cs │ │ │ ├── IdCommand.cs │ │ │ ├── BestMoveCommand.cs │ │ │ ├── RegistrationCommand.cs │ │ │ └── InfoCommand.cs │ ├── LynxException.cs │ ├── ObjectPools.cs │ ├── SilentChannelWriter.cs │ ├── WDL.cs │ ├── Search │ │ └── OnlineTablebase.cs │ └── LynxRandom.cs ├── Lynx.Benchmark │ ├── BaseBenchmark.cs │ ├── Program.cs │ ├── Lynx.Benchmark.csproj │ ├── UCI_Benchmark.cs │ ├── ResetLS1B_Benchmark.cs │ ├── PriorityQueue_EnqueueRange_Benchmark.cs │ ├── EnumCasting_Benchmark.cs │ ├── EncodeMove_Benchmark.cs │ ├── PVTable_SumVsArrayAccess_Benchmark.cs │ ├── PieceOffset_Boolean_Benchmark.cs │ ├── PEXT_Benchmark.cs │ ├── ResetLS1BvsWithoutLS1B_Benchmark.cs │ ├── ZobristHash_EnPassant_Benchmark.cs │ └── IsDarkSquare_IsLightSquare_Benchmark.cs ├── Lynx.Dev │ ├── Properties │ │ └── launchSettings.json │ └── Lynx.Dev.csproj ├── Lynx.Cli │ ├── Properties │ │ └── launchSettings.json │ ├── Program.cs │ ├── Writer.cs │ ├── Listener.cs │ ├── appsettings.Development.json │ ├── Runner.cs │ └── Lynx.Cli.csproj └── Lynx.ConstantsGenerator │ ├── Lynx.ConstantsGenerator.csproj │ ├── OccupancyGenerator.cs │ ├── BitBoardsGenerator.cs │ └── PawnIslandsGenerator.cs ├── tests ├── Lynx.Test │ ├── Properties │ │ └── AssembyInfo.cs │ ├── PerftFRCXFENTestSuite.cs │ ├── Model │ │ ├── PVTableTest.cs │ │ ├── BoardSquareTest.cs │ │ └── MoveScoreTest.cs │ ├── BestMove │ │ ├── SacrificesTest.cs │ │ ├── IncreaseDepthWhenInCheckTest.cs │ │ ├── QuiescenceTest.cs │ │ ├── SingleLegalMoveTest.cs │ │ ├── MatesInExactlyXTest.cs │ │ └── MatesTest.cs │ ├── ConfigurationValuesTest.cs │ ├── Categories.cs │ ├── IncrementalEvalTest.cs │ ├── UCIHandlerTest.cs │ ├── Lynx.Test.csproj │ ├── Commands │ │ ├── StopCommandTest.cs │ │ ├── GoCommandTest.cs │ │ └── PositionCommandTest.cs │ ├── PregeneratedAttacks │ │ ├── RookOccupancyTest.cs │ │ ├── BishopOccupancyTest.cs │ │ ├── SetBishopOrRookOccupancyTest.cs │ │ ├── KingAttacksTest.cs │ │ ├── KnightAttacksTest.cs │ │ ├── PawnAttacksTest.cs │ │ └── BishopAttacksTest.cs │ ├── BaseTest.cs │ ├── MoveGeneration │ │ ├── RegressionTest.cs │ │ ├── GeneralMoveGeneratorTest.cs │ │ └── CastlingMoveTest.cs │ ├── WDLTest.cs │ ├── ConfigurationTest.cs │ ├── ZobristHashGenerationTest.cs │ ├── PSQTTest.cs │ └── PawnIslandsTest.cs └── runsettings.xml ├── global.json ├── .github ├── dependabot.yml └── workflows │ ├── on_release.yml │ ├── bench.yml │ ├── on-demand-tests.yml │ ├── benchmarks.yml │ └── perft.yml ├── .git-blame-ignore-revs ├── Lynx.slnx ├── LICENSE ├── Makefile ├── Directory.Build.props └── Directory.Packages.props /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynx-chess/Lynx/HEAD/resources/icon.ico -------------------------------------------------------------------------------- /resources/lynx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynx-chess/Lynx/HEAD/resources/lynx.png -------------------------------------------------------------------------------- /src/Lynx/Model/Side.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.Model; 2 | 3 | public enum Side { Black, White, Both } 4 | -------------------------------------------------------------------------------- /tests/Lynx.Test/Properties/AssembyInfo.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | [assembly: Parallelizable(ParallelScope.All)] -------------------------------------------------------------------------------- /tests/Lynx.Test/PerftFRCXFENTestSuite.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynx-chess/Lynx/HEAD/tests/Lynx.Test/PerftFRCXFENTestSuite.cs -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "10.0.100", 4 | "rollForward": "latestMajor", 5 | "allowPrerelease": false 6 | } 7 | } -------------------------------------------------------------------------------- /src/Lynx.Benchmark/BaseBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | 3 | namespace Lynx.Benchmark; 4 | 5 | [MarkdownExporterAttribute.GitHub] 6 | [HtmlExporter] 7 | [MemoryDiagnoser] 8 | //[NativeMemoryProfiler] 9 | public class BaseBenchmark; 10 | -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/GUI/QuitCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.UCI.Commands.GUI; 2 | 3 | /// 4 | /// quit 5 | /// quit the program as soon as possible 6 | /// 7 | public sealed class QuitCommand 8 | { 9 | public const string Id = "quit"; 10 | } 11 | -------------------------------------------------------------------------------- /src/Lynx.Dev/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Lynx.Dev": { 4 | "commandName": "Project" 5 | }, 6 | "WSL 2": { 7 | "commandName": "WSL2", 8 | "environmentVariables": {}, 9 | "distributionName": "" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/GUI/StopCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.UCI.Commands.GUI; 2 | 3 | /// 4 | /// stop 5 | /// stop calculating as soon as possible, 6 | /// don't forget the "bestmove" and possibly the "ponder" token when finishing the search 7 | /// 8 | public sealed class StopCommand 9 | { 10 | public const string Id = "stop"; 11 | } 12 | -------------------------------------------------------------------------------- /tests/Lynx.Test/Model/PVTableTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | 4 | namespace Lynx.Test.Model; 5 | 6 | public class PVTableTest 7 | { 8 | [Test] 9 | public void PVTable_Indexes_Support_MaxDepthPlusOne() 10 | { 11 | Assert.DoesNotThrow(() => _ = PVTable.Indexes[Configuration.EngineSettings.MaxDepth]); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/Engine/UciOKCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.UCI.Commands.Engine; 2 | 3 | /// 4 | /// uciok 5 | /// Must be sent after the id and optional options to tell the GUI that the engine 6 | /// has sent all infos and is ready in uci mode. 7 | /// 8 | public sealed class UciOKCommand 9 | { 10 | public const string Id = "uciok"; 11 | } 12 | -------------------------------------------------------------------------------- /src/Lynx/Model/Piece.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.Model; 2 | 3 | #pragma warning disable CA1708 // Identifiers should differ by more than case 4 | 5 | public enum Piece 6 | { 7 | Unknown = -1, 8 | P, N, B, R, Q, K, // White 9 | p, n, b, r, q, k, // Black 10 | None 11 | } 12 | 13 | #pragma warning restore CA1708 // Identifiers should differ by more than case 14 | -------------------------------------------------------------------------------- /src/Lynx/LynxException.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx; 2 | 3 | public class LynxException : Exception 4 | { 5 | public LynxException() 6 | { 7 | } 8 | 9 | public LynxException(string? message) : base(message) 10 | { 11 | } 12 | 13 | public LynxException(string? message, Exception? innerException) : base(message, innerException) 14 | { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Lynx.Cli/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Lynx.Cli": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "ASPNETCORE_ENVIRONMENT": "Development" 7 | } 8 | }, 9 | "WSL 2": { 10 | "commandName": "WSL2", 11 | "environmentVariables": { 12 | "ASPNETCORE_ENVIRONMENT": "Development" 13 | }, 14 | "distributionName": "" 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "nuget" 4 | directory: "/" 5 | allow: 6 | - dependency-type: "all" 7 | ignore: 8 | - dependency-name: "Moq" 9 | schedule: 10 | interval: "weekly" 11 | assignees : 12 | - "eduherminio" 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | assignees : 18 | - "eduherminio" 19 | -------------------------------------------------------------------------------- /src/Lynx.Benchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | using System.Reflection; 3 | #if DEBUG 4 | using BenchmarkDotNet.Configs; 5 | #endif 6 | 7 | //Lynx.Benchmark.BitBoard_Struct_ReadonlyStruct_Class_Record.SizeTest(); 8 | 9 | #if DEBUG 10 | BenchmarkSwitcher.FromAssembly(Assembly.GetExecutingAssembly()).Run(args, new DebugInProcessConfig()); 11 | #else 12 | BenchmarkSwitcher.FromAssembly(Assembly.GetExecutingAssembly()).Run(args); 13 | #endif 14 | -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/GUI/PonderHitCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.UCI.Commands.GUI; 2 | 3 | /// 4 | /// ponderhit 5 | /// the user has played the expected move. This will be sent if the engine was told to ponder on the same move 6 | /// the user has played. The engine should continue searching but switch from pondering to normal search. 7 | /// 8 | public sealed class PonderHitCommand 9 | { 10 | public const string Id = "ponderhit"; 11 | } 12 | -------------------------------------------------------------------------------- /src/Lynx.ConstantsGenerator/Lynx.ConstantsGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NonProdExe 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | $(NoWarn),CS8321,S125,S1481,CS0618,CS1591,S3353,S106,S2228 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Lynx/ObjectPools.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.ObjectPool; 2 | using System.Text; 3 | 4 | namespace Lynx; 5 | public static class ObjectPools 6 | { 7 | public static readonly ObjectPool StringBuilderPool = 8 | new DefaultObjectPoolProvider().CreateStringBuilderPool( 9 | initialCapacity: 256, 10 | maximumRetainedCapacity: 4 * 1024); //Default value in StringBuilderPooledObjectPolicy.MaximumRetainedCapacity) 11 | } 12 | -------------------------------------------------------------------------------- /src/Lynx/Model/PlyStackEntry.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.Model; 2 | 3 | public struct PlyStackEntry 4 | { 5 | public int StaticEval { get; set; } 6 | 7 | public int DoubleExtensions { get; set; } 8 | 9 | public Move Move { get; set; } 10 | 11 | public PlyStackEntry() 12 | { 13 | Reset(); 14 | } 15 | 16 | public void Reset() 17 | { 18 | StaticEval = int.MaxValue; 19 | DoubleExtensions = 0; 20 | Move = 0; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Lynx/Model/PawnTableElement.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.Model; 2 | 3 | #pragma warning disable CA1051 // Do not declare visible instance fields 4 | 5 | public struct PawnTableElement 6 | { 7 | public ulong Key; 8 | 9 | public int PackedScore; 10 | 11 | public void Update(ulong key, int packedScore) 12 | { 13 | Key = key; 14 | PackedScore = packedScore; 15 | } 16 | } 17 | 18 | #pragma warning restore CA1051 // Do not declare visible instance fields 19 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Seal all relevant classes 2 | ade22ec3dd024f16544f34a3f5c27b25c487bc1f 3 | 4 | # Enable implicit usings 5 | d4c05906bb9129bc00c5f8d91d61dd49c446fd7d 6 | 7 | # File scoped namespaces 8 | 403c9ee3ff5c23bb6564b27f9aeaa6960354bc10 9 | 10 | # Benchmark files rename 11 | 0ef149771cad8c69b3024273ec18c33d21fa571e 12 | 13 | # General formatting/cleaning commits 14 | 0c7a8ce29a88c182dd9db95fd6dffbf580a51ba2 15 | d3df78f393b5486954964533bb4ef59743b88447 16 | c5deabcaa157161ef782525dc2e4dbbdc471e3c1 -------------------------------------------------------------------------------- /src/Lynx.Dev/Lynx.Dev.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | NonProdExe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | $(NoWarn),CS8321,S125,S1481,CS0618 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Lynx/Model/TranspositionTableFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.Model; 2 | 3 | public static class TranspositionTableFactory 4 | { 5 | public static ITranspositionTable Create() 6 | { 7 | var hashBytes = (ulong)Configuration.EngineSettings.TranspositionTableSize * 1024 * 1024; 8 | var ttEntries = hashBytes / TranspositionTableElement.Size; 9 | 10 | if (ttEntries <= (ulong)Constants.MaxTTArrayLength) 11 | { 12 | return new SingleArrayTranspositionTable(); 13 | } 14 | 15 | return new MultiArrayTranspositionTable(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/Engine/ReadyOKCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.UCI.Commands.Engine; 2 | 3 | /// 4 | /// readyok 5 | /// This must be sent when the engine has received an "isready" command and has 6 | /// processed all input and is ready to accept new commands now. 7 | /// It is usually sent after a command that can take some time to be able to wait for the engine, 8 | /// but it can be used anytime, even when the engine is searching, 9 | /// and must always be answered with "isready". 10 | /// 11 | public sealed class ReadyOKCommand 12 | { 13 | public const string Id = "readyok"; 14 | } 15 | -------------------------------------------------------------------------------- /tests/Lynx.Test/BestMove/SacrificesTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace Lynx.Test.BestMove; 4 | 5 | public class SacrificesTest : BaseTest 6 | { 7 | [Explicit] 8 | [Category(Categories.LongRunning)] 9 | [TestCase("4n3/1p2k2p/p2p2pP/P1bP1pP1/2P2P2/2BB4/3K4/8 w - - 0 43", new[] { "d3f5" }, 10 | Description = "Actual Bishop sacrifice - https://lichess.org/VaY6zfHI/white#84")] 11 | public void Sacrifices(string fen, string[]? allowedUCIMoveString, string[]? excludedUCIMoveString = null) 12 | { 13 | TestBestMove(fen, allowedUCIMoveString, excludedUCIMoveString, depth: 25); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Lynx/Model/CastlingRights.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.Model; 2 | 3 | /// 4 | /// bin dec 5 | /// 0001 1 White king can O-O 6 | /// 0010 2 White king can O-O-O 7 | /// 0100 4 Black king can O-O 8 | /// 1000 8 Black king can O-O-O 9 | /// Examples: 10 | /// * 1111 Both sides can castle both directions 11 | /// * 1001 Black king => only O-O-O; White king => only O-O 12 | /// 13 | [Flags] 14 | #pragma warning disable S4022 // Enumerations should have "Int32" storage 15 | public enum CastlingRights : byte 16 | #pragma warning restore S4022 // Enumerations should have "Int32" storage 17 | { 18 | None = 0, // RCS1135 19 | WK = 1, WQ = 2, BK = 4, BQ = 8 20 | } 21 | -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/GUI/UCICommand.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.UCI.Commands.GUI; 2 | 3 | /// 4 | /// uci 5 | /// tell engine to use the uci (universal chess interface), 6 | /// this will be send once as a first command after program boot 7 | /// to tell the engine to switch to uci mode. 8 | /// After receiving the uci command the engine must identify itself with the "id" command 9 | /// and sent the "option" commands to tell the GUI which engine settings the engine supports if any. 10 | /// After that the engine should sent "uciok" to acknowledge the uci mode. 11 | /// If no uciok is sent within a certain time period, the engine task will be killed by the GUI. 12 | /// 13 | public sealed class UCICommand 14 | { 15 | public const string Id = "uci"; 16 | } 17 | -------------------------------------------------------------------------------- /src/Lynx/Model/PVTable.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | namespace Lynx.Model; 4 | 5 | public static class PVTable 6 | { 7 | public static readonly ImmutableArray Indexes = Initialize(); 8 | 9 | private static ImmutableArray Initialize() 10 | { 11 | Span indexes = stackalloc int[Configuration.EngineSettings.MaxDepth + Constants.ArrayDepthMargin]; 12 | int previousPVIndex = 0; 13 | indexes[0] = previousPVIndex; 14 | 15 | for (int depth = 0; depth < indexes.Length - 1; ++depth) 16 | { 17 | indexes[depth + 1] = previousPVIndex + Configuration.EngineSettings.MaxDepth - depth; 18 | previousPVIndex = indexes[depth + 1]; 19 | } 20 | 21 | return [.. indexes]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/on_release.yml: -------------------------------------------------------------------------------- 1 | name: Notify release 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | release-lynx-bot: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: "Extract tag from '${{ github.ref }}'" 14 | shell: pwsh 15 | run: | 16 | $tag = "${{ github.ref }}".Replace("refs/tags/", "") 17 | echo "RELEASE_TAG=$tag" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append 18 | 19 | - name: 'Repository Dispatch (tag: ${{ env.RELEASE_TAG }})' 20 | uses: peter-evans/repository-dispatch@v2 21 | with: 22 | token: ${{ secrets.REPO_ACCESS_TOKEN }} 23 | repository: lynx-chess/Lynx_BOT 24 | event-type: lynx-release 25 | client-payload: '{"tag": "${{ env.RELEASE_TAG }}", "sha": "${{ github.sha }}"}' 26 | -------------------------------------------------------------------------------- /tests/runsettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | cobertura 8 | true 9 | Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | true 18 | 19 | -------------------------------------------------------------------------------- /src/Lynx.Benchmark/Lynx.Benchmark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | Benchmark 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | $(NoWarn),CA1822,CA1806,CS0618 21 | true 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Lynx/Model/CastlingData.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.Model; 2 | 3 | #pragma warning disable CA1051 // Do not declare visible instance fields 4 | 5 | public readonly ref struct CastlingData 6 | { 7 | public const int DefaultValues = -1; 8 | 9 | public readonly int WhiteKingsideRook; 10 | public readonly int WhiteQueensideRook; 11 | public readonly int BlackKingsideRook; 12 | public readonly int BlackQueensideRook; 13 | 14 | public CastlingData( 15 | int whiteKingsideRook, 16 | int whiteQueensideRook, 17 | int blackKingsideRook, 18 | int blackQueensideRook) 19 | { 20 | WhiteKingsideRook = whiteKingsideRook; 21 | WhiteQueensideRook = whiteQueensideRook; 22 | BlackKingsideRook = blackKingsideRook; 23 | BlackQueensideRook = blackQueensideRook; 24 | } 25 | } 26 | 27 | #pragma warning restore CA1051 // Do not declare visible instance fields 28 | -------------------------------------------------------------------------------- /src/Lynx/Model/TTProbeResult.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.Model; 2 | 3 | #pragma warning disable CA1051 // Do not declare visible instance fields 4 | 5 | public readonly ref struct TTProbeResult 6 | { 7 | public readonly int Score = EvaluationConstants.NoScore; 8 | 9 | public readonly int StaticEval = EvaluationConstants.NoScore; 10 | 11 | public readonly int Depth; 12 | 13 | public readonly short BestMove; 14 | 15 | public readonly NodeType NodeType; 16 | 17 | public readonly bool WasPv; 18 | 19 | public TTProbeResult(int score, short bestMove, NodeType nodeType, int staticEval, int depth, bool wasPv) 20 | { 21 | Score = score; 22 | BestMove = bestMove; 23 | NodeType = nodeType; 24 | StaticEval = staticEval; 25 | Depth = depth; 26 | WasPv = wasPv; 27 | } 28 | } 29 | 30 | #pragma warning restore CA1051 // Do not declare visible instance fields 31 | -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/GUI/IsReadyCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.UCI.Commands.GUI; 2 | 3 | /// 4 | /// isready 5 | /// this is used to synchronize the engine with the GUI. When the GUI has sent a command or 6 | /// multiple commands that can take some time to complete, 7 | /// this command can be used to wait for the engine to be ready again or 8 | /// to ping the engine to find out if it is still alive. 9 | /// E.g. this should be sent after setting the path to the tablebases as this can take some time. 10 | /// This command is also required once before the engine is asked to do any search 11 | /// to wait for the engine to finish initializing. 12 | /// This command must always be answered with "readyok" and can be sent also when the engine is calculating 13 | /// in which case the engine should also immediately answer with "readyok" without stopping the search. 14 | /// 15 | public sealed class IsReadyCommand 16 | { 17 | public const string Id = "isready"; 18 | } 19 | -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/GUI/UCINewGameCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.UCI.Commands.GUI; 2 | 3 | /// 4 | /// ucinewgame 5 | /// this is sent to the engine when the next search (started with "position" and "go") will be from 6 | /// a different game. This can be a new game the engine should play or a new game it should analyse but 7 | /// also the next position from a testsuite with positions only. 8 | /// If the GUI hasn't sent a "ucinewgame" before the first "position" command, the engine shouldn't 9 | /// expect any further ucinewgame commands as the GUI is probably not supporting the ucinewgame command. 10 | /// So the engine should not rely on this command even though all new GUIs should support it. 11 | /// As the engine's reaction to "ucinewgame" can take some time the GUI should always send "isready" 12 | /// after "ucinewgame" to wait for the engine to finish its operation. 13 | /// 14 | public sealed class UCINewGameCommand 15 | { 16 | public const string Id = "ucinewgame"; 17 | } 18 | -------------------------------------------------------------------------------- /src/Lynx/Model/EvaluationContext.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.Model; 2 | 3 | #pragma warning disable CA1051 // Do not declare visible instance fields 4 | 5 | public ref struct EvaluationContext 6 | { 7 | public Span Attacks; 8 | public Span AttacksBySide; 9 | 10 | public int WhiteKingRingAttacks; 11 | public int BlackKingRingAttacks; 12 | 13 | public EvaluationContext(Span attacks, Span attacksBySide) 14 | { 15 | Attacks = attacks; 16 | AttacksBySide = attacksBySide; 17 | 18 | Attacks.Clear(); 19 | AttacksBySide.Clear(); 20 | } 21 | 22 | public void IncreaseKingRingAttacks(int side, int count) 23 | { 24 | if (side == (int)Side.White) 25 | { 26 | WhiteKingRingAttacks += count; 27 | } 28 | else 29 | { 30 | BlackKingRingAttacks += count; 31 | } 32 | } 33 | } 34 | 35 | #pragma warning restore CA1051 // Do not declare visible instance fields 36 | -------------------------------------------------------------------------------- /tests/Lynx.Test/ConfigurationValuesTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using NUnit.Framework; 3 | 4 | namespace Lynx.Test; 5 | 6 | /// 7 | /// Current logic relies on this 8 | /// 9 | [Explicit] 10 | [Category(Categories.Configuration)] 11 | [NonParallelizable] 12 | public class ConfigurationValuesTest 13 | { 14 | [Test] 15 | public void RazoringValues() 16 | { 17 | Assert.Greater(Configuration.EngineSettings.RFP_MaxDepth, Configuration.EngineSettings.Razoring_MaxDepth); 18 | 19 | var config = new ConfigurationBuilder() 20 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) 21 | .Build(); 22 | 23 | var engineSettingsSection = config.GetRequiredSection(nameof(EngineSettings)); 24 | Assert.IsNotNull(engineSettingsSection); 25 | engineSettingsSection.Bind(Configuration.EngineSettings); 26 | 27 | Assert.Greater(Configuration.EngineSettings.RFP_MaxDepth, Configuration.EngineSettings.Razoring_MaxDepth); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/GUI/SetOptionCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.UCI.Commands.GUI; 2 | 3 | /// 4 | /// setoption name [value] 5 | /// this is sent to the engine when the user wants to change the internal parameters 6 | /// of the engine. For the "button" type no value is needed. 7 | /// One string will be sent for each parameter and this will only be sent when the engine is waiting. 8 | /// The name of the option in should not be case sensitive and can inludes spaces like also the value. 9 | /// The substrings "value" and "name" should be avoided in and to allow unambiguous parsing, 10 | /// for example do not use = "draw value". 11 | /// Here are some strings for the example below: 12 | /// "setoption name Nullmove value true\n" 13 | /// "setoption name Selectivity value 3\n" 14 | /// "setoption name Style value Risky\n" 15 | /// "setoption name Clear Hash\n" 16 | /// "setoption name NalimovPath value c:\chess\tb\4;c:\chess\tb\5\n" 17 | /// 18 | public sealed class SetOptionCommand 19 | { 20 | public const string Id = "setoption"; 21 | } 22 | -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/Engine/CopyProtectionCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.UCI.Commands.Engine; 2 | 3 | /// 4 | /// copyprotection 5 | /// this is needed for copyprotected engines. After the uciok command the engine can tell the GUI, 6 | /// that it will check the copy protection now. This is done by "copyprotection checking". 7 | /// If the check is ok the engine should sent "copyprotection ok", otherwise "copyprotection error". 8 | /// If there is an error the engine should not function properly but should not quit alone. 9 | /// If the engine reports "copyprotection error" the GUI should not use this engine 10 | /// and display an error message instead! 11 | /// The code in the engine can look like this 12 | /// TellGUI("copyprotection checking\n"); 13 | /// ... check the copy protection here ... 14 | /// if (ok) 15 | /// TellGUI("copyprotection ok\n"); 16 | /// else 17 | /// TellGUI("copyprotection error\n"); 18 | /// 19 | public sealed class CopyProtectionCommand 20 | { 21 | public const string Id = "copyprotection"; 22 | } 23 | -------------------------------------------------------------------------------- /tests/Lynx.Test/Categories.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.Test; 2 | 3 | public static class Categories 4 | { 5 | public const string Perft = "Perft"; 6 | 7 | public const string PerftFRC = "PerftFRC"; 8 | 9 | public const string PerftFRCExhaustive = "PerftFRCExhaustive"; 10 | 11 | public const string LongRunning = "LongRunning"; 12 | 13 | /// 14 | /// Need to run in isolation, since other tests might modify values 15 | /// 16 | public const string Configuration = "Configuration"; 17 | 18 | /// 19 | /// Can't be run since it'd take way too long for regular CI 20 | /// 21 | public const string TooLong = "TooLongToBeRun"; 22 | 23 | /// 24 | /// Can't be run since no prunning is required 25 | /// 26 | public const string NoPruning = "RequireNoPruning"; 27 | 28 | /// 29 | /// Can't be run since our engine is simply not good enough yet 30 | /// 31 | public const string NotGoodEnough = "NotGoodEnough"; 32 | } 33 | -------------------------------------------------------------------------------- /Lynx.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Lynx.Cli/Program.cs: -------------------------------------------------------------------------------- 1 | using Lynx; 2 | using Lynx.Cli; 3 | using Microsoft.Extensions.Configuration; 4 | using NLog; 5 | using NLog.Extensions.Logging; 6 | 7 | #if DEBUG 8 | Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); 9 | #endif 10 | 11 | var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"; 12 | 13 | var config = new ConfigurationBuilder() 14 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) 15 | .AddJsonFile($"appsettings.{environmentName}.json", optional: true, reloadOnChange: false) 16 | .AddJsonFile("appsettings.tournament.json", optional: true, reloadOnChange: false) 17 | .AddEnvironmentVariables() 18 | .Build(); 19 | 20 | config.GetSection(nameof(EngineSettings)).Bind(Configuration.EngineSettings); 21 | config.GetSection(nameof(GeneralSettings)).Bind(Configuration.GeneralSettings); 22 | 23 | if (Configuration.GeneralSettings.EnableLogging) 24 | { 25 | LogManager.Configuration = new NLogLoggingConfiguration(config.GetSection("NLog")); 26 | } 27 | 28 | await Runner.Run(args); 29 | 30 | Thread.Sleep(2_000); 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Eduardo Cáceres 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/bench.yml: -------------------------------------------------------------------------------- 1 | name: Bench 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | 8 | bench: 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, windows-latest, macOS-latest, macOS-15-intel] 14 | fail-fast: false 15 | 16 | env: 17 | DOTNET_VERSION: 10.0.x 18 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 19 | 20 | steps: 21 | - uses: actions/checkout@v6 22 | 23 | - name: Setup .NET 24 | uses: actions/setup-dotnet@v5 25 | with: 26 | dotnet-version: ${{ env.DOTNET_VERSION }} 27 | 28 | - name: Nuget cache 29 | uses: actions/cache@v5 30 | with: 31 | path: 32 | ~/.nuget/packages 33 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} 34 | restore-keys: | 35 | ${{ runner.os }}-nuget- 36 | 37 | - name: Build 38 | run: dotnet build -c Release 39 | working-directory: ./src/Lynx.Cli 40 | 41 | - name: Run bench ${{ github.event.inputs.depth }} on ${{ github.event.inputs.fen }} 42 | run: dotnet run -c Release --no-build "bench" 43 | working-directory: ./src/Lynx.Cli 44 | -------------------------------------------------------------------------------- /src/Lynx.Cli/Writer.cs: -------------------------------------------------------------------------------- 1 | using NLog; 2 | using System.Threading.Channels; 3 | 4 | namespace Lynx.Cli; 5 | 6 | public sealed class Writer 7 | { 8 | private readonly ChannelReader _engineOutputReader; 9 | private readonly Logger _logger; 10 | 11 | public Writer(ChannelReader engineOutputReader) 12 | { 13 | _engineOutputReader = engineOutputReader; 14 | _logger = LogManager.GetCurrentClassLogger(); 15 | } 16 | 17 | public async Task Run(CancellationToken cancellationToken) 18 | { 19 | try 20 | { 21 | await foreach (var output in _engineOutputReader.ReadAllAsync(cancellationToken)) 22 | { 23 | var str = output.ToString(); 24 | Console.WriteLine(str); 25 | 26 | if (_logger.IsInfoEnabled) 27 | { 28 | _logger.Info("[Lynx]\t{0}", str); 29 | } 30 | } 31 | } 32 | catch (Exception e) 33 | { 34 | _logger.Fatal(e); 35 | } 36 | finally 37 | { 38 | _logger.Info("Finishing {0}", nameof(Writer)); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Lynx.Test/BestMove/IncreaseDepthWhenInCheckTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using Lynx.UCI.Commands.GUI; 3 | using NUnit.Framework; 4 | 5 | namespace Lynx.Test.BestMove; 6 | public class IncreaseDepthWhenInCheckTest : BaseTest 7 | { 8 | /// 9 | /// 8 . r . . . . . . 10 | /// 7 . . . . . . . . 11 | /// 6 . . . . . . . . 12 | /// 5 . . . . . . k P 13 | /// 4 K . . . . . . . 14 | /// 3 . . . . . R . . 15 | /// 2 . q . . . . . . 16 | /// 1 R . . . . . q . 17 | /// a b c d e f g h 18 | /// 19 | [Ignore("Not valid any more, gotta find another way of checking this")] 20 | [Test] 21 | public void DepthLimit() 22 | { 23 | var engine = GetEngine("1r6/8/8/6kP/K7/5R1p/1q6/R5q1 w - - 0 2"); 24 | Assert.AreEqual(Side.White, engine.Game.CurrentPosition.Side); 25 | 26 | var searchResult = engine.BestMove(new GoCommand("go depth 1")); 27 | 28 | // In Quiescence search, which would be triggered after Rxg1+ without 29 | // the depth increase, Black would capture the pawn and get checkmated 30 | Assert.AreEqual("g5h4", searchResult.Moves[1].UCIString()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Lynx.ConstantsGenerator/OccupancyGenerator.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | 3 | namespace Lynx.ConstantsGenerator; 4 | 5 | public static class OccupancyGenerator 6 | { 7 | public static void BishopRelevantOccupancyBits() 8 | { 9 | for (var rank = 0; rank < 8; ++rank) 10 | { 11 | for (var file = 0; file < 8; ++file) 12 | { 13 | int square = BitBoardExtensions.SquareIndex(rank, file); 14 | 15 | var bishopOccupancy = AttackGenerator.MaskBishopOccupancy(square); 16 | Console.Write($"{bishopOccupancy.CountBits()}, "); 17 | } 18 | 19 | Console.WriteLine(); 20 | } 21 | } 22 | 23 | public static void RookRelevantOccupancyBits() 24 | { 25 | for (var rank = 0; rank < 8; ++rank) 26 | { 27 | for (var file = 0; file < 8; ++file) 28 | { 29 | int square = BitBoardExtensions.SquareIndex(rank, file); 30 | 31 | var bishopOccupancy = AttackGenerator.MaskRookOccupancy(square); 32 | Console.Write($"{bishopOccupancy.CountBits()}, "); 33 | } 34 | 35 | Console.WriteLine(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Lynx/Model/SearchConstraints.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.Model; 2 | 3 | #pragma warning disable CA1051 // Do not declare visible instance fields 4 | 5 | public readonly struct SearchConstraints 6 | { 7 | /// 8 | /// ~ / 200. 9 | /// Not to avoid overflows in case we're ever attempt to scale it 10 | /// 11 | private const int DefaultTimeBound = 10_000_000; 12 | 13 | public const int DefaultHardLimitTimeBound = DefaultTimeBound; 14 | 15 | public const int DefaultSoftLimitTimeBound = DefaultTimeBound; 16 | 17 | public readonly int HardLimitTimeBound; 18 | 19 | public readonly int SoftLimitTimeBound; 20 | 21 | public readonly int MaxDepth; 22 | 23 | public static readonly SearchConstraints InfiniteSearchConstraint = new(DefaultHardLimitTimeBound, DefaultSoftLimitTimeBound, -1); 24 | 25 | public SearchConstraints(int hardLimitTimeBound, int softLimitTimeBound, int maxDepth) 26 | { 27 | HardLimitTimeBound = hardLimitTimeBound; 28 | SoftLimitTimeBound = softLimitTimeBound; 29 | MaxDepth = maxDepth; 30 | } 31 | } 32 | 33 | #pragma warning restore CA1051 // Do not declare visible instance fields -------------------------------------------------------------------------------- /.github/workflows/on-demand-tests.yml: -------------------------------------------------------------------------------- 1 | name: On demand tests 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | test_pattern: 7 | description: 'Test --filter pattern' 8 | default: '*' 9 | required: false 10 | 11 | jobs: 12 | 13 | on-demand-tests: 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, windows-latest, macOS-latest, macOS-15-intel] 19 | fail-fast: false 20 | 21 | env: 22 | DOTNET_VERSION: 10.0.x 23 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 24 | 25 | steps: 26 | - uses: actions/checkout@v6 27 | 28 | - name: Setup .NET 29 | uses: actions/setup-dotnet@v5 30 | with: 31 | dotnet-version: ${{ env.DOTNET_VERSION }} 32 | 33 | - name: Nuget cache 34 | uses: actions/cache@v5 35 | with: 36 | path: 37 | ~/.nuget/packages 38 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} 39 | restore-keys: | 40 | ${{ runner.os }}-nuget- 41 | 42 | - name: Build 43 | run: dotnet build -c Release 44 | 45 | - name: Running tests with `--filter '${{ github.event.inputs.test_pattern }}' 46 | run: dotnet test -c Release --no-build --filter '${{ github.event.inputs.test_pattern }}' -v normal 47 | -------------------------------------------------------------------------------- /src/Lynx/SilentChannelWriter.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Channels; 2 | 3 | namespace Lynx; 4 | 5 | public sealed class SilentChannelWriter : ChannelWriter 6 | { 7 | #pragma warning disable CA1000 // Do not declare static members on generic types 8 | public static SilentChannelWriter Instance { get; } = new(); 9 | #pragma warning restore CA1000 // Do not declare static members on generic types 10 | 11 | /// 12 | /// Explicit static constructor to tell C# compiler not to mark type as beforefieldinit 13 | /// https://csharpindepth.com/articles/singleton 14 | /// 15 | #pragma warning disable S3253 // Constructor and destructor declarations should not be redundant 16 | static SilentChannelWriter() { } 17 | #pragma warning restore S3253 // Constructor and destructor declarations should not be redundant 18 | 19 | private SilentChannelWriter() { } 20 | 21 | /// 22 | /// Returns 23 | /// 24 | public override bool TryWrite(T item) => true; 25 | 26 | /// 27 | /// Returns a static wrapping 28 | /// 29 | /// A non-usable 30 | public override ValueTask WaitToWriteAsync(CancellationToken cancellationToken = default) => default; 31 | } 32 | -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/Engine/IdCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Lynx.UCI.Commands.Engine; 4 | 5 | /// 6 | /// id 7 | /// * name 8 | /// this must be sent after receiving the "uci" command to identify the engine, 9 | /// e.g. "id name Shredder X.Y\n" 10 | /// * author 11 | /// this must be sent after receiving the "uci" command to identify the engine, 12 | /// e.g. "id author Stefan MK\n" 13 | /// 14 | public sealed class IdCommand 15 | { 16 | public const string IdString = "id"; 17 | 18 | public const string EngineName = "Lynx"; 19 | 20 | public const string EngineAuthor = "Eduardo Caceres"; 21 | 22 | public static string GetLynxVersion() 23 | { 24 | var fullVersion = Assembly.GetAssembly(typeof(IdCommand)) 25 | !.GetCustomAttribute() 26 | ?.InformationalVersion; 27 | 28 | var parts = fullVersion?.Split('+'); 29 | 30 | #if LYNX_RELEASE 31 | return parts?[0] ?? "Unknown"; 32 | #else 33 | return parts?.Length switch 34 | { 35 | >= 2 => $"{parts[0]}-dev-{parts[1][..Math.Min(8, parts[1].Length)]}", 36 | 1 => parts[0], 37 | _ => "Unknown" 38 | }; 39 | #endif 40 | } 41 | 42 | public static string NameString => $"id name {EngineName} {GetLynxVersion()}"; 43 | 44 | public static string AuthorString => $"id author {EngineAuthor}"; 45 | } 46 | -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/GUI/DebugCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.UCI.Commands.GUI; 2 | 3 | /// 4 | /// debug [ on | off ] 5 | /// switch the debug mode of the engine on and off. 6 | /// In debug mode the engine should sent additional infos to the GUI, e.g. with the "info string" command, 7 | /// to help debugging, e.g. the commands that the engine has received etc. 8 | /// This mode should be switched off by default and this command can be sent 9 | /// any time, also when the engine is thinking. 10 | /// 11 | public sealed class DebugCommand 12 | { 13 | public const string Id = "debug"; 14 | 15 | /// 16 | /// Parse debug command 17 | /// 18 | /// 19 | /// true if debug command sent 'on' 20 | /// false if debug command sent 'off' 21 | /// if something else was sent 22 | /// 23 | /// 24 | public static bool Parse(ReadOnlySpan command) 25 | { 26 | const string on = "on"; 27 | const string off= "off"; 28 | 29 | Span items = stackalloc Range[2]; 30 | command.Split(items, ' ', StringSplitOptions.RemoveEmptyEntries); 31 | 32 | var debugValue = command[items[1]]; 33 | 34 | return debugValue.Equals(on, StringComparison.OrdinalIgnoreCase) 35 | || (!debugValue.Equals(off, StringComparison.OrdinalIgnoreCase) 36 | && Configuration.IsDebug); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Lynx/Model/BoardSquare.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.Model; 2 | 3 | /// 4 | /// Big-Endian Rank-File Mapping 5 | /// (opposte to LERF, https://www.chessprogramming.org/Square_Mapping_Considerations#LittleEndianRankFileMapping) 6 | /// 7 | /// NW N NE 8 | /// -9 -8 -7 9 | /// \ | / 10 | /// W -1 <- 0 -> +1 E 11 | /// / | \ 12 | /// +7 +8 +9 13 | /// SW S SE 14 | /// 15 | /// 16 | public enum BoardSquare 17 | { 18 | a8, b8, c8, d8, e8, f8, g8, h8, 19 | a7, b7, c7, d7, e7, f7, g7, h7, 20 | a6, b6, c6, d6, e6, f6, g6, h6, 21 | a5, b5, c5, d5, e5, f5, g5, h5, 22 | a4, b4, c4, d4, e4, f4, g4, h4, 23 | a3, b3, c3, d3, e3, f3, g3, h3, 24 | a2, b2, c2, d2, e2, f2, g2, h2, 25 | a1, b1, c1, d1, e1, f1, g1, h1, 26 | noSquare = -1 27 | } 28 | 29 | public static class BoardSquareExtensions 30 | { 31 | /// 32 | /// https://www.chessprogramming.org/Color_of_a_Square 33 | /// 34 | public static bool DifferentColor(int squareIndex1, int squareIndex2) 35 | { 36 | return ((9 * (squareIndex1 ^ squareIndex2)) & 8) != 0; 37 | } 38 | 39 | /// 40 | /// https://www.chessprogramming.org/Color_of_a_Square 41 | /// 42 | public static bool SameColor(int squareIndex1, int squareIndex2) 43 | { 44 | return ((9 * (squareIndex1 ^ squareIndex2)) & 8) == 0; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Lynx/Model/ParseFENResult.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.Model; 2 | 3 | #pragma warning disable CA1051 // Do not declare visible instance fields 4 | 5 | public readonly ref struct ParseFENResult 6 | { 7 | #pragma warning disable S3887 // Mutable, non-private fields should not be "readonly" 8 | public readonly BitBoard[] PieceBitBoards; 9 | public readonly BitBoard[] OccupancyBitBoards; 10 | public readonly int[] Board; 11 | #pragma warning restore S3887 // Mutable, non-private fields should not be "readonly" 12 | 13 | public readonly int HalfMoveClock; 14 | //public readonly int FullMoveCounter; 15 | public readonly BoardSquare EnPassant; 16 | 17 | public readonly CastlingData CastlingData; 18 | 19 | public readonly Side Side; 20 | public readonly byte Castle; 21 | 22 | public ParseFENResult( 23 | BitBoard[] pieceBitBoards, 24 | BitBoard[] occupancyBitBoards, 25 | int[] board, 26 | Side side, 27 | byte castle, 28 | BoardSquare enPassant, 29 | CastlingData castlingData, 30 | int halfMoveClock) 31 | { 32 | PieceBitBoards = pieceBitBoards; 33 | OccupancyBitBoards = occupancyBitBoards; 34 | Board = board; 35 | Side = side; 36 | Castle = castle; 37 | EnPassant = enPassant; 38 | CastlingData = castlingData; 39 | HalfMoveClock = halfMoveClock; 40 | } 41 | } 42 | 43 | #pragma warning restore CA1051 // Do not declare visible instance fields 44 | -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/Engine/BestMoveCommand.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | 3 | namespace Lynx.UCI.Commands.Engine; 4 | 5 | /// 6 | /// bestmove [ ponder ] 7 | /// the engine has stopped searching and found the best move in this position. 8 | /// the engine can send the move it likes to ponder on. The engine must not start pondering automatically. 9 | /// this command must always be sent if the engine stops searching, also in pondering mode if there is a 10 | /// "stop" command, so for every "go" command a "bestmove" command is needed! 11 | /// Directly before that the engine should send a final "info" command with the final search information, 12 | /// the GUI has the complete statistics about the last search. 13 | /// 14 | public sealed class BestMoveCommand 15 | { 16 | public const string Id = "bestmove"; 17 | 18 | private readonly Move _move; 19 | private readonly Move? _moveToPonder; 20 | 21 | public BestMoveCommand(SearchResult searchResult) 22 | { 23 | _move = searchResult.BestMove; 24 | 25 | // We alwaus try to print ponder move, regardless of ponder on/off 26 | _moveToPonder = searchResult.Moves.Length >= 2 ? searchResult.Moves[1] : null; 27 | } 28 | 29 | public override string ToString() 30 | { 31 | return $"bestmove {_move.UCIStringMemoized()}" + 32 | (_moveToPonder.HasValue 33 | ? $" ponder {_moveToPonder!.Value.UCIStringMemoized()}" 34 | : string.Empty); 35 | } 36 | } -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/Engine/RegistrationCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.UCI.Commands.Engine; 2 | 3 | /// 4 | /// registration 5 | /// this is needed for engines that need a username and / or a code to function with all features. 6 | /// Analog to the "copyprotection" command the engine can send "registration checking" 7 | /// after the uciok command followed by either "registration ok" or "registration error". 8 | /// Also after every attempt to register the engine it should answer with "registration checking" 9 | /// and then either "registration ok" or "registration error". 10 | /// In contrast to the "copyprotection" command, the GUI can use the engine after the engine has 11 | /// reported an error, but should inform the user that the engine is not properly registered 12 | /// and might not use all its features. 13 | /// In addition the GUI should offer to open a dialog to 14 | /// enable registration of the engine.To try to register an engine the GUI can send 15 | /// the "register" command. 16 | /// The GUI has to always answer with the "register" command if the engine sends "registration error" 17 | /// at engine startup (this can also be done with "register later") 18 | /// and tell the user somehow that the engine is not registered. 19 | /// This way the engine knows that the GUI can deal with the registration procedure and the user 20 | /// will be informed that the engine is not properly registered. 21 | /// 22 | public sealed class RegistrationCommand 23 | { 24 | public const string Id = "uciok"; 25 | } 26 | -------------------------------------------------------------------------------- /tests/Lynx.Test/IncrementalEvalTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | 4 | namespace Lynx.Test; 5 | public class IncrementalEvalTest 6 | { 7 | /// 8 | /// If castling moves are ever refactored, i.e. when adding FRC support, this should break. 9 | /// That'll mean that incremental eval condition in needs to change 10 | /// 11 | [Test] 12 | public void CastlingMovesAreKingMoves() 13 | { 14 | var position = new Position("r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQkq - 0 1"); 15 | 16 | ValidateCastlingMoves(position); 17 | 18 | position = new Position("r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R b KQkq - 0 1"); 19 | ValidateCastlingMoves(position); 20 | 21 | static void ValidateCastlingMoves(Position position) 22 | { 23 | Span moveSpan = stackalloc Move[2]; 24 | var index = 0; 25 | 26 | Span attacks = stackalloc BitBoard[12]; 27 | Span attacksBySide = stackalloc BitBoard[2]; 28 | var evaluationContext = new EvaluationContext(attacks, attacksBySide); 29 | 30 | MoveGenerator.GenerateCastlingMoves(ref index, moveSpan, position, ref evaluationContext); 31 | 32 | foreach (var move in moveSpan[..index]) 33 | { 34 | Assert.IsTrue(move.IsCastle()); 35 | Assert.AreEqual((int)Piece.K + Utils.PieceOffset(position.Side), move.Piece()); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Lynx.Test/UCIHandlerTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.UCI.Commands.Engine; 2 | using NUnit.Framework; 3 | using System.Buffers; 4 | using System.Diagnostics; 5 | using System.Threading.Channels; 6 | 7 | namespace Lynx.Test; 8 | 9 | public class UCIHandlerTest 10 | { 11 | [Test] 12 | public async Task HandleUCI() 13 | { 14 | var engineChannel = Channel.CreateUnbounded(); 15 | var guiChannel = Channel.CreateUnbounded(); 16 | 17 | using var searcher = new Searcher(guiChannel.Reader, engineChannel.Writer); 18 | using var cts = new CancellationTokenSource(); 19 | cts.CancelAfter(5_000); 20 | 21 | await new UCIHandler(guiChannel, engineChannel, searcher).HandleUCI(cts.Token); 22 | 23 | var booleansDetected = 0; 24 | 25 | var correctValues = SearchValues.Create(["true", "false"], StringComparison.Ordinal); 26 | var incorrectValues = SearchValues.Create([bool.TrueString, bool.FalseString], StringComparison.Ordinal); 27 | 28 | await foreach (var command in engineChannel.Reader.ReadAllAsync(cts.Token)) 29 | { 30 | var str = command.ToString(); 31 | var span = str.AsSpan(); 32 | 33 | if (span == UciOKCommand.Id) 34 | { 35 | break; 36 | } 37 | 38 | Assert.False(span.ContainsAny(incorrectValues), "No 'True' or 'False' or expected, only lowercase in: " + str); 39 | 40 | if (span.ContainsAny(correctValues)) 41 | { 42 | booleansDetected++; 43 | } 44 | } 45 | 46 | Debug.Assert(booleansDetected >= 4); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := publish 2 | 3 | EXE= 4 | 5 | RUNTIME= 6 | OUTPUT_DIR=artifacts/Lynx/ 7 | 8 | ifeq ($(OS),Windows_NT) 9 | ifeq ($(PROCESSOR_ARCHITEW6432),AMD64) 10 | RUNTIME=win-x64 11 | else ifeq ($(PROCESSOR_ARCHITECTURE),AMD64) 12 | RUNTIME=win-x64 13 | else ifeq ($(PROCESSOR_ARCHITEW6432),ARM64) 14 | RUNTIME=win-arm64 15 | else ifeq ($(PROCESSOR_ARCHITECTURE),ARM64) 16 | RUNTIME=win-arm64 17 | else 18 | RUNTIME=win-x86 19 | endif 20 | else 21 | UNAME_S := $(shell uname -s) 22 | UNAME_P := $(shell uname -p) 23 | ifeq ($(UNAME_S),Linux) 24 | RUNTIME=linux-x64 25 | ifneq ($(filter aarch64%,$(UNAME_P)),) 26 | RUNTIME=linux-arm64 27 | else ifneq ($(filter armv8%,$(UNAME_P)),) 28 | RUNTIME=linux-arm64 29 | else ifneq ($(filter arm%,$(UNAME_P)),) 30 | RUNTIME=linux-arm 31 | endif 32 | else ifneq ($(filter arm%,$(UNAME_P)),) 33 | RUNTIME=osx-arm64 34 | else 35 | RUNTIME=osx-x64 36 | endif 37 | endif 38 | 39 | ifndef RUNTIME 40 | $(error RUNTIME is not set for $(OS) $(UNAME_S) $(UNAME_P), please fill an issue in https://github.com/lynx-chess/Lynx/issues/new/choose) 41 | endif 42 | 43 | ifdef EXE 44 | OUTPUT_DIR=./ 45 | endif 46 | 47 | build: 48 | dotnet build -c Release 49 | 50 | test: 51 | dotnet test -c Release & dotnet test -c Release --filter "TestCategory=Configuration" & dotnet test -c Release --filter "TestCategory=LongRunning" & dotnet test -c Release --filter "TestCategory=Perft" 52 | 53 | publish: 54 | dotnet publish src/Lynx.Cli/Lynx.Cli.csproj --runtime ${RUNTIME} --self-contained /p:Optimized=true /p:ExecutableName=$(EXE) -o ${OUTPUT_DIR} 55 | 56 | run: 57 | dotnet run --project src/Lynx.Cli/Lynx.Cli.csproj -c Release --runtime ${RUNTIME} 58 | -------------------------------------------------------------------------------- /tests/Lynx.Test/Lynx.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | true 6 | $(InterceptorsPreviewNamespaces);Microsoft.Extensions.Configuration.Binder.SourceGeneration 7 | Test 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | $(NoWarn),LYNX0,CA1861,CS0618 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/Lynx.Test/Commands/StopCommandTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.UCI.Commands.GUI; 2 | using Moq; 3 | using NUnit.Framework; 4 | using System.Threading.Channels; 5 | 6 | namespace Lynx.Test.Commands; 7 | 8 | /// 9 | /// https://github.com/lynx-chess/Lynx/issues/31 10 | /// 11 | public class StopCommandTest 12 | { 13 | [TestCase(Constants.TrickyTestPositionFEN)] 14 | public async Task StopCommandShouldNotModifyPositionOrAddMoveToMoveHistory(string initialPositionFEN) 15 | { 16 | // Arrange 17 | var engine = new Engine(new Mock>().Object); 18 | engine.NewGame(); 19 | engine.AdjustPosition($"position fen {initialPositionFEN}"); 20 | 21 | // A command that guarantees that the search doesn't finish before the end of the test 22 | var goCommand = new GoCommand($"go depth {Configuration.EngineSettings.MaxDepth}"); 23 | var searchConstraints = TimeManager.CalculateTimeManagement(engine.Game, goCommand); 24 | using var cts = new CancellationTokenSource(); 25 | cts.CancelAfter(10_000); 26 | 27 | var resultTask = Task.Run(() => engine.Search(searchConstraints, isPondering: false, cts.Token, CancellationToken.None)); 28 | // Wait 2s so that there's some best move available 29 | await Task.Delay(2000); 30 | 31 | // Act 32 | await cts.CancelAsync(); 33 | 34 | // Assert 35 | var result = await resultTask; 36 | Assert.NotNull(result); 37 | Assert.AreNotEqual(default, result?.BestMove); 38 | 39 | Assert.AreEqual(initialPositionFEN, engine.Game.CurrentPosition.FEN()); 40 | 41 | #if DEBUG 42 | Assert.IsEmpty(engine.Game.MoveHistory); 43 | #endif 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Lynx.Test/PregeneratedAttacks/RookOccupancyTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | using BS = Lynx.Model.BoardSquare; 4 | 5 | namespace Lynx.Test.PregeneratedAttacks; 6 | 7 | public class RookOccupancyTest 8 | { 9 | [TestCase(BS.a8, new[] { BS.b8, BS.c8, BS.d8, BS.e8, BS.f8, BS.g8, BS.a2, BS.a3, BS.a4, BS.a5, BS.a6, BS.a7 })] 10 | [TestCase(BS.a1, new[] { BS.b1, BS.c1, BS.d1, BS.e1, BS.f1, BS.g1, BS.a2, BS.a3, BS.a4, BS.a5, BS.a6, BS.a7 })] 11 | [TestCase(BS.h8, new[] { BS.b8, BS.c8, BS.d8, BS.e8, BS.f8, BS.g8, BS.h2, BS.h3, BS.h4, BS.h5, BS.h6, BS.h7 })] 12 | [TestCase(BS.h1, new[] { BS.b1, BS.c1, BS.d1, BS.e1, BS.f1, BS.g1, BS.h2, BS.h3, BS.h4, BS.h5, BS.h6, BS.h7 })] 13 | 14 | [TestCase(BS.c6, new[] { BS.b6, BS.d6, BS.e6, BS.f6, BS.g6, BS.c7, BS.c5, BS.c4, BS.c3, BS.c2 })] 15 | [TestCase(BS.f6, new[] { BS.b6, BS.c6, BS.d6, BS.e6, BS.g6, BS.f7, BS.f5, BS.f4, BS.f3, BS.f2 })] 16 | [TestCase(BS.c3, new[] { BS.b3, BS.d3, BS.e3, BS.f3, BS.g3, BS.c7, BS.c6, BS.c5, BS.c4, BS.c2 })] 17 | [TestCase(BS.f3, new[] { BS.b3, BS.c3, BS.d3, BS.e3, BS.g3, BS.f7, BS.f6, BS.f5, BS.f4, BS.f2 })] 18 | 19 | [TestCase(BS.e4, new[] { BS.b4, BS.c4, BS.d4, BS.f4, BS.g4, BS.e7, BS.e6, BS.e5, BS.e3, BS.e2 })] 20 | [TestCase(BS.d4, new[] { BS.b4, BS.c4, BS.e4, BS.f4, BS.g4, BS.d7, BS.d6, BS.d5, BS.d3, BS.d2 })] 21 | public void MaskRookOccupancy(BS rookSquare, BS[] attackedSquares) 22 | { 23 | var attacks = AttackGenerator.MaskRookOccupancy((int)rookSquare); 24 | 25 | foreach (var attackedSquare in attackedSquares) 26 | { 27 | Assert.True(attacks.GetBit(attackedSquare)); 28 | attacks.PopBit(attackedSquare); 29 | } 30 | 31 | Assert.AreEqual(default(BitBoard), attacks); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/benchmarks.yml: -------------------------------------------------------------------------------- 1 | name: Benchmarks 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | benchmark_name: 7 | description: 'Benchmark name' 8 | default: '*' 9 | required: false 10 | 11 | jobs: 12 | 13 | benchmark: 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, windows-latest, macOS-latest, macOS-15-intel] 19 | fail-fast: false 20 | 21 | env: 22 | DOTNET_VERSION: 10.0.x 23 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 24 | 25 | steps: 26 | - uses: actions/checkout@v6 27 | 28 | - name: Setup .NET 29 | uses: actions/setup-dotnet@v5 30 | with: 31 | dotnet-version: ${{ env.DOTNET_VERSION }} 32 | 33 | - name: Nuget cache 34 | uses: actions/cache@v5 35 | with: 36 | path: 37 | ~/.nuget/packages 38 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} 39 | restore-keys: | 40 | ${{ runner.os }}-nuget- 41 | 42 | - name: Build 43 | run: dotnet build -c Release 44 | working-directory: ./src/Lynx.Benchmark 45 | 46 | - name: Run ${{ github.event.inputs.benchmark_name }} benchmark 47 | run: dotnet run -c Release --no-build --filter '${{ github.event.inputs.benchmark_name }}' 48 | working-directory: ./src/Lynx.Benchmark 49 | 50 | - name: 'Upload ${{ matrix.os }} artifacts' 51 | continue-on-error: false 52 | uses: actions/upload-artifact@v6 53 | with: 54 | name: artifacts-${{ matrix.os }}-${{ github.run_number }} 55 | # Can't include 'github.event.inputs.benchmark_name' in the name, since * is an invalid character 56 | path: ./src/Lynx.Benchmark/BenchmarkDotNet.Artifacts/results/ 57 | if-no-files-found: error 58 | -------------------------------------------------------------------------------- /tests/Lynx.Test/PregeneratedAttacks/BishopOccupancyTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | using BS = Lynx.Model.BoardSquare; 4 | 5 | namespace Lynx.Test.PregeneratedAttacks; 6 | 7 | public class BishopOccupancyTest 8 | { 9 | [TestCase(BS.a8, new[] { BS.b7, BS.c6, BS.d5, BS.e4, BS.f3, BS.g2 })] 10 | [TestCase(BS.h1, new[] { BS.b7, BS.c6, BS.d5, BS.e4, BS.f3, BS.g2 })] 11 | [TestCase(BS.a1, new[] { BS.b2, BS.c3, BS.d4, BS.e5, BS.f6, BS.g7 })] 12 | [TestCase(BS.h8, new[] { BS.b2, BS.c3, BS.d4, BS.e5, BS.f6, BS.g7 })] 13 | 14 | [TestCase(BS.b7, new[] { BS.c6, BS.d5, BS.e4, BS.f3, BS.g2 })] 15 | [TestCase(BS.g7, new[] { BS.f6, BS.e5, BS.d4, BS.c3, BS.b2 })] 16 | [TestCase(BS.b2, new[] { BS.c3, BS.d4, BS.e5, BS.f6, BS.g7 })] 17 | [TestCase(BS.g2, new[] { BS.f3, BS.e4, BS.d5, BS.c6, BS.b7 })] 18 | 19 | [TestCase(BS.c6, new[] { BS.b7, BS.d5, BS.e4, BS.f3, BS.g2, BS.d7, BS.b5 })] 20 | [TestCase(BS.f6, new[] { BS.g7, BS.e5, BS.d4, BS.c3, BS.b2, BS.e7, BS.g5 })] 21 | [TestCase(BS.c3, new[] { BS.b2, BS.d4, BS.e5, BS.f6, BS.g7, BS.b4, BS.d2 })] 22 | [TestCase(BS.f3, new[] { BS.g2, BS.e4, BS.d5, BS.c6, BS.b7, BS.g4, BS.e2 })] 23 | 24 | [TestCase(BS.e4, new[] { BS.b7, BS.c6, BS.d5, BS.f3, BS.g2, BS.c2, BS.d3, BS.f5, BS.g6 })] 25 | [TestCase(BS.d4, new[] { BS.g7, BS.f6, BS.e5, BS.c3, BS.b2, BS.b6, BS.c5, BS.e3, BS.f2 })] 26 | public void MaskBishopOccupancy(BS bishopSquare, BS[] attackedSquares) 27 | { 28 | var attacks = AttackGenerator.MaskBishopOccupancy((int)bishopSquare); 29 | 30 | foreach (var attackedSquare in attackedSquares) 31 | { 32 | Assert.True(attacks.GetBit(attackedSquare)); 33 | attacks.PopBit(attackedSquare); 34 | } 35 | 36 | Assert.AreEqual(default(BitBoard), attacks); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Lynx.Test/PregeneratedAttacks/SetBishopOrRookOccupancyTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | 4 | namespace Lynx.Test.PregeneratedAttacks; 5 | 6 | public class SetBishopOrRookOccupancyTest 7 | { 8 | [Test] 9 | public void SetBishopOccupancy() 10 | { 11 | // Arrange 12 | var occupancyMask = AttackGenerator.MaskBishopOccupancy((int)BoardSquare.d4); 13 | var maxIndex = (int)Math.Pow(2, occupancyMask.CountBits()) - 1; 14 | 15 | // Act 16 | var occupancy = AttackGenerator.SetBishopOrRookOccupancy(0, occupancyMask); 17 | // Assert 18 | Assert.True(occupancy.Empty()); 19 | 20 | // Act 21 | occupancy = AttackGenerator.SetBishopOrRookOccupancy(maxIndex, occupancyMask); 22 | // Assert 23 | Assert.AreEqual(occupancyMask, occupancy); 24 | } 25 | 26 | [TestCase(BoardSquare.a8)] 27 | [TestCase(BoardSquare.a7)] 28 | public void SetRookOccupancy(BoardSquare rookSquare) 29 | { 30 | // Arrange 31 | var occupancyMask = AttackGenerator.MaskRookOccupancy((int)rookSquare); 32 | var maxIndex = (int)Math.Pow(2, occupancyMask.CountBits()) - 1; 33 | 34 | // Act - empty board 35 | var occupancy = AttackGenerator.SetBishopOrRookOccupancy(0, occupancyMask); 36 | // Assert 37 | Assert.True(occupancy.Empty()); 38 | 39 | // Act - top rank occupied 40 | var index = (int)Math.Pow(2, 6) - 1; 41 | occupancy = AttackGenerator.SetBishopOrRookOccupancy(index, occupancyMask); 42 | // Assert 43 | Assert.AreEqual(0b01111110UL << 8 * ((int)rookSquare / 8), occupancy); 44 | 45 | // Act - max occupancy 46 | occupancy = AttackGenerator.SetBishopOrRookOccupancy(maxIndex, occupancyMask); 47 | // Assert 48 | Assert.AreEqual(occupancyMask, occupancy); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Lynx.Test/BaseTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using Moq; 3 | using System.Threading.Channels; 4 | 5 | namespace Lynx.Test; 6 | 7 | public abstract class BaseTest 8 | { 9 | protected const int DefaultSearchDepth = 10; 10 | 11 | protected BaseTest() 12 | { 13 | Configuration.EngineSettings.TranspositionTableSize = 32; 14 | } 15 | 16 | protected static SearchResult TestBestMove(string fen, string[]? allowedUCIMoveString, string[]? excludedUCIMoveString, int depth = DefaultSearchDepth) 17 | { 18 | var seachResult = SearchBestMove(fen, depth); 19 | var bestMoveFound = seachResult.BestMove; 20 | 21 | if (allowedUCIMoveString is not null) 22 | { 23 | Assert.Contains(bestMoveFound.UCIString(), allowedUCIMoveString); 24 | } 25 | 26 | if (excludedUCIMoveString is not null) 27 | { 28 | Assert.False(excludedUCIMoveString.Contains(bestMoveFound.UCIString())); 29 | } 30 | 31 | return seachResult; 32 | } 33 | 34 | protected static SearchResult SearchBestMove(string fen, int depth = DefaultSearchDepth) 35 | { 36 | var engine = GetEngine(fen); 37 | return engine.BestMove(new($"go depth {depth}")); 38 | } 39 | 40 | protected static Engine GetEngine(string fen) 41 | { 42 | var engine = GetEngine(); 43 | 44 | return SetEnginePosition(engine, fen); 45 | } 46 | 47 | protected static Engine GetEngine() 48 | { 49 | var mock = new Mock>(); 50 | 51 | mock 52 | .Setup(m => m.WriteAsync(It.IsAny(), It.IsAny())) 53 | .Returns(ValueTask.CompletedTask); 54 | 55 | return new Engine(mock.Object); 56 | } 57 | 58 | protected static Engine SetEnginePosition(Engine engine, string fen) 59 | { 60 | engine.SetGame(new Game(fen)); 61 | 62 | return engine; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/GUI/PositionCommand.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NLog; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace Lynx.UCI.Commands.GUI; 6 | 7 | /// 8 | /// position[fen | startpos] moves .... 9 | /// set up the position described in fenstring on the internal board and 10 | /// play the moves on the internal chess board. 11 | /// if the game was played from the start position the string "startpos" will be sent 12 | /// Note: no "new" command is needed. However, if this position is from a different game than 13 | /// the last position sent to the engine, the GUI should have sent a "ucinewgame" inbetween. 14 | /// 15 | public sealed class PositionCommand 16 | { 17 | public const string Id = "position"; 18 | 19 | public const string StartPositionString = "startpos"; 20 | public const string MovesString = "moves"; 21 | 22 | private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); 23 | 24 | public static bool TryParseLastMove(string positionCommand, Game game, [NotNullWhen(true)] out Move? lastMove) 25 | { 26 | var moveString = positionCommand 27 | .Split(' ', StringSplitOptions.RemoveEmptyEntries)[^1]; 28 | 29 | Span movePool = stackalloc Move[Constants.MaxNumberOfPseudolegalMovesInAPosition]; 30 | 31 | Span attacks = stackalloc BitBoard[12]; 32 | Span attacksBySide = stackalloc BitBoard[2]; 33 | var evaluationContext = new EvaluationContext(attacks, attacksBySide); 34 | 35 | if (!MoveExtensions.TryParseFromUCIString( 36 | moveString, 37 | MoveGenerator.GenerateAllMoves(game.CurrentPosition, ref evaluationContext, movePool), 38 | out lastMove)) 39 | { 40 | _logger.Warn("Error parsing last move {0} from position command {1}", lastMove, positionCommand); 41 | return false; 42 | } 43 | 44 | return true; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Lynx/Model/GameState.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.Model; 2 | 3 | #pragma warning disable CA1051 // Do not declare visible instance fields 4 | 5 | public readonly struct GameState 6 | { 7 | public readonly ulong ZobristKey; 8 | 9 | public readonly ulong KingPawnKey; 10 | 11 | public readonly ulong NonPawnWhiteKey; 12 | 13 | public readonly ulong NonPawnBlackKey; 14 | 15 | public readonly ulong MinorKey; 16 | 17 | public readonly ulong MajorKey; 18 | 19 | public readonly int IncrementalEvalAccumulator; 20 | 21 | public readonly int IncrementalPhaseAccumulator; 22 | 23 | public readonly BoardSquare EnPassant; 24 | 25 | public readonly byte Castle; 26 | 27 | public readonly bool IsIncrementalEval; 28 | 29 | public GameState(Position position) 30 | { 31 | ZobristKey = position.UniqueIdentifier; 32 | 33 | KingPawnKey = position.KingPawnUniqueIdentifier; 34 | NonPawnWhiteKey = position.NonPawnHash[(int)Side.White]; 35 | NonPawnBlackKey = position.NonPawnHash[(int)Side.Black]; 36 | MinorKey = position.MinorHash; 37 | MajorKey = position.MajorHash; 38 | 39 | EnPassant = position.EnPassant; 40 | Castle = position.Castle; 41 | IncrementalEvalAccumulator = position.IncrementalEvalAccumulator; 42 | IncrementalPhaseAccumulator = position.IncrementalPhaseAccumulator; 43 | 44 | // We also save a copy of _isIncrementalEval, so that current move doesn't affect 'sibling' moves exploration 45 | IsIncrementalEval = position.IsIncrementalEval; 46 | } 47 | } 48 | 49 | public readonly struct NullMoveGameState 50 | { 51 | public readonly ulong ZobristKey; 52 | 53 | public readonly BoardSquare EnPassant; 54 | 55 | public NullMoveGameState(Position position) 56 | { 57 | ZobristKey = position.UniqueIdentifier; 58 | EnPassant = position.EnPassant; 59 | } 60 | } 61 | 62 | #pragma warning restore CA1051 // Do not declare visible instance fields 63 | -------------------------------------------------------------------------------- /tests/Lynx.Test/BestMove/QuiescenceTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace Lynx.Test.BestMove; 4 | 5 | public class QuiescenceTest : BaseTest 6 | { 7 | [TestCase("r2qkb1r/ppp2ppp/2n2n2/1B1p1b2/3P4/2N2N2/PPP2PPP/R1BQ1RK1 b kq - 0 1", 1, 12, 8 | null, 9 | new[] { "f5c2", "f5d3", "f5h3", "f8c5", "f8a3" }, 10 | Description = "Avoid trading pawn for minor piece or sacrificing pieces for nothing")] 11 | [TestCase("r2qkb1r/1pp2ppp/p1n2n2/1B1p1b2/3P4/2N2N2/PPP2PPP/R1BQ1RK1 w kq - 0 2", 1, 12, 12 | new[] { "b5c6", "b5a4", "f1e1", "f3h4", "d1e1", "b5d3" }, 13 | new[] { "b5c1", "c3d5" }, 14 | Description = "Originally, it captured in c1")] 15 | [TestCase("r1bq1b1r/ppppk2p/2n1pp2/3n2B1/3P4/P4N2/1PP2PPP/RN1QKB1R w KQ - 0 1", 1, 12, 16 | new[] { "g5h4", "g5e3", "g5d2", "g5c1", "c2c4" }, 17 | new[] { "a3a4" }, 18 | Description = "Avoid allowing pieces to be captured")] 19 | [TestCase("2kr3q/pbppppp1/1p1P3r/4bB2/1n2n1Q1/8/PPPPNBPP/R4RK1 b Q - 0 1", 3, 12, 20 | new[] { "e5h2" }, 21 | Description = "Mate in 6 with quiescence, https://gameknot.com/chess-puzzle.pl?pz=257112", 22 | Ignore = "Fails after fixing LMR implementation")] 23 | #pragma warning disable RCS1163, IDE0060 // Unused parameter. 24 | public void Quiescence(string fen, int depth, int minQuiescenceSearchDepth, string[]? allowedUCIMoveString, string[]? excludedUCIMoveString = null) 25 | #pragma warning restore RCS1163, IDE0060 // Unused parameter. 26 | { 27 | TestBestMove(fen, allowedUCIMoveString, excludedUCIMoveString, depth); 28 | } 29 | 30 | [TestCase("7k/8/5NQ1/8/8/KN6/8/1r6 b - - 0 1", 1, new[] { "b1b3" })] 31 | [TestCase("5Rbk/8/5N2/6Q1/8/1r6/8/KN6 b - - 0 1", 1, new[] { "b3b1" })] 32 | public void DetectDrawWhenNoCaptures(string fen, int depth, string[]? allowedUCIMoveString, string[]? excludedUCIMoveString = null) 33 | { 34 | TestBestMove(fen, allowedUCIMoveString, excludedUCIMoveString, depth); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Lynx.Test/MoveGeneration/RegressionTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | 4 | namespace Lynx.Test.MoveGeneration; 5 | public class MoveGeneratorRegressionTest : BaseTest 6 | { 7 | [Test] 8 | public void AllMovesAreGenerated() 9 | { 10 | var position = new Position("r3k2r/pP1pqpb1/bn2pnp1/2pPN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq c6 0 1"); 11 | 12 | var moves = MoveGenerator.GenerateAllMoves(position).ToList(); 13 | 14 | Assert.True(moves.Exists(m => m.IsShortCastle())); 15 | Assert.True(moves.Exists(m => m.IsLongCastle())); 16 | Assert.True(moves.Exists(m => m.IsEnPassant())); 17 | Assert.True(moves.Exists(m => m.PromotedPiece() != default)); 18 | Assert.True(moves.Exists(m => m.PromotedPiece() != default && m.CapturedPiece() != (int)Piece.None)); 19 | Assert.True(moves.Exists(m => m.PromotedPiece() != default && m.CapturedPiece() == (int)Piece.None)); 20 | Assert.True(moves.Exists(m => m.IsDoublePawnPush())); 21 | 22 | Span moveSpan = stackalloc Move[Constants.MaxNumberOfPseudolegalMovesInAPosition]; 23 | Span attacks = stackalloc BitBoard[12]; 24 | Span attacksBySide = stackalloc BitBoard[2]; 25 | var evaluationContext = new EvaluationContext(attacks, attacksBySide); 26 | 27 | var captures = MoveGenerator.GenerateAllCaptures(position, ref evaluationContext, moveSpan).ToArray().ToList(); 28 | 29 | Assert.True(moves.Exists(m => m.IsShortCastle())); 30 | Assert.True(moves.Exists(m => m.IsLongCastle())); 31 | Assert.True(captures.Exists(m => m.IsEnPassant())); 32 | Assert.True(captures.Exists(m => m.PromotedPiece() != default)); 33 | Assert.True(captures.Exists(m => m.PromotedPiece() != default && m.CapturedPiece() != (int)Piece.None)); 34 | Assert.True(captures.Exists(m => m.PromotedPiece() != default && m.CapturedPiece() == (int)Piece.None)); 35 | Assert.False(captures.Exists(m => m.IsDoublePawnPush())); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Lynx/WDL.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx; 2 | 3 | using static EvaluationConstants; 4 | 5 | public static class WDL 6 | { 7 | /// 8 | /// Adjust score so that 100cp == 50% win probability 9 | /// Based on https://github.com/Ciekce/Stormphrax/blob/main/src/wdl.h 10 | /// 11 | public static int NormalizeScore(int score) 12 | { 13 | return (score == 0 || score > PositiveCheckmateDetectionLimit || score < NegativeCheckmateDetectionLimit) 14 | 15 | ? score 16 | : score * 100 / EvalNormalizationCoefficient; 17 | } 18 | 19 | /// 20 | /// Based on https://github.com/Ciekce/Stormphrax/blob/main/src/wdl.h 21 | /// 22 | public static int UnNormalizeScore(int normalizedScore) 23 | { 24 | return (normalizedScore == 0 || normalizedScore > PositiveCheckmateDetectionLimit || normalizedScore < NegativeCheckmateDetectionLimit) 25 | ? normalizedScore 26 | : normalizedScore * EvalNormalizationCoefficient / 100; 27 | } 28 | 29 | /// 30 | /// Based on https://github.com/Ciekce/Stormphrax/blob/main/src/wdl.cpp and https://github.com/official-stockfish/Stockfish/blob/master/src/uci.cpp 31 | /// 32 | public static (int WDLWin, int WDLDraw, int WDLLoss) WDLModel(int score, int ply) 33 | { 34 | // The model only captures up to 240 plies, so limit the input and then rescale 35 | double m = Math.Min(240, ply) / 64.0; 36 | 37 | double a = (((((As[0] * m) + As[1]) * m) + As[2]) * m) + As[3]; 38 | double b = (((((Bs[0] * m) + Bs[1]) * m) + Bs[2]) * m) + Bs[3]; 39 | 40 | // Transform the eval to centipawns with limited range 41 | double x = Math.Clamp(score, -4000.0, 4000.0); 42 | 43 | int wdlWin = (int)Math.Round(1000.0 / (1.0 + Math.Exp((a - x) / b))); 44 | int wdlLoss = (int)Math.Round(1000.0 / (1.0 + Math.Exp((a + x) / b))); 45 | int wdlDraw = 1000 - wdlWin - wdlLoss; 46 | 47 | return (wdlWin, wdlDraw, wdlLoss); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Lynx.Test/Commands/GoCommandTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.UCI.Commands.GUI; 2 | using NUnit.Framework; 3 | 4 | namespace Lynx.Test.Commands; 5 | 6 | public class GoCommandTest 7 | { 8 | [TestCase] 9 | public void ParseGoCommand() 10 | { 11 | const string goCommandString = "go infinite wtime 10 btime 20 winc 30 binc 40 movestogo 50 movetime 70 mate 80 nodes 90 depth 60 ponder "; 12 | 13 | var goCommand = new GoCommand(goCommandString); 14 | 15 | Assert.AreEqual(10, goCommand.WhiteTime); 16 | Assert.AreEqual(20, goCommand.BlackTime); 17 | Assert.AreEqual(30, goCommand.WhiteIncrement); 18 | Assert.AreEqual(40, goCommand.BlackIncrement); 19 | Assert.AreEqual(50, goCommand.MovesToGo); 20 | Assert.AreEqual(60, goCommand.Depth); 21 | Assert.AreEqual(70, goCommand.MoveTime); 22 | Assert.True(goCommand.Ponder); 23 | _ = Assert.Throws(() => _ = GoCommand.Mate); 24 | _ = Assert.Throws(() => _ = GoCommand.Nodes); 25 | _ = Assert.Throws(() => _ = GoCommand.SearchMoves); 26 | } 27 | 28 | [TestCase("go wtime 10 btime 20 winc 30 binc 40 ponder")] 29 | [TestCase("go ponder wtime 10 btime 20 winc 30 binc 40")] 30 | [TestCase("go wtime 10 btime 20 ponder winc 30 binc 40")] 31 | [TestCase("go wtime 10 btime 20 winc 30 ponder binc 40")] 32 | [TestCase("go btime 20 wtime 10 winc 30 binc 40 ponder")] 33 | [TestCase("go winc 30 btime 20 wtime 10 binc 40 ponder")] 34 | [TestCase("go binc 40 winc 30 btime 20 wtime 10 ponder")] 35 | [TestCase("go ponder binc 40 winc 30 btime 20 wtime 10")] 36 | public void ParseGoCommandUnordered(string goCommandString) 37 | { 38 | var goCommand = new GoCommand(goCommandString); 39 | 40 | Assert.AreEqual(10, goCommand.WhiteTime); 41 | Assert.AreEqual(20, goCommand.BlackTime); 42 | Assert.AreEqual(30, goCommand.WhiteIncrement); 43 | Assert.AreEqual(40, goCommand.BlackIncrement); 44 | 45 | Assert.True(goCommand.Ponder); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Lynx.Test/Model/BoardSquareTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | 4 | namespace Lynx.Test.Model; 5 | public class BoardSquareTest : BaseTest 6 | { 7 | [TestCase(BoardSquare.a1, BoardSquare.a3)] 8 | [TestCase(BoardSquare.a1, BoardSquare.a5)] 9 | [TestCase(BoardSquare.a1, BoardSquare.a7)] 10 | [TestCase(BoardSquare.a1, BoardSquare.c1)] 11 | [TestCase(BoardSquare.a1, BoardSquare.e1)] 12 | [TestCase(BoardSquare.a1, BoardSquare.g1)] 13 | [TestCase(BoardSquare.a1, BoardSquare.b2)] 14 | [TestCase(BoardSquare.a1, BoardSquare.c3)] 15 | [TestCase(BoardSquare.a1, BoardSquare.d4)] 16 | [TestCase(BoardSquare.a1, BoardSquare.e5)] 17 | [TestCase(BoardSquare.a1, BoardSquare.f6)] 18 | [TestCase(BoardSquare.a1, BoardSquare.g7)] 19 | [TestCase(BoardSquare.a1, BoardSquare.h8)] 20 | public void SameColor(BoardSquare square1, BoardSquare square2) 21 | { 22 | Assert.True(BoardSquareExtensions.SameColor((int)square1, (int)square2)); 23 | Assert.False(BoardSquareExtensions.DifferentColor((int)square1, (int)square2)); 24 | } 25 | 26 | [TestCase(BoardSquare.a1, BoardSquare.a2)] 27 | [TestCase(BoardSquare.a1, BoardSquare.a4)] 28 | [TestCase(BoardSquare.a1, BoardSquare.a6)] 29 | [TestCase(BoardSquare.a1, BoardSquare.a8)] 30 | [TestCase(BoardSquare.a1, BoardSquare.b1)] 31 | [TestCase(BoardSquare.a1, BoardSquare.d1)] 32 | [TestCase(BoardSquare.a1, BoardSquare.f1)] 33 | [TestCase(BoardSquare.a1, BoardSquare.h1)] 34 | [TestCase(BoardSquare.a1, BoardSquare.b3)] 35 | [TestCase(BoardSquare.a1, BoardSquare.c4)] 36 | [TestCase(BoardSquare.a1, BoardSquare.d5)] 37 | [TestCase(BoardSquare.a1, BoardSquare.e6)] 38 | [TestCase(BoardSquare.a1, BoardSquare.f7)] 39 | [TestCase(BoardSquare.a1, BoardSquare.g8)] 40 | [TestCase(BoardSquare.a1, BoardSquare.h7)] 41 | public void DifferentColor(BoardSquare square1, BoardSquare square2) 42 | { 43 | Assert.True(BoardSquareExtensions.DifferentColor((int)square1, (int)square2)); 44 | Assert.False(BoardSquareExtensions.SameColor((int)square1, (int)square2)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Lynx.Test/PregeneratedAttacks/KingAttacksTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | using BS = Lynx.Model.BoardSquare; 4 | 5 | namespace Lynx.Test.PregeneratedAttacks; 6 | 7 | public class KingAttacksTest 8 | { 9 | [TestCase(BS.a8, new[] { BS.b8, BS.a7, BS.b7 })] 10 | [TestCase(BS.h8, new[] { BS.g8, BS.g7, BS.h7 })] 11 | [TestCase(BS.a1, new[] { BS.a2, BS.b2, BS.b1 })] 12 | [TestCase(BS.h1, new[] { BS.g2, BS.h2, BS.g1 })] 13 | 14 | [TestCase(BS.b8, new[] { BS.a8, BS.c8, BS.a7, BS.b7, BS.c7 })] 15 | [TestCase(BS.g8, new[] { BS.f8, BS.h8, BS.f7, BS.g7, BS.h7 })] 16 | [TestCase(BS.b1, new[] { BS.a1, BS.c1, BS.a2, BS.b2, BS.c2 })] 17 | [TestCase(BS.g1, new[] { BS.f1, BS.h1, BS.f2, BS.g2, BS.h2 })] 18 | 19 | [TestCase(BS.a7, new[] { BS.a8, BS.b8, BS.b7, BS.a6, BS.b6 })] 20 | [TestCase(BS.h7, new[] { BS.g8, BS.h8, BS.g7, BS.g6, BS.h6 })] 21 | [TestCase(BS.a2, new[] { BS.a3, BS.b3, BS.b2, BS.a1, BS.b1 })] 22 | [TestCase(BS.h2, new[] { BS.g3, BS.h3, BS.g2, BS.g1, BS.h1 })] 23 | 24 | [TestCase(BS.e4, new[] { BS.d5, BS.e5, BS.f5, BS.d4, BS.f4, BS.d3, BS.e3, BS.f3 })] 25 | [TestCase(BS.e5, new[] { BS.d6, BS.e6, BS.f6, BS.d5, BS.f5, BS.d4, BS.e4, BS.f4 })] 26 | [TestCase(BS.d4, new[] { BS.c5, BS.d5, BS.e5, BS.c4, BS.e4, BS.c3, BS.d3, BS.e3 })] 27 | [TestCase(BS.d5, new[] { BS.c6, BS.d6, BS.e6, BS.c5, BS.e5, BS.c4, BS.d4, BS.e4 })] 28 | public void MaskKingAttacks(BS kingSquare, BS[] attackedSquares) 29 | { 30 | var attacks = AttackGenerator.MaskKingAttacks((int)kingSquare); 31 | ValidateAttacks(attackedSquares, attacks); 32 | 33 | attacks = Attacks.KingAttacks[(int)kingSquare]; 34 | ValidateAttacks(attackedSquares, attacks); 35 | 36 | static void ValidateAttacks(BS[] attackedSquares, BitBoard attacks) 37 | { 38 | foreach (var attackedSquare in attackedSquares) 39 | { 40 | Assert.True(attacks.GetBit(attackedSquare)); 41 | attacks.PopBit(attackedSquare); 42 | } 43 | 44 | Assert.AreEqual(default(BitBoard), attacks); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Lynx.Test/WDLTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace Lynx.Test; 4 | 5 | [Explicit] 6 | [Category(Categories.Configuration)] 7 | [NonParallelizable] 8 | public class WDLTest 9 | { 10 | /// 11 | /// Enforce that NormalizeToPawnValue corresponds to a 50% win rate at ply 64 12 | /// 13 | [Test] 14 | public void NormalizeCoefficientAndArrayValues() 15 | { 16 | var sum = (int)EvaluationConstants.As.ToArray().Sum(); 17 | Assert.True(EvaluationConstants.EvalNormalizationCoefficient == sum 18 | || EvaluationConstants.EvalNormalizationCoefficient == sum + 1 19 | || EvaluationConstants.EvalNormalizationCoefficient == sum - 1); 20 | } 21 | 22 | [TestCase(500)] 23 | [TestCase(1000)] 24 | public void NormalizeScore(int score) 25 | { 26 | Assert.AreEqual(score * 100 / EvaluationConstants.EvalNormalizationCoefficient, WDL.NormalizeScore(score)); 27 | } 28 | 29 | [TestCase(0, 0)] 30 | [TestCase(EvaluationConstants.PositiveCheckmateDetectionLimit + 5, EvaluationConstants.PositiveCheckmateDetectionLimit + 5)] 31 | [TestCase(EvaluationConstants.NegativeCheckmateDetectionLimit - 5, EvaluationConstants.NegativeCheckmateDetectionLimit - 5)] 32 | public void NormalizeScore(int score, int expectecNormalizedEval) 33 | { 34 | Assert.AreEqual(expectecNormalizedEval, WDL.NormalizeScore(score)); 35 | } 36 | 37 | [TestCase(1000)] 38 | [TestCase(2000)] 39 | public void UnNormalizeScore(int score) 40 | { 41 | Assert.AreEqual(score * EvaluationConstants.EvalNormalizationCoefficient / 100, WDL.UnNormalizeScore(score)); 42 | } 43 | 44 | [TestCase(0, 0)] 45 | [TestCase(EvaluationConstants.PositiveCheckmateDetectionLimit + 5, EvaluationConstants.PositiveCheckmateDetectionLimit + 5)] 46 | [TestCase(EvaluationConstants.NegativeCheckmateDetectionLimit - 5, EvaluationConstants.NegativeCheckmateDetectionLimit - 5)] 47 | public void UnNormalizeScore(int score, int expectecNormalizedEval) 48 | { 49 | Assert.AreEqual(expectecNormalizedEval, WDL.UnNormalizeScore(score)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | preview 6 | Enable 7 | enable 8 | true 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 1.11.0 26 | Eduardo Cáceres 27 | $(MSBuildThisFileDirectory)resources\icon.ico 28 | https://github.com/lynx-chess/Lynx 29 | git 30 | 31 | 32 | 33 | true 34 | true 35 | 36 | 37 | 38 | LYNX_RELEASE 39 | 40 | 41 | 42 | $(NoWarn),1591,S101,S104,S103,S107,S109,S125,S1067,S1135,S1659,S2148,S3903,S4041,S1133,IDE0290,RCS1090,CA1031,CA1045,CA1062,CA1505,CA1724,CA1815,CA1819,CA2007,VSTHRD200,IDE0290 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/Lynx/Model/TablebaseResult.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using System.Runtime.Serialization; 3 | 4 | namespace Lynx.Model; 5 | 6 | #pragma warning disable S4022 // Enumerations should have "Int32" storage 7 | public enum TablebaseEvaluationCategory : byte 8 | #pragma warning restore S4022 // Enumerations should have "Int32" storage 9 | { 10 | Unknown, 11 | Draw, 12 | Win, 13 | Loss, 14 | [EnumMember(Value = "cursed-win")] 15 | CursedWin, 16 | [EnumMember(Value = "blessed-loss")] 17 | BlessedLoss, 18 | [EnumMember(Value = "maybe-win")] 19 | MaybeWin, 20 | [EnumMember(Value = "maybe-loss")] 21 | MaybeLoss, 22 | Cancelled = byte.MaxValue 23 | } 24 | 25 | public record class TablebaseEvaluation() 26 | { 27 | public TablebaseEvaluationCategory Category { get; init; } 28 | 29 | [JsonPropertyName("dtm")] 30 | public int? DistanceToMate { get; init; } 31 | 32 | [JsonPropertyName("dtz")] 33 | public int? DistanceToZero { get; init; } 34 | [JsonPropertyName("insufficient_material")] 35 | public bool IsInsufficientMaterial { get; init; } 36 | 37 | [JsonPropertyName("checkmate")] 38 | public bool IsCheckmate { get; init; } 39 | 40 | [JsonPropertyName("stalemate")] 41 | public bool IsStalemate { get; init; } 42 | 43 | #pragma warning disable CA1002 // Do not expose generic lists 44 | public List? Moves { get; init; } 45 | #pragma warning restore CA1002 // Do not expose generic lists 46 | } 47 | 48 | public record class TablebaseEvalMove() 49 | { 50 | public string Uci { get; init; } = string.Empty; 51 | 52 | public TablebaseEvaluationCategory Category { get; init; } 53 | 54 | [JsonPropertyName("dtm")] 55 | public int? DistanceToMate { get; init; } 56 | 57 | [JsonPropertyName("dtz")] 58 | public int? DistanceToZero { get; init; } 59 | } 60 | 61 | [JsonSourceGenerationOptions( 62 | GenerationMode = JsonSourceGenerationMode.Default)] // https://github.com/dotnet/runtime/issues/78602#issuecomment-1322004254 63 | [JsonSerializable(typeof(TablebaseEvaluation))] 64 | internal sealed partial class SourceGenerationContext : JsonSerializerContext; -------------------------------------------------------------------------------- /.github/workflows/perft.yml: -------------------------------------------------------------------------------- 1 | name: Perft 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | fen: 7 | description: 'fen' 8 | required: true 9 | default: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' 10 | depth: 11 | description: 'depth' 12 | required: true 13 | divide: 14 | description: 'Run also divide' 15 | required: false 16 | default: false 17 | type: boolean 18 | 19 | env: 20 | DOTNET_VERSION: 10.0.x 21 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 22 | 23 | jobs: 24 | perft: 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - uses: actions/checkout@v6 29 | 30 | - name: Setup .NET 31 | uses: actions/setup-dotnet@v5 32 | with: 33 | dotnet-version: ${{ env.DOTNET_VERSION }} 34 | 35 | - name: Nuget cache 36 | uses: actions/cache@v5 37 | with: 38 | path: 39 | ~/.nuget/packages 40 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} 41 | restore-keys: | 42 | ${{ runner.os }}-nuget- 43 | 44 | - name: Build 45 | run: dotnet build -c Release 46 | working-directory: ./src/Lynx.Cli 47 | 48 | - name: Run perft ${{ github.event.inputs.depth }} on ${{ github.event.inputs.fen }} 49 | run: dotnet run -c Release --no-build "position fen ${{ github.event.inputs.fen }}" "perft ${{ github.event.inputs.depth }}" "quit" 50 | working-directory: ./src/Lynx.Cli 51 | 52 | divide: 53 | runs-on: ubuntu-latest 54 | if: github.event.inputs.divide 55 | 56 | steps: 57 | - uses: actions/checkout@v6 58 | 59 | - name: Setup .NET 60 | uses: actions/setup-dotnet@v5 61 | with: 62 | dotnet-version: ${{ env.DOTNET_VERSION }} 63 | 64 | - name: Nuget cache 65 | uses: actions/cache@v5 66 | with: 67 | path: 68 | ~/.nuget/packages 69 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} 70 | restore-keys: | 71 | ${{ runner.os }}-nuget- 72 | 73 | - name: Build 74 | run: dotnet build -c Release 75 | 76 | - name: Run divide ${{ github.event.inputs.depth }} on ${{ github.event.inputs.fen }} 77 | run: dotnet run -c Release --no-build "position fen ${{ github.event.inputs.fen }}" "divide ${{ github.event.inputs.depth }}" "quit" 78 | working-directory: ./src/Lynx.Cli 79 | -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/GUI/RegisterCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Lynx.UCI.Commands.GUI; 4 | 5 | /// 6 | /// register 7 | /// this is the command to try to register an engine or to tell the engine that registration 8 | /// will be done later. This command should always be sent if the engine has send "registration error" 9 | /// at program startup. 10 | /// The following tokens are allowed: 11 | /// * later 12 | /// the user doesn't want to register the engine now 13 | /// * name 14 | /// the engine should be registered with the name 15 | /// * code 16 | /// the engine should be registered with the code 17 | /// Example: 18 | /// "register later" 19 | /// "register name Stefan MK code 4359874324" 20 | /// 21 | public sealed class RegisterCommand 22 | { 23 | public const string Id = "register"; 24 | 25 | public bool Later { get; } 26 | 27 | public string Name { get; } = string.Empty; 28 | 29 | public string Code { get; } = string.Empty; 30 | 31 | public RegisterCommand(ReadOnlySpan command) 32 | { 33 | const string later = "later"; 34 | const string name = "name"; 35 | const string code = "code"; 36 | 37 | Span items = stackalloc Range[6]; 38 | var itemsLength = command.Split(items, ' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); 39 | 40 | if (command[items[1]].Equals(later, StringComparison.OrdinalIgnoreCase)) 41 | { 42 | Later = true; 43 | return; 44 | } 45 | 46 | var sb = new StringBuilder(); 47 | 48 | for (int i = 1; i < itemsLength; ++i) 49 | { 50 | var item = command[items[i]]; 51 | if (item.Equals(name, StringComparison.OrdinalIgnoreCase)) 52 | { 53 | Code = sb.ToString(); 54 | sb.Clear(); 55 | } 56 | else if (item.Equals(code, StringComparison.OrdinalIgnoreCase)) 57 | { 58 | Name = sb.ToString(); 59 | sb.Clear(); 60 | } 61 | else 62 | { 63 | sb.Append(item); 64 | sb.Append(' '); 65 | } 66 | } 67 | 68 | if (string.IsNullOrEmpty(Name)) 69 | { 70 | Name = sb.ToString(); 71 | } 72 | else 73 | { 74 | Code = sb.ToString(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Lynx.Benchmark/UCI_Benchmark.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * BenchmarkDotNet v0.13.11, Ubuntu 22.04.3 LTS (Jammy Jellyfish) 3 | * AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores 4 | * .NET SDK 8.0.100 5 | * [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 6 | * DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 7 | * 8 | * | Method | Mean | Error | StdDev | Allocated | 9 | * |------------------- |---------:|---------:|---------:|----------:| 10 | * | Bench_DefaultDepth | 512.4 ms | 10.17 ms | 28.18 ms | 265.16 MB | 11 | * 12 | * 13 | * BenchmarkDotNet v0.13.11, Windows 10 (10.0.20348.2113) (Hyper-V) 14 | * AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores 15 | * .NET SDK 8.0.100 16 | * [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 17 | * DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 18 | * 19 | * | Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | 20 | * |------------------- |---------:|--------:|---------:|----------:|----------:|----------:|----------:| 21 | * | Bench_DefaultDepth | 410.0 ms | 8.12 ms | 16.03 ms | 5000.0000 | 1000.0000 | 1000.0000 | 265.18 MB | 22 | * 23 | * 24 | * BenchmarkDotNet v0.13.11, macOS Monterey 12.6.9 (21G726) [Darwin 21.6.0] 25 | * Intel Core i7-8700B CPU 3.20GHz (Max: 3.19GHz) (Coffee Lake), 1 CPU, 4 logical and 4 physical cores 26 | * .NET SDK 8.0.100 27 | * [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 28 | * DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 29 | * 30 | * | Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | 31 | * |------------------- |--------:|---------:|---------:|-----------:|----------:|----------:|----------:| 32 | * | Bench_DefaultDepth | 1.139 s | 0.1229 s | 0.3526 s | 13000.0000 | 2000.0000 | 1000.0000 | 265.21 MB | 33 | */ 34 | 35 | using BenchmarkDotNet.Attributes; 36 | using System.Threading.Channels; 37 | 38 | namespace Lynx.Benchmark; 39 | 40 | public class UCI_Benchmark : BaseBenchmark 41 | { 42 | private readonly Channel _channel = Channel.CreateBounded(new BoundedChannelOptions(100_000) { SingleReader = true, SingleWriter = false }); 43 | 44 | [Benchmark] 45 | public (ulong, ulong) Bench_DefaultDepth() 46 | { 47 | var engine = new Engine(_channel.Writer); 48 | return engine.Bench(Configuration.EngineSettings.BenchDepth); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Lynx.Cli/Listener.cs: -------------------------------------------------------------------------------- 1 | using NLog; 2 | using System.Text; 3 | 4 | namespace Lynx.Cli; 5 | 6 | public sealed class Listener 7 | { 8 | private readonly Logger _logger; 9 | private readonly UCIHandler _uciHandler; 10 | 11 | public Listener(UCIHandler uCIHandler) 12 | { 13 | _uciHandler = uCIHandler; 14 | _logger = LogManager.GetCurrentClassLogger(); 15 | } 16 | 17 | public async Task Run(CancellationToken cancellationToken, params string[] args) 18 | { 19 | try 20 | { 21 | foreach (var arg in args) 22 | { 23 | await _uciHandler.Handle(arg, cancellationToken); 24 | } 25 | 26 | IncreaseInputBufferSize(); 27 | 28 | while (!cancellationToken.IsCancellationRequested) 29 | { 30 | var input = Console.ReadLine(); 31 | 32 | if (!string.IsNullOrEmpty(input)) 33 | { 34 | await _uciHandler.Handle(input, cancellationToken); 35 | } 36 | } 37 | } 38 | catch (Exception e) 39 | { 40 | _logger.Fatal(e); 41 | } 42 | finally 43 | { 44 | _logger.Info("Finishing {0}", nameof(Listener)); 45 | } 46 | } 47 | 48 | /// 49 | /// By default, the method reads input from a 256-character input buffer. 50 | /// Because this includes the Environment.NewLine character(s), the method can read lines that contain up to 254 characters. 51 | /// To read longer lines, call the OpenStandardInput(Int32) method. 52 | /// Source: https://learn.microsoft.com/en-us/dotnet/api/system.console.readline?view=net-8.0 53 | /// Something like this would work as well to read input without the input buffer limitation: 54 | /// private static string ReadInput() 55 | /// { 56 | /// Span bytes = stackalloc byte[4096 * 4]; 57 | /// Stream inputStream = Console.OpenStandardInput(); 58 | /// int outputLength = inputStream.Read(bytes); 59 | /// 60 | /// return Encoding.UTF8.GetString(bytes[..outputLength]); 61 | /// } 62 | /// 63 | private static void IncreaseInputBufferSize() 64 | { 65 | // Based on Lizard's solution. 4096 * 4 is enough to accept position commands with at least 500 moves 66 | Console.SetIn(new StreamReader(Console.OpenStandardInput(), Encoding.UTF8, false, 4096 * 4)); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Lynx.Test/BestMove/SingleLegalMoveTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | 4 | namespace Lynx.Test.BestMove; 5 | 6 | /// 7 | /// "If there's a single move, just do it" 8 | /// 9 | public class SingleLegalMoveTest : BaseTest 10 | { 11 | // https://lichess.org/MThBKius/black#313 12 | [TestCase("8/PPPPP1Pk/1PPPPP2/5PPP/2R5/R2RKR2/1R4R1/7R b - - 58 154")] 13 | [TestCase("Q5k1/1PPPP1P1/1PPPPP2/5PPP/2R5/R2RKR2/1R4R1/7R b - - 0 155")] 14 | [TestCase("6Q1/1PPPP1Pk/1PPPPP2/5PPP/2R5/R2RKR2/1R4R1/7R b - - 2 156")] 15 | [TestCase("6k1/1PPPP1P1/1PPPPP2/5PPP/2R5/R2RKRR1/1R6/7R b - - 1 157")] 16 | 17 | // https://lichess.org/BmNRWSXV/black#363 18 | [TestCase("1k6/3PPPPP/PPPPPPPP/PPPRK3/1RR3RR/R4R2/8/8 b - - 8 181")] 19 | [TestCase("k7/3PPPPP/PPPPPPPP/PPPRK3/1RR3RR/R7/5R2/8 b - - 10 182")] 20 | 21 | [TestCase("8/8/8/8/8/k7/1R6/K1q5 w - - 0 1")] 22 | [TestCase("8/8/8/8/8/k7/2B5/K1q5 w - - 0 1")] 23 | [TestCase("8/8/8/8/8/k1N5/8/K1q5 w - - 0 1")] 24 | [TestCase("1Q6/8/8/8/8/k7/8/K1q5 w - - 0 1")] 25 | 26 | [TestCase("8/8/8/8/8/K7/1r6/k1Q5 b - - 0 1")] 27 | [TestCase("8/8/8/8/8/K7/2b5/k1Q5 b - - 0 1")] 28 | [TestCase("8/8/8/8/8/K1n5/8/k1Q5 b - - 0 1")] 29 | [TestCase("1q6/8/8/8/8/K7/8/k1Q5 b - - 0 1")] 30 | public void SingleMove(string fen) 31 | { 32 | // Arrange 33 | const int depth = 61; 34 | Move? singleMove = null; 35 | var pos = new Position(fen); 36 | foreach (var move in MoveGenerator.GenerateAllMoves(pos)) 37 | { 38 | var state = pos.MakeMove(move); 39 | if (pos.IsValid()) 40 | { 41 | Assert.IsNull(singleMove); 42 | singleMove = move; 43 | } 44 | 45 | pos.UnmakeMove(move, state); 46 | } 47 | 48 | Assert.LessOrEqual(depth, Configuration.EngineSettings.MaxDepth); 49 | 50 | // Act 51 | var result = SearchBestMove(fen, depth); 52 | 53 | Assert.AreEqual(singleMove, result.BestMove); 54 | Assert.AreEqual(singleMove, result.Moves.Single()); 55 | 56 | if (pos.Side == Side.White) 57 | { 58 | Assert.AreEqual(EvaluationConstants.SingleMoveScore, result.Score); 59 | } 60 | else 61 | { 62 | Assert.AreEqual(-EvaluationConstants.SingleMoveScore, result.Score); 63 | } 64 | 65 | Assert.AreEqual(0, result.Depth); 66 | Assert.AreEqual(0, result.DepthReached); 67 | Assert.AreEqual(0, result.NodesPerSecond); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/Lynx.Test/MoveGeneration/GeneralMoveGeneratorTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | 4 | namespace Lynx.Test.MoveGeneration; 5 | 6 | public class GeneralMoveGeneratorTest 7 | { 8 | /// 9 | /// http://www.talkchess.com/forum3/viewtopic.php?f=7&t=78241&sid=c0f623952408bbd4a891bd36adcc132d&start=10#p907063 10 | /// 11 | [Test] 12 | public void DiscoveredCheckAfterEnPassantCapture() 13 | { 14 | var originalPosition = new Position("8/8/8/k1pP3R/8/8/8/n4K2 w - c6 0 1"); 15 | 16 | Span moves = stackalloc Move[Constants.MaxNumberOfPseudolegalMovesInAPosition]; 17 | Span attacks = stackalloc BitBoard[12]; 18 | Span attacksBySide = stackalloc BitBoard[2]; 19 | var evaluationContext = new EvaluationContext(attacks, attacksBySide); 20 | 21 | var enPassantMove = MoveGenerator.GenerateAllMoves(originalPosition, ref evaluationContext, moves).ToArray().Single(m => m.IsEnPassant()); 22 | var positionAfterEnPassant = new Position(originalPosition); 23 | positionAfterEnPassant.MakeMove(enPassantMove); 24 | 25 | moves = stackalloc Move[Constants.MaxNumberOfPseudolegalMovesInAPosition]; 26 | foreach (var move in MoveGenerator.GenerateAllMoves(positionAfterEnPassant, ref evaluationContext, moves)) 27 | { 28 | var newPosition = new Position(positionAfterEnPassant); 29 | newPosition.MakeMove(move); 30 | if (newPosition.IsValid()) 31 | { 32 | Assert.AreNotEqual(Piece.n, (Piece)move.Piece()); 33 | Assert.AreEqual(Piece.k, (Piece)move.Piece()); 34 | } 35 | } 36 | } 37 | 38 | [TestCase("QQQQQQBk/Q6B/Q6Q/Q6Q/Q6Q/Q6Q/Q6Q/KQQQQQQQ w - - 0 1")] // 265 pseudolegal moves at the time of writing this 39 | public void PositionWithMoreThan256PseudolegalMoves(string fen) 40 | { 41 | // 265 pseudolegal moves at the time of writing this 42 | var position = new Position(fen); 43 | 44 | Span moveSpan = stackalloc Move[Constants.MaxNumberOfPseudolegalMovesInAPosition]; 45 | Span attacks = stackalloc BitBoard[12]; 46 | Span attacksBySide = stackalloc BitBoard[2]; 47 | var evaluationContext = new EvaluationContext(attacks, attacksBySide); 48 | 49 | var allMoves = MoveGenerator.GenerateAllMoves(position, ref evaluationContext, moveSpan); 50 | 51 | Assert.LessOrEqual(allMoves.Length, Constants.MaxNumberOfPseudolegalMovesInAPosition); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Lynx.Test/PregeneratedAttacks/KnightAttacksTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | using BS = Lynx.Model.BoardSquare; 4 | 5 | namespace Lynx.Test.PregeneratedAttacks; 6 | 7 | public class KnightAttacksTest 8 | { 9 | [TestCase(BS.a8, new[] { BS.c7, BS.b6 })] 10 | [TestCase(BS.h8, new[] { BS.f7, BS.g6 })] 11 | [TestCase(BS.a1, new[] { BS.b3, BS.c2 })] 12 | [TestCase(BS.h1, new[] { BS.g3, BS.f2 })] 13 | 14 | [TestCase(BS.b8, new[] { BS.a6, BS.c6, BS.d7 })] 15 | [TestCase(BS.g8, new[] { BS.e7, BS.f6, BS.h6 })] 16 | [TestCase(BS.b1, new[] { BS.a3, BS.c3, BS.d2 })] 17 | [TestCase(BS.g1, new[] { BS.e2, BS.f3, BS.h3 })] 18 | 19 | [TestCase(BS.a7, new[] { BS.c8, BS.c6, BS.b5 })] 20 | [TestCase(BS.h7, new[] { BS.f8, BS.f6, BS.g5 })] 21 | [TestCase(BS.a2, new[] { BS.b4, BS.c3, BS.c1 })] 22 | [TestCase(BS.h2, new[] { BS.f1, BS.f3, BS.g4 })] 23 | 24 | [TestCase(BS.d8, new[] { BS.b7, BS.c6, BS.e6, BS.f7 })] 25 | [TestCase(BS.a5, new[] { BS.b7, BS.c6, BS.c4, BS.b3 })] 26 | [TestCase(BS.h5, new[] { BS.g7, BS.f6, BS.f4, BS.g3 })] 27 | [TestCase(BS.d1, new[] { BS.b2, BS.c3, BS.e3, BS.f2 })] 28 | 29 | [TestCase(BS.b6, new[] { BS.a8, BS.c8, BS.d7, BS.d5, BS.c4, BS.a4 })] 30 | [TestCase(BS.g6, new[] { BS.h8, BS.f8, BS.e7, BS.e5, BS.f4, BS.h4 })] 31 | [TestCase(BS.b3, new[] { BS.a5, BS.c5, BS.d4, BS.d2, BS.c1, BS.a1 })] 32 | [TestCase(BS.g3, new[] { BS.f5, BS.h5, BS.e4, BS.e2, BS.f1, BS.h1 })] 33 | 34 | [TestCase(BS.e4, new[] { BS.d6, BS.f6, BS.c5, BS.g5, BS.c3, BS.g3, BS.d2, BS.f2 })] 35 | [TestCase(BS.e5, new[] { BS.d7, BS.f7, BS.c6, BS.g6, BS.c4, BS.g4, BS.d3, BS.f3 })] 36 | [TestCase(BS.d4, new[] { BS.c6, BS.e6, BS.b5, BS.f5, BS.b3, BS.f3, BS.c2, BS.e2 })] 37 | [TestCase(BS.d5, new[] { BS.c7, BS.e7, BS.b6, BS.f6, BS.b4, BS.f4, BS.c3, BS.e3 })] 38 | public void MaskKnightAttacks(BS knightSquare, BS[] attackedSquares) 39 | { 40 | var attacks = AttackGenerator.MaskKnightAttacks((int)knightSquare); 41 | ValidateAttacks(attackedSquares, attacks); 42 | 43 | attacks = Attacks.KnightAttacks[(int)knightSquare]; 44 | ValidateAttacks(attackedSquares, attacks); 45 | 46 | static void ValidateAttacks(BS[] attackedSquares, BitBoard attacks) 47 | { 48 | foreach (var attackedSquare in attackedSquares) 49 | { 50 | Assert.True(attacks.GetBit(attackedSquare)); 51 | attacks.PopBit(attackedSquare); 52 | } 53 | 54 | Assert.AreEqual(default(BitBoard), attacks); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Lynx.Cli/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "GeneralSettings": { 3 | "EnableTuning": true 4 | }, 5 | "EngineSettings": { 6 | //"TranspositionTableSize": 256, 7 | "EstimateMultithreadedSearchNPS": false, 8 | "IsChess960": false 9 | }, 10 | "NLog": { 11 | "internalLogLevel": "Warn", 12 | "throwExceptions": true, 13 | "rules": { 14 | 15 | "1": { 16 | "logger": "*", 17 | //"minLevel": "Off", 18 | "minLevel": "Debug", 19 | "writeTo": "logs" 20 | }, 21 | 22 | "2": { 23 | "logger": "*", 24 | "minLevel": "Off", 25 | //"minLevel": "Trace", 26 | "writeTo": "moves" 27 | }, 28 | 29 | "100": { 30 | "logger": "*", 31 | //"minLevel": "Warn", 32 | "minLevel": "Debug", 33 | "writeTo": "console" 34 | } 35 | }, 36 | "targets": { 37 | "console": { 38 | "wordHighlightingRules": [ 39 | { 40 | "Text": "[Lynx]", 41 | "foregroundColor": "Green", 42 | "IgnoreCase": true, 43 | "WholeWords": false 44 | }, 45 | { 46 | "Text": "Lynx", 47 | "foregroundColor": "DarkGreen", 48 | "IgnoreCase": true, 49 | "WholeWords": false 50 | }, 51 | 52 | // UCI GUI commands 53 | { 54 | "Words": "[GUI],debug,go,isready,ponderhit,position,quit,register,setoption,stop,uci,ucinewgame", 55 | "foregroundColor": "Blue", 56 | "IgnoreCase": false, 57 | "WholeWords": true 58 | }, 59 | 60 | // UCI GUI subcommands 61 | { 62 | "Words": "searchmoves,ponder,wtime,btime,winc,binc,movestogo,depth,mate,movetime,infinite,startpos,fen,moves,later,name,value,type,min,max,default,author,code", 63 | "foregroundColor": "DarkMagenta", 64 | "IgnoreCase": false, 65 | "WholeWords": true 66 | }, 67 | 68 | // UCI engine commands 69 | { 70 | "Words": "bestmove,copyprotection,id,info,option,readyok,registration,uciok", 71 | "foregroundColor": "Green", 72 | "IgnoreCase": false, 73 | "WholeWords": true 74 | }, 75 | 76 | // UCI Engine subcommands 77 | { 78 | "Words": "checking,ok,error,seldepth,depth,time,nodes,pv,multipv,score,cp,mate,lowerbound,upperbound,currmove,currmovenumber,hashfull,nps,tbhits,cpuload,string,refutation,currline,wdl", 79 | "foregroundColor": "DarkMagenta", 80 | "IgnoreCase": false, 81 | "WholeWords": true 82 | } 83 | ] 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Lynx/Search/OnlineTablebase.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using System.Diagnostics; 3 | 4 | namespace Lynx; 5 | public sealed partial class Engine 6 | { 7 | public async Task ProbeOnlineTablebase(Position position, ulong[] positionHashHistory, int halfMovesWithoutCaptureOrPawnMove, CancellationToken cancellationToken) 8 | { 9 | var stopWatch = Stopwatch.StartNew(); 10 | 11 | try 12 | { 13 | var tablebaseResult = await OnlineTablebaseProber.RootSearch(position, positionHashHistory, halfMovesWithoutCaptureOrPawnMove, cancellationToken); 14 | 15 | if (tablebaseResult.BestMove != 0) 16 | { 17 | var elapsedSeconds = Utils.CalculateElapsedSeconds(stopWatch); 18 | 19 | var searchResult = new SearchResult( 20 | #if MULTITHREAD_DEBUG 21 | _id, 22 | #endif 23 | tablebaseResult.BestMove, score: 0, targetDepth: 0, [tablebaseResult.BestMove], mate: tablebaseResult.MateScore) 24 | { 25 | DepthReached = 0, 26 | Depth = 666, // In case some guis proritize the info command with biggest depth 27 | Time = Utils.CalculateUCITime(elapsedSeconds), 28 | NodesPerSecond = 0, 29 | HashfullPermill = _tt.HashfullPermillApprox(), 30 | WDL = WDL.WDLModel( 31 | (int)Math.CopySign( 32 | EvaluationConstants.PositiveCheckmateDetectionLimit + tablebaseResult.MateScore, 33 | tablebaseResult.MateScore), 34 | 0) 35 | }; 36 | 37 | await _engineWriter.WriteAsync(searchResult, cancellationToken); 38 | //await _searchCancellationTokenSource.CancelAsync(); // TODO revisit 39 | 40 | return searchResult; 41 | } 42 | 43 | return null; 44 | } 45 | catch (OperationCanceledException) // Also catches TaskCanceledException 46 | { 47 | #pragma warning disable S6667 // Logging in a catch clause should pass the caught exception as a parameter. - expected 48 | _logger.Info("Online tb probing cancellation requested after {0}ms", _stopWatch.ElapsedMilliseconds); 49 | #pragma warning restore S6667 // Logging in a catch clause should pass the caught exception as a parameter. 50 | 51 | return null; 52 | } 53 | catch (Exception e) 54 | { 55 | _logger.Error(e, "Unexpected error ocurred during the online tb probing\n{0}", e.StackTrace); 56 | 57 | return null; 58 | } 59 | finally 60 | { 61 | _stopWatch.Stop(); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 10.0.1 9 | 6.0.6 10 | 0.15.8 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /tests/Lynx.Test/PregeneratedAttacks/PawnAttacksTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | 4 | namespace Lynx.Test.PregeneratedAttacks; 5 | 6 | public class PawnAttacksTest 7 | { 8 | [TestCase(BoardSquare.a1, true, 1UL << (int)BoardSquare.b2)] 9 | [TestCase(BoardSquare.a1, false, 0UL)] 10 | [TestCase(BoardSquare.a8, true, 0UL)] 11 | [TestCase(BoardSquare.a8, false, 1UL << (int)BoardSquare.b7)] 12 | 13 | [TestCase(BoardSquare.h1, true, 1UL << (int)BoardSquare.g2)] 14 | [TestCase(BoardSquare.h1, false, 0UL)] 15 | [TestCase(BoardSquare.h8, true, 0UL)] 16 | [TestCase(BoardSquare.h8, false, 1UL << (int)BoardSquare.g7)] 17 | 18 | [TestCase(BoardSquare.b6, true, 0b101UL << (int)BoardSquare.a7)] 19 | [TestCase(BoardSquare.b6, false, 0b101UL << (int)BoardSquare.a5)] 20 | [TestCase(BoardSquare.e3, true, 0b101UL << (int)BoardSquare.d4)] 21 | [TestCase(BoardSquare.e3, false, 0b101UL << (int)BoardSquare.d2)] 22 | public void MaskPawnAttacks(BoardSquare square, bool isWhite, ulong expectedResult) 23 | { 24 | // Act 25 | var attacks = AttackGenerator.MaskPawnAttacks((int)square, isWhite); 26 | 27 | // Assert 28 | Assert.AreEqual(expectedResult, attacks); 29 | 30 | // Act 31 | attacks = Attacks.PawnAttacks[isWhite ? 1 : 0][(int)square]; 32 | 33 | // Assert 34 | Assert.AreEqual(expectedResult, attacks); 35 | } 36 | 37 | [Test] 38 | public void InitializePawnAttacks() 39 | { 40 | // Act 41 | var result = AttackGenerator.InitializePawnAttacks(); 42 | 43 | // Assert 44 | foreach (var square in Enum.GetValues()) 45 | { 46 | var intSquare = (int)square; 47 | 48 | if (intSquare < 8 || intSquare > (63 - 8)) 49 | { 50 | continue; 51 | } 52 | 53 | bool aFile = !Constants.NotAFile.GetBit(intSquare); 54 | bool hFile = !Constants.NotHFile.GetBit(intSquare); 55 | 56 | var attackDiagram = aFile || hFile 57 | ? 1UL 58 | : 0b101UL; 59 | 60 | var whiteOffset = aFile 61 | ? 7 // a7 -> b8 (1) 62 | : 9; // c7 -> b8 (101) 63 | 64 | var expectedWhiteResult = attackDiagram << (intSquare - whiteOffset); 65 | Assert.AreEqual(expectedWhiteResult, result[1][intSquare]); 66 | 67 | var blackOffset = aFile 68 | ? 9 // a7 -> b8 (1) 69 | : 7; // c7 -> b6 (101) 70 | 71 | var expectedBlackResult = attackDiagram << (intSquare + blackOffset); 72 | Assert.AreEqual(expectedBlackResult, result[0][intSquare]); 73 | } 74 | 75 | for (int square = 0; square < 8; ++square) 76 | { 77 | Assert.AreEqual(0UL, result[1][square]); 78 | Assert.AreEqual(0UL, result[0][63 - square]); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Lynx.Cli/Runner.cs: -------------------------------------------------------------------------------- 1 | using Lynx.UCI.Commands.Engine; 2 | using NLog; 3 | using System.Threading.Channels; 4 | 5 | namespace Lynx.Cli; 6 | 7 | public static class Runner 8 | { 9 | private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); 10 | 11 | public static async Task Run(params string[] args) 12 | { 13 | var uciChannel = Channel.CreateBounded(new BoundedChannelOptions(100) { SingleReader = true, SingleWriter = true, FullMode = BoundedChannelFullMode.Wait }); 14 | var engineChannel = Channel.CreateBounded(new BoundedChannelOptions(2 * Configuration.EngineSettings.MaxDepth) { SingleReader = true, SingleWriter = false, FullMode = BoundedChannelFullMode.DropOldest }); 15 | 16 | using CancellationTokenSource source = new(); 17 | CancellationToken cancellationToken = source.Token; 18 | 19 | var searcher = new Searcher(uciChannel, engineChannel); 20 | var uciHandler = new UCIHandler(uciChannel, engineChannel, searcher); 21 | var writer = new Writer(engineChannel); 22 | var listener = new Listener(uciHandler); 23 | 24 | var tasks = new List 25 | { 26 | Task.Run(() => writer.Run(cancellationToken)), 27 | Task.Run(() => searcher.Run(cancellationToken)), 28 | Task.Run(() => listener.Run(cancellationToken, args)), 29 | uciChannel.Reader.Completion, 30 | engineChannel.Reader.Completion 31 | }; 32 | 33 | try 34 | { 35 | Console.WriteLine($"{IdCommand.EngineName} {IdCommand.GetLynxVersion()} by {IdCommand.EngineAuthor}"); 36 | await Task.WhenAny(tasks); 37 | } 38 | catch (AggregateException ae) 39 | { 40 | foreach (var e in ae.InnerExceptions) 41 | { 42 | if (e is TaskCanceledException taskCanceledException) 43 | { 44 | Console.WriteLine("Cancellation requested exception: {0}", taskCanceledException.Message); 45 | _logger.Fatal(ae, "Cancellation requested exception: {0}", taskCanceledException.Message); 46 | } 47 | else 48 | { 49 | Console.WriteLine("Exception {0}: {1}", e.GetType().Name, e.Message); 50 | _logger.Fatal(ae, "Exception {0}: {1}", e.GetType().Name, e.Message); 51 | } 52 | } 53 | } 54 | catch (Exception e) 55 | { 56 | Console.WriteLine("Unexpected exception"); 57 | Console.WriteLine(e.Message); 58 | 59 | _logger.Fatal(e, "Unexpected exception: {Exception}", e.Message); 60 | } 61 | finally 62 | { 63 | engineChannel.Writer.TryComplete(); 64 | uciChannel.Writer.TryComplete(); 65 | //source.Cancel(); 66 | LogManager.Shutdown(); // Flush and close down internal threads and timers 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Lynx.Benchmark/ResetLS1B_Benchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | 3 | namespace Lynx.Benchmark; 4 | 5 | public static class ResetLS1BImplementations 6 | { 7 | public static int GetLS1BIndex(ulong bitboard) 8 | { 9 | if (bitboard == default) 10 | { 11 | return -1; 12 | } 13 | 14 | return CountBits(bitboard ^ (bitboard - 1)) - 1; 15 | } 16 | 17 | public static int CountBits(ulong bitboard) 18 | { 19 | int counter = 0; 20 | 21 | // Consecutively reset LSB 22 | while (bitboard != default) 23 | { 24 | ++counter; 25 | bitboard = ResetLS1B(bitboard); 26 | } 27 | 28 | return counter; 29 | } 30 | 31 | public static ulong ResetLS1B(ulong bitboard) 32 | { 33 | return bitboard & (bitboard - 1); 34 | } 35 | 36 | public static ulong PopBit(ulong bitboard, int square) 37 | { 38 | return bitboard & ~(1UL << square); 39 | } 40 | } 41 | 42 | public class ResetLS1B_Benchmark : BaseBenchmark 43 | { 44 | public static IEnumerable Data => [1, 10, 1_000, 10_000, 100_000]; 45 | 46 | /// 47 | /// Same perf. 48 | /// 49 | [Benchmark(Baseline = true)] 50 | [ArgumentsSource(nameof(Data))] 51 | public int GetAndReset(int iterations) 52 | { 53 | int ls1b = 0; 54 | ulong bitboard = 6060551578861568; 55 | ulong bitboard2 = 335588096; 56 | 57 | for (int i = 0; i < iterations; ++i) 58 | { 59 | while (bitboard != default) 60 | { 61 | ls1b = ResetLS1BImplementations.GetLS1BIndex(bitboard); 62 | bitboard = ResetLS1BImplementations.ResetLS1B(bitboard); 63 | } 64 | 65 | while (bitboard2 != default) 66 | { 67 | ls1b = ResetLS1BImplementations.GetLS1BIndex(bitboard2); 68 | bitboard2 = ResetLS1BImplementations.ResetLS1B(bitboard2); 69 | } 70 | } 71 | 72 | return ls1b; 73 | } 74 | 75 | /// 76 | /// Same perf. 77 | /// 78 | [Benchmark] 79 | [ArgumentsSource(nameof(Data))] 80 | public int GetAndPop(int iterations) 81 | { 82 | int ls1b = 0; 83 | ulong bitboard = 6060551578861568; 84 | ulong bitboard2 = 335588096; 85 | 86 | for (int i = 0; i < iterations; ++i) 87 | { 88 | while (bitboard != default) 89 | { 90 | ls1b = ResetLS1BImplementations.GetLS1BIndex(bitboard); 91 | bitboard = ResetLS1BImplementations.PopBit(bitboard, ls1b); 92 | } 93 | 94 | while (bitboard2 != default) 95 | { 96 | ls1b = ResetLS1BImplementations.GetLS1BIndex(bitboard2); 97 | bitboard2 = ResetLS1BImplementations.PopBit(bitboard2, ls1b); 98 | } 99 | } 100 | 101 | return ls1b; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Lynx.Cli/Lynx.Cli.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | true 6 | 7 | $(InterceptorsPreviewNamespaces);Microsoft.Extensions.Configuration.Binder.SourceGeneration 8 | 9 | 10 | 11 | ProxExe 12 | 13 | 14 | 15 | Release 16 | true 17 | true 18 | true 19 | true 20 | true 21 | 22 | true 23 | true 24 | 25 | false 26 | 27 | false 28 | 0 29 | 30 | 31 | false 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | PreserveNewest 48 | 49 | 50 | PreserveNewest 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/Lynx.Benchmark/PriorityQueue_EnqueueRange_Benchmark.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * | Method | itemsCount | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | 3 | * |---------------- |----------- |----------------:|--------------:|--------------:|------:|--------:|---------:|---------:|---------:|-----------:| 4 | * | EnqueueOneByOne | 10 | 147.2 ns | 0.65 ns | 0.58 ns | 1.00 | 0.00 | 0.0725 | - | - | 152 B | 5 | * | EnqueueRange | 10 | 228.2 ns | 2.42 ns | 1.89 ns | 1.55 | 0.01 | 0.1376 | - | - | 288 B | 6 | * | | | | | | | | | | | | 7 | * | EnqueueOneByOne | 100 | 1,355.6 ns | 5.01 ns | 4.18 ns | 1.00 | 0.00 | 0.4158 | - | - | 872 B | 8 | * | EnqueueRange | 100 | 1,655.2 ns | 6.09 ns | 4.76 ns | 1.22 | 0.01 | 0.6523 | - | - | 1368 B | 9 | * | | | | | | | | | | | | 10 | * | EnqueueOneByOne | 1000 | 13,730.4 ns | 94.51 ns | 78.92 ns | 1.00 | 0.00 | 3.8452 | - | - | 8072 B | 11 | * | EnqueueRange | 1000 | 15,925.2 ns | 61.39 ns | 51.26 ns | 1.16 | 0.01 | 5.7983 | - | - | 12168 B | 12 | * | | | | | | | | | | | | 13 | * | EnqueueOneByOne | 1000000 | 15,554,643.8 ns | 229,715.58 ns | 203,636.88 ns | 1.00 | 0.00 | 468.7500 | 468.7500 | 468.7500 | 8000064 B | 14 | * | EnqueueRange | 1000000 | 19,020,092.0 ns | 378,978.47 ns | 354,496.70 ns | 1.22 | 0.03 | 625.0000 | 625.0000 | 625.0000 | 12001959 B | 15 | * 16 | */ 17 | 18 | using BenchmarkDotNet.Attributes; 19 | 20 | namespace Lynx.Benchmark; 21 | 22 | public class PriorityQueue_EnqueueRange_Benchmark : BaseBenchmark 23 | { 24 | private const int Priority = 1_1111_111; 25 | 26 | public static IEnumerable Data => [10, 100, 1_000, 1_000_000]; 27 | 28 | [Benchmark(Baseline = true)] 29 | [ArgumentsSource(nameof(Data))] 30 | public void EnqueueOneByOne(int itemsCount) 31 | { 32 | var queue = new PriorityQueue(itemsCount); 33 | 34 | for (int i = 0; i < itemsCount; ++i) 35 | { 36 | queue.Enqueue(i, Priority); 37 | } 38 | } 39 | 40 | [Benchmark] 41 | [ArgumentsSource(nameof(Data))] 42 | public void EnqueueRange(int itemsCount) 43 | { 44 | var queue = new PriorityQueue(itemsCount); 45 | var items = new List(itemsCount); 46 | 47 | for (int i = 0; i < itemsCount; ++i) 48 | { 49 | items.Add(i); 50 | } 51 | 52 | queue.EnqueueRange(items, Priority); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Lynx/LynxRandom.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace Lynx; 5 | 6 | #pragma warning disable CA5394 // Do not use insecure randomness 7 | 8 | public class LynxRandom : Random 9 | { 10 | private readonly XoshiroImpl _impl; 11 | 12 | public LynxRandom() 13 | { 14 | _impl = new XoshiroImpl(this); 15 | } 16 | 17 | public LynxRandom(int seed) : base(seed) 18 | { 19 | _impl = new XoshiroImpl(this); 20 | } 21 | 22 | /// 23 | /// Based on dotnet/runtime implementation, 24 | /// https://github.com/dotnet/runtime/blob/508fef51e841aa16ffed1aae32bf4793a2cea363/src/libraries/System.Private.CoreLib/src/System/Random.Xoshiro256StarStarImpl.cs 25 | /// 26 | public ulong NextUInt64() => _impl.NextUInt64(); 27 | 28 | /// 29 | /// Based on dotnet/runtime implementation, 30 | /// https://github.com/dotnet/runtime/blob/a7f96cb070ffb8adf266b2e09d26759d7f978a60/src/libraries/System.Private.CoreLib/src/System/Random.Xoshiro256StarStarImpl.cs 31 | /// is subsequently based on the algorithm from http://prng.di.unimi.it/xoshiro256starstar.c 32 | /// Written in 2018 by David Blackman and Sebastiano Vigna (vigna@acm.org) 33 | /// To the extent possible under law, the author has dedicated all copyright 34 | /// and related and neighboring rights to this software to the public domain 35 | /// worldwide. This software is distributed without any warranty. 36 | /// See . 37 | /// 38 | internal sealed class XoshiroImpl 39 | { 40 | private ulong _s0, _s1, _s2, _s3; 41 | 42 | public XoshiroImpl(Random random) 43 | { 44 | do 45 | { 46 | _s0 = InternalNextUInt64(); 47 | _s1 = InternalNextUInt64(); 48 | _s2 = InternalNextUInt64(); 49 | _s3 = InternalNextUInt64(); 50 | } 51 | while ((_s0 | _s1 | _s2 | _s3) == 0); // at least one value must be non-zero 52 | 53 | // 'Naive' version of what we're trying to achieve 54 | ulong InternalNextUInt64() 55 | { 56 | Span arr = stackalloc byte[8]; 57 | random.NextBytes(arr); 58 | 59 | return BitConverter.ToUInt64(arr); 60 | } 61 | } 62 | 63 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 64 | public ulong NextUInt64() 65 | { 66 | ulong s0 = _s0, s1 = _s1, s2 = _s2, s3 = _s3; 67 | 68 | ulong result = BitOperations.RotateLeft(s1 * 5, 7) * 9; 69 | ulong t = s1 << 17; 70 | 71 | s2 ^= s0; 72 | s3 ^= s1; 73 | s1 ^= s2; 74 | s0 ^= s3; 75 | 76 | s2 ^= t; 77 | s3 = BitOperations.RotateLeft(s3, 45); 78 | 79 | _s0 = s0; 80 | _s1 = s1; 81 | _s2 = s2; 82 | _s3 = s3; 83 | 84 | return result; 85 | } 86 | } 87 | } 88 | 89 | #pragma warning restore CA5394 // Do not use insecure randomness 90 | -------------------------------------------------------------------------------- /tests/Lynx.Test/BestMove/MatesInExactlyXTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace Lynx.Test.BestMove; 4 | 5 | #pragma warning disable S4144, IDE0060, RCS1163 // Methods should not have identical implementations, unused arguments 6 | 7 | /// 8 | /// All the tests in this class used to pass when null-pruning wasn't implemented 9 | /// 10 | [Explicit] 11 | [Category(Categories.NoPruning)] 12 | public class MatesInExactlyXTest : BaseTest 13 | { 14 | [TestCaseSource(typeof(MatePositions), nameof(MatePositions.Mates_in_1))] 15 | public void Mate_in_Exactly_1(string fen, string[]? allowedUCIMoveString, string description) 16 | { 17 | var result = TestBestMove(fen, allowedUCIMoveString, null, depth: 1); 18 | Assert.AreEqual(1, result.Mate); 19 | } 20 | 21 | [TestCaseSource(typeof(MatePositions), nameof(MatePositions.Mates_in_2))] 22 | public void Mate_in_Exactly_2(string fen, string[]? allowedUCIMoveString, string description) 23 | { 24 | var result = TestBestMove(fen, allowedUCIMoveString, null, depth: 3); 25 | Assert.AreEqual(2, result.Mate); 26 | } 27 | 28 | [TestCaseSource(typeof(MatePositions), nameof(MatePositions.Mates_in_3))] 29 | public void Mate_in_Exactly_3(string fen, string[]? allowedUCIMoveString, string description) 30 | { 31 | var result = TestBestMove(fen, allowedUCIMoveString, null, depth: 5); 32 | Assert.AreEqual(3, result.Mate); 33 | } 34 | 35 | [TestCaseSource(typeof(MatePositions), nameof(MatePositions.Mates_in_4))] 36 | public void Mate_in_Exactly_4(string fen, string[]? allowedUCIMoveString, string description) 37 | { 38 | var result = TestBestMove(fen, allowedUCIMoveString, null, depth: 8); 39 | Assert.AreEqual(4, result.Mate); 40 | } 41 | 42 | [TestCaseSource(typeof(MatePositions), nameof(MatePositions.Mates_in_4_Collection))] 43 | public void Mate_in_Exactly_4_Collection(string fen, string[]? allowedUCIMoveString, string description) 44 | { 45 | var result = TestBestMove(fen, allowedUCIMoveString, null, depth: 8); 46 | Assert.AreEqual(4, result.Mate); 47 | } 48 | 49 | [TestCaseSource(typeof(MatePositions), nameof(MatePositions.Mates_in_5))] 50 | public void Mate_in_Exactly_5(string fen, string[]? allowedUCIMoveString, string description) 51 | { 52 | var result = TestBestMove(fen, allowedUCIMoveString, null, depth: 10); 53 | Assert.AreEqual(5, result.Mate); 54 | } 55 | 56 | [TestCaseSource(typeof(MatePositions), nameof(MatePositions.Mates_in_6))] 57 | public void Mate_in_Exactly_6(string fen, string[]? allowedUCIMoveString, string description) 58 | { 59 | var result = TestBestMove(fen, allowedUCIMoveString, null, depth: 12); 60 | Assert.AreEqual(6, result.Mate); 61 | } 62 | 63 | [TestCaseSource(typeof(MatePositions), nameof(MatePositions.Mates_in_7))] 64 | public void Mate_in_Exactly_7(string fen, string[]? allowedUCIMoveString, string description) 65 | { 66 | var result = TestBestMove(fen, allowedUCIMoveString, null, depth: 14); 67 | Assert.AreEqual(7, result.Mate); 68 | } 69 | } 70 | 71 | #pragma warning restore S4144, IDE0060, RCS1163 // Methods should not have identical implementations, unused arguments -------------------------------------------------------------------------------- /tests/Lynx.Test/BestMove/MatesTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | #pragma warning disable S4144, IDE0060, RCS1163 // Methods should not have identical implementations, unused arguments 4 | 5 | namespace Lynx.Test.BestMove; 6 | 7 | public class MatesTest : BaseTest 8 | { 9 | [TestCaseSource(typeof(MatePositions), nameof(MatePositions.Mates_in_1))] 10 | public void Mate_in_1(string fen, string[]? allowedUCIMoveString, string description) 11 | { 12 | var result = SearchBestMove(fen); 13 | Assert.AreEqual(1, result.Mate); 14 | } 15 | 16 | [TestCaseSource(typeof(MatePositions), nameof(MatePositions.Mates_in_2))] 17 | public void Mate_in_2(string fen, string[]? allowedUCIMoveString, string description) 18 | { 19 | var result = SearchBestMove(fen); 20 | Assert.AreNotEqual(default, result.Mate); 21 | } 22 | 23 | [Explicit] 24 | [Category(Categories.LongRunning)] 25 | [TestCaseSource(typeof(MatePositions), nameof(MatePositions.Mates_in_3))] 26 | public void Mate_in_3(string fen, string[]? allowedUCIMoveString, string description) 27 | { 28 | var result = SearchBestMove(fen); 29 | Assert.AreNotEqual(default, result.Mate); 30 | } 31 | 32 | [Explicit] 33 | [Category(Categories.LongRunning)] 34 | [TestCaseSource(typeof(MatePositions), nameof(MatePositions.Mates_in_4))] 35 | public void Mate_in_4(string fen, string[]? allowedUCIMoveString, string description) 36 | { 37 | var result = SearchBestMove(fen); 38 | Assert.AreNotEqual(default, result.Mate); 39 | } 40 | 41 | /// 42 | /// http://www.talkchess.com/forum3/viewtopic.php?f=7&t=78583 43 | /// 44 | [Explicit] 45 | [Category(Categories.LongRunning)] 46 | [TestCaseSource(typeof(MatePositions), nameof(MatePositions.Mates_in_4_Collection))] 47 | public void Mate_in_4_Collection(string fen, string[]? allowedUCIMoveString, string description) 48 | { 49 | var result = SearchBestMove(fen); 50 | Assert.AreNotEqual(default, result.Mate); 51 | } 52 | 53 | [Explicit] 54 | [Category(Categories.LongRunning)] 55 | [TestCaseSource(typeof(MatePositions), nameof(MatePositions.Mates_in_5))] 56 | public void Mate_in_5(string fen, string[]? allowedUCIMoveString, string description) 57 | { 58 | var result = SearchBestMove(fen); 59 | Assert.AreNotEqual(default, result.Mate); 60 | } 61 | 62 | [Explicit] 63 | [Category(Categories.LongRunning)] 64 | [TestCaseSource(typeof(MatePositions), nameof(MatePositions.Mates_in_6))] 65 | public void Mate_in_6(string fen, string[]? allowedUCIMoveString, string description) 66 | { 67 | var result = SearchBestMove(fen); 68 | Assert.AreNotEqual(default, result.Mate); 69 | } 70 | 71 | [Explicit] 72 | [Category(Categories.LongRunning)] 73 | [TestCaseSource(typeof(MatePositions), nameof(MatePositions.Mates_in_7))] 74 | public void Mate_in_7(string fen, string[]? allowedUCIMoveString, string description) 75 | { 76 | var result = SearchBestMove(fen, depth: 14); 77 | Assert.AreNotEqual(default, result.Mate); 78 | } 79 | } 80 | 81 | #pragma warning restore S4144, IDE0060, RCS1163 // Methods should not have identical implementations, unused arguments -------------------------------------------------------------------------------- /tests/Lynx.Test/ConfigurationTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using NUnit.Framework; 3 | using System.Text.Json; 4 | using System.Text.Json.Nodes; 5 | 6 | namespace Lynx.Test; 7 | 8 | [Explicit] 9 | [Category(Categories.Configuration)] 10 | [NonParallelizable] 11 | public class ConfigurationTest 12 | { 13 | [Test] 14 | public void SynchronizedAppSettings() 15 | { 16 | var config = new ConfigurationBuilder() 17 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) 18 | .Build(); 19 | 20 | var engineSettingsSection = config.GetRequiredSection(nameof(EngineSettings)); 21 | Assert.IsNotNull(engineSettingsSection); 22 | 23 | var serializedEngineSettingsConfig = JsonSerializer.Serialize(Configuration.EngineSettings, EngineSettingsJsonSerializerContext.Default.EngineSettings); 24 | #pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code 25 | var jsonNode = JsonSerializer.Deserialize(serializedEngineSettingsConfig); 26 | #pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code 27 | Assert.IsNotNull(jsonNode); 28 | 29 | #pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code - using sourcegenerator 30 | engineSettingsSection.Bind(Configuration.EngineSettings); 31 | #pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code 32 | 33 | var reflectionProperties = Configuration.EngineSettings.GetType().GetProperties( 34 | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); 35 | Assert.GreaterOrEqual(reflectionProperties.Length, 35); 36 | 37 | var originalCulture = Thread.CurrentThread.CurrentCulture; 38 | Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture; 39 | 40 | Assert.Multiple(() => 41 | { 42 | foreach (var property in reflectionProperties) 43 | { 44 | if (property.PropertyType == typeof(int[]) 45 | || property.Name == "EstimateMultithreadedSearchNPS") 46 | { 47 | continue; 48 | } 49 | 50 | var sourceSetting = jsonNode![property.Name]!.ToString() 51 | .Replace("\r", "") 52 | .Replace("\n", "") 53 | .Replace(" ", "") 54 | .ToLowerInvariant(); 55 | 56 | var configSetting = property.GetValue(Configuration.EngineSettings)!.ToString()! 57 | .Replace("\r", "") 58 | .Replace("\n", "") 59 | .Replace(" ", "") 60 | .ToLowerInvariant(); 61 | 62 | Assert.AreEqual(sourceSetting, configSetting, $"Error in {property.Name} ({property.PropertyType}): (Configuration.cs) {sourceSetting} != {configSetting} (appSettings.json)"); 63 | } 64 | 65 | Thread.CurrentThread.CurrentCulture = originalCulture; 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Lynx.Benchmark/EnumCasting_Benchmark.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * | Method | iterations | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | 3 | * |--------- |----------- |---------------:|--------------:|--------------:|------:|--------:|----------:|------------:| 4 | * | Constant | 1 | 4.663 ns | 0.1549 ns | 0.1449 ns | 1.00 | 0.00 | - | NA | 5 | * | Cast | 1 | 4.122 ns | 0.1462 ns | 0.1795 ns | 0.88 | 0.07 | - | NA | 6 | * | | | | | | | | | | 7 | * | Constant | 10 | 22.844 ns | 0.5169 ns | 0.5077 ns | 1.00 | 0.00 | - | NA | 8 | * | Cast | 10 | 23.195 ns | 0.4863 ns | 0.7714 ns | 1.02 | 0.04 | - | NA | 9 | * | | | | | | | | | | 10 | * | Constant | 1000 | 2,754.421 ns | 14.6464 ns | 12.9836 ns | 1.00 | 0.00 | - | NA | 11 | * | Cast | 1000 | 2,768.868 ns | 16.7819 ns | 14.8767 ns | 1.01 | 0.01 | - | NA | 12 | * | | | | | | | | | | 13 | * | Constant | 10000 | 27,710.451 ns | 165.8820 ns | 138.5190 ns | 1.00 | 0.00 | - | NA | 14 | * | Cast | 10000 | 28,080.178 ns | 289.2892 ns | 270.6013 ns | 1.01 | 0.01 | - | NA | 15 | * | | | | | | | | | | 16 | * | Constant | 100000 | 277,720.653 ns | 2,782.1333 ns | 2,602.4092 ns | 1.00 | 0.00 | - | NA | 17 | * | Cast | 100000 | 278,077.362 ns | 2,431.3312 ns | 2,274.2687 ns | 1.00 | 0.01 | - | NA | 18 | */ 19 | 20 | using BenchmarkDotNet.Attributes; 21 | using Lynx.Model; 22 | using static Lynx.TunableEvalParameters; 23 | 24 | namespace Lynx.Benchmark; 25 | 26 | public class EnumCasting_Benchmark : BaseBenchmark 27 | { 28 | public static IEnumerable Data => [1, 10, 1_000, 10_000, 100_000]; 29 | 30 | private const int Pawn = (int)Piece.P; 31 | 32 | [Benchmark(Baseline = true)] 33 | [ArgumentsSource(nameof(Data))] 34 | public int Constant(int iterations) 35 | { 36 | var sum = 0; 37 | for (int i = 0; i < iterations; ++i) 38 | { 39 | sum += MiddleGamePieceValues[0][0][Pawn]; 40 | sum += MiddleGamePieceValues[0][0][Pawn]; 41 | sum += MiddleGamePieceValues[0][0][Pawn]; 42 | sum += MiddleGamePieceValues[0][0][Pawn]; 43 | sum += MiddleGamePieceValues[0][0][Pawn]; 44 | sum += MiddleGamePieceValues[0][0][Pawn]; 45 | } 46 | 47 | return sum; 48 | } 49 | 50 | [Benchmark] 51 | [ArgumentsSource(nameof(Data))] 52 | public int Cast(int iterations) 53 | { 54 | var sum = 0; 55 | for (int i = 0; i < iterations; ++i) 56 | { 57 | sum += MiddleGamePieceValues[0][0][(int)Piece.P]; 58 | sum += MiddleGamePieceValues[0][0][(int)Piece.P]; 59 | sum += MiddleGamePieceValues[0][0][(int)Piece.P]; 60 | sum += MiddleGamePieceValues[0][0][(int)Piece.P]; 61 | sum += MiddleGamePieceValues[0][0][(int)Piece.P]; 62 | sum += MiddleGamePieceValues[0][0][(int)Piece.P]; 63 | } 64 | 65 | return sum; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Lynx/UCI/Commands/Engine/InfoCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Lynx.UCI.Commands.Engine; 2 | 3 | /// 4 | /// info 5 | /// the engine wants to send infos to the GUI. This should be done whenever one of the info has changed. 6 | /// The engine can send only selected infos and multiple infos can be send with one info command, 7 | /// e.g. "info currmove e2e4 currmovenumber 1" or 8 | /// "info depth 12 nodes 123456 nps 100000". 9 | /// Also all infos belonging to the pv should be sent together 10 | /// e.g. "info depth 2 score cp 214 time 1242 nodes 2124 nps 34928 pv e2e4 e7e5 g1f3" 11 | /// I suggest to start sending "currmove", "currmovenumber", "currline" and "refutation" only after one second 12 | /// to avoid too much traffic. 13 | /// Additional info: 14 | /// * depth 15 | /// search depth in plies 16 | /// * seldepth 17 | /// selective search depth in plies, 18 | /// if the engine sends seldepth there must also a "depth" be present in the same string. 19 | /// * time 20 | /// the time searched in ms, this should be sent together with the pv. 21 | /// * nodes 22 | /// x nodes searched, the engine should send this info regularly 23 | /// * pv ... 24 | /// the best line found 25 | /// * multipv 26 | /// this for the multi pv mode. 27 | /// for the best move / pv add "multipv 1" in the string when you send the pv. 28 | /// in k-best mode always send all k variants in k strings together. 29 | /// * score 30 | /// * cp 31 | /// the score from the engine's point of view in centipawns. 32 | /// * mate 33 | /// mate in y moves, not plies. 34 | /// If the engine is getting mated use negativ values for y. 35 | /// * lowerbound 36 | /// the score is just a lower bound. 37 | /// * upperbound 38 | /// the score is just an upper bound. 39 | /// * currmove 40 | /// currently searching this move 41 | /// * currmovenumber 42 | /// currently searching move number x, for the first move x should be 1 not 0. 43 | /// * hashfull 44 | /// the hash is x permill full, the engine should send this info regularly 45 | /// * nps 46 | /// x nodes per second searched, the engine should send this info regularly 47 | /// * tbhits 48 | /// x positions where found in the endgame table bases 49 | /// * sbhits 50 | /// x positions where found in the shredder endgame databases 51 | /// * cpuload 52 | /// the cpu usage of the engine is x permill. 53 | /// * string 54 | /// any string str which will be displayed be the engine, 55 | /// if there is a string command the rest of the line will be interpreted as . 56 | /// * refutation ... 57 | /// move is refuted by the line ... , i can be any number >= 1. 58 | /// Example: 59 | /// after move d1h5 is searched, the engine can send 60 | /// "info refutation d1h5 g6h5" 61 | /// if g6h5 is the best answer after d1h5 or if g6h5 refutes the move d1h5. 62 | /// if there is norefutation for d1h5 found, the engine should just send 63 | /// "info refutation d1h5" 64 | /// The engine should only send this if the option "UCI_ShowRefutations" is set to true. 65 | /// * currline ... 66 | /// this is the current line the engine is calculating. is the number of the cpu if 67 | /// the engine is running on more than one cpu. = 1,2,3.... 68 | /// if the engine is just using one cpu, can be omitted. 69 | /// If is greater than 1, always send all k lines in k strings together. 70 | /// The engine should only send this if the option "UCI_ShowCurrLine" is set to true. 71 | /// 72 | public sealed class InfoCommand 73 | { 74 | public const string Id = "info"; 75 | } 76 | -------------------------------------------------------------------------------- /src/Lynx.ConstantsGenerator/BitBoardsGenerator.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | 3 | namespace Lynx.ConstantsGenerator; 4 | 5 | #pragma warning disable S3353 // Unchanged local variables should be "const" - FP https://community.sonarsource.com/t/fp-s3353-value-modified-in-ref-extension-method/132389 6 | #pragma warning disable S106, S2228 // Standard outputs should not be used directly to log anything 7 | 8 | /// 9 | /// See layout 3 in https://tearth.dev/bitboard-viewer/ 10 | /// 11 | public static class BitBoardsGenerator 12 | { 13 | public static BitBoard NotAFile() 14 | { 15 | BitBoard b = default; 16 | 17 | for (int rank = 0; rank < 8; ++rank) 18 | { 19 | for (int file = 0; file < 8; ++file) 20 | { 21 | var squareIndex = BitBoardExtensions.SquareIndex(rank, file); 22 | 23 | if (file > 0) 24 | { 25 | b.SetBit(squareIndex); 26 | } 27 | } 28 | } 29 | 30 | b.Print(); 31 | 32 | return b; 33 | } 34 | 35 | public static BitBoard NotHFile() 36 | { 37 | BitBoard b = default; 38 | 39 | for (int rank = 0; rank < 8; ++rank) 40 | { 41 | for (int file = 0; file < 8; ++file) 42 | { 43 | var squareIndex = BitBoardExtensions.SquareIndex(rank, file); 44 | 45 | if (file < 7) 46 | { 47 | b.SetBit(squareIndex); 48 | } 49 | } 50 | } 51 | 52 | b.Print(); 53 | 54 | return b; 55 | } 56 | 57 | public static BitBoard NotABFiles() 58 | { 59 | BitBoard b = default; 60 | 61 | for (int rank = 0; rank < 8; ++rank) 62 | { 63 | for (int file = 0; file < 8; ++file) 64 | { 65 | var squareIndex = BitBoardExtensions.SquareIndex(rank, file); 66 | 67 | if (file > 1) 68 | { 69 | b.SetBit(squareIndex); 70 | } 71 | } 72 | } 73 | 74 | b.Print(); 75 | 76 | return b; 77 | } 78 | 79 | public static BitBoard NotHGFiles() 80 | { 81 | BitBoard b = default; 82 | 83 | for (int rank = 0; rank < 8; ++rank) 84 | { 85 | for (int file = 0; file < 8; ++file) 86 | { 87 | var squareIndex = BitBoardExtensions.SquareIndex(rank, file); 88 | 89 | if (file < 6) 90 | { 91 | b.SetBit(squareIndex); 92 | } 93 | } 94 | } 95 | 96 | b.Print(); 97 | 98 | return b; 99 | } 100 | 101 | public static void PrintSquares() 102 | { 103 | for (int rank = 8; rank >= 1; --rank) 104 | { 105 | //Console.WriteLine($"a{rank}, b{rank}, c{rank}, d{rank}, e{rank}, f{rank}, g{rank}, h{rank},"); 106 | Console.WriteLine($"\"a{rank}\", \"b{rank}\", \"c{rank}\", \"d{rank}\", \"e{rank}\", \"f{rank}\", \"g{rank}\", \"h{rank}\","); 107 | } 108 | } 109 | 110 | public static void PrintCoordinates() 111 | { 112 | for (int rank = 8; rank >= 1; --rank) 113 | { 114 | Console.WriteLine($"a{rank}, b{rank}, c{rank}, d{rank}, e{rank}, f{rank}, g{rank}, h{rank},"); 115 | } 116 | } 117 | } 118 | 119 | #pragma warning restore S3353 // Unchanged local variables should be "const" 120 | #pragma warning restore S106, S2228 // Standard outputs should not be used directly to log anything 121 | -------------------------------------------------------------------------------- /src/Lynx/Model/SearchResult.cs: -------------------------------------------------------------------------------- 1 | using Lynx.UCI.Commands.Engine; 2 | 3 | namespace Lynx.Model; 4 | 5 | public sealed class SearchResult 6 | { 7 | #if MULTITHREAD_DEBUG 8 | public int EngineId { get; init; } 9 | #endif 10 | 11 | public Move[] Moves { get; init; } 12 | 13 | public (int WDLWin, int WDLDraw, int WDLLoss)? WDL { get; set; } 14 | 15 | public ulong Nodes { get; set; } 16 | 17 | public ulong Time { get; set; } 18 | 19 | public ulong NodesPerSecond { get; set; } 20 | 21 | public int Score { get; init; } 22 | 23 | public int Depth { get; set; } 24 | 25 | public int Mate { get; init; } 26 | 27 | public int DepthReached { get; set; } 28 | 29 | public int HashfullPermill { get; set; } = -1; 30 | 31 | public Move BestMove { get; init; } 32 | 33 | #if MULTITHREAD_DEBUG 34 | public SearchResult(Move bestMove, int score, int targetDepth, Move[] moves, int mate = default) 35 | : this(-2, bestMove, score, targetDepth, moves, mate) 36 | { 37 | } 38 | #endif 39 | 40 | public SearchResult( 41 | #if MULTITHREAD_DEBUG 42 | int engineId, 43 | #endif 44 | Move bestMove, int score, int targetDepth, Move[] moves, int mate = default) 45 | { 46 | #if MULTITHREAD_DEBUG 47 | EngineId = engineId; 48 | #endif 49 | BestMove = bestMove; 50 | Score = score; 51 | Depth = targetDepth; 52 | Moves = moves; 53 | Mate = mate; 54 | } 55 | 56 | public override string ToString() 57 | { 58 | var sb = ObjectPools.StringBuilderPool.Get(); 59 | sb.EnsureCapacity(128 + (Moves.Length * 5)); 60 | 61 | #if MULTITHREAD_DEBUG 62 | sb.Append("[#").Append(EngineId).Append("] "); 63 | #endif 64 | 65 | var nps = NodesPerSecond; 66 | 67 | if (Configuration.EngineSettings.Threads > 1 68 | && Configuration.EngineSettings.EstimateMultithreadedSearchNPS 69 | && HashfullPermill == -1) // Not last info command 70 | { 71 | // Estimate total nps 72 | nps *= (ulong)Configuration.EngineSettings.Threads; 73 | 74 | // Remove the 5 less significative digits to hint that this is an estimate 75 | const int k = 100_000; 76 | nps = nps / k * k; 77 | } 78 | 79 | sb.Append(InfoCommand.Id) 80 | .Append(" depth ").Append(Depth) 81 | .Append(" seldepth ").Append(DepthReached) 82 | .Append(" multipv 1") 83 | .Append(" score ").Append(Mate == default ? "cp " + Lynx.WDL.NormalizeScore(Score) : "mate " + Mate) 84 | .Append(" nodes ").Append(Nodes) 85 | .Append(" nps ").Append(nps) 86 | .Append(" time ").Append(Time); 87 | 88 | if (HashfullPermill != -1) 89 | { 90 | sb.Append(" hashfull ").Append(HashfullPermill); 91 | } 92 | 93 | if (WDL is not null) 94 | { 95 | var (wdlWin, wdlDraw, wdlLoss) = WDL.Value; 96 | 97 | sb.Append(" wdl ") 98 | .Append(wdlWin).Append(' ') 99 | .Append(wdlDraw).Append(' ') 100 | .Append(wdlLoss); 101 | } 102 | 103 | sb.Append(" pv "); 104 | foreach (var move in Moves) 105 | { 106 | sb.Append(move.UCIStringMemoized()).Append(' '); 107 | } 108 | 109 | // Remove the trailing space 110 | if (Moves.Length > 0) 111 | { 112 | sb.Length--; 113 | } 114 | 115 | var result = sb.ToString(); 116 | 117 | ObjectPools.StringBuilderPool.Return(sb); 118 | 119 | return result; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Lynx.Benchmark/EncodeMove_Benchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using Lynx.Model; 3 | 4 | namespace Lynx.Benchmark; 5 | 6 | internal static class EncodeMoveImplementation 7 | { 8 | public static int EncodeMoveBool(int sourceSquare, int targetSquare, int piece, int promotedPiece = 0, bool isCapture = false, bool isDoublePawnPush = false, bool enPassant = false, bool isCastle = false) 9 | { 10 | var encodedMove = sourceSquare | (targetSquare << 6) | (piece << 12) | (promotedPiece << 16); 11 | 12 | if (isCapture) 13 | { 14 | encodedMove |= (1 << 20); 15 | } 16 | 17 | if (isDoublePawnPush) 18 | { 19 | encodedMove |= (1 << 21); 20 | } 21 | 22 | if (enPassant) 23 | { 24 | encodedMove |= (1 << 22); 25 | } 26 | 27 | if (isCastle) 28 | { 29 | encodedMove |= (1 << 23); 30 | } 31 | 32 | return encodedMove; 33 | } 34 | 35 | public static int EncodeMoveInt(int sourceSquare, int targetSquare, int piece, int promotedPiece = default, int isCapture = default, int isDoublePawnPush = default, int enPassant = default, int isCastle = 0) 36 | { 37 | return sourceSquare | (targetSquare << 6) | (piece << 12) | (promotedPiece << 16) 38 | | (isCapture << 20) 39 | | (isDoublePawnPush << 21) 40 | | (enPassant << 22) 41 | | (isCastle << 23); 42 | } 43 | } 44 | 45 | public class EncodeMove_Benchmark : BaseBenchmark 46 | { 47 | public static IEnumerable Data => [1, 10, 1_000, 10_000, 100_000]; 48 | 49 | [Benchmark(Baseline = true)] 50 | [ArgumentsSource(nameof(Data))] 51 | public void EncodeMoveBool(int iterations) 52 | { 53 | for (int i = 0; i < iterations; ++i) 54 | { 55 | EncodeMoveImplementation.EncodeMoveBool((int)BoardSquare.a1, (int)BoardSquare.h8, (int)Piece.q); 56 | EncodeMoveImplementation.EncodeMoveBool((int)BoardSquare.a7, (int)BoardSquare.a8, (int)Piece.K, promotedPiece: (int)Piece.N); 57 | EncodeMoveImplementation.EncodeMoveBool((int)BoardSquare.a7, (int)BoardSquare.b8, (int)Piece.K, promotedPiece: (int)Piece.N, isCapture: true); 58 | EncodeMoveImplementation.EncodeMoveBool((int)BoardSquare.e2, (int)BoardSquare.e4, (int)Piece.K, isDoublePawnPush: true); 59 | EncodeMoveImplementation.EncodeMoveBool((int)BoardSquare.c7, (int)BoardSquare.b6, (int)Piece.K, isCapture: true, enPassant: true); 60 | EncodeMoveImplementation.EncodeMoveBool((int)BoardSquare.e8, (int)BoardSquare.g8, (int)Piece.k, isCastle: true); 61 | } 62 | } 63 | 64 | /// 65 | /// ~70x faster 66 | /// 67 | [Benchmark] 68 | [ArgumentsSource(nameof(Data))] 69 | public void EncodeMoveInt(int iterations) 70 | { 71 | for (int i = 0; i < iterations; ++i) 72 | { 73 | EncodeMoveImplementation.EncodeMoveInt((int)BoardSquare.a1, (int)BoardSquare.h8, (int)Piece.q); 74 | EncodeMoveImplementation.EncodeMoveInt((int)BoardSquare.a7, (int)BoardSquare.a8, (int)Piece.K, promotedPiece: (int)Piece.N); 75 | EncodeMoveImplementation.EncodeMoveInt((int)BoardSquare.a7, (int)BoardSquare.b8, (int)Piece.K, promotedPiece: (int)Piece.N, isCapture: 1); 76 | EncodeMoveImplementation.EncodeMoveInt((int)BoardSquare.e2, (int)BoardSquare.e4, (int)Piece.K, isDoublePawnPush: 1); 77 | EncodeMoveImplementation.EncodeMoveInt((int)BoardSquare.c7, (int)BoardSquare.b6, (int)Piece.K, isCapture: 1, enPassant: 1); 78 | EncodeMoveImplementation.EncodeMoveInt((int)BoardSquare.e8, (int)BoardSquare.g8, (int)Piece.k, isCastle: 1); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/Lynx.Test/MoveGeneration/CastlingMoveTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | 4 | namespace Lynx.Test.MoveGeneration; 5 | 6 | public class CastlingMoveTest 7 | { 8 | private static readonly int _whiteShortCastle = MoveExtensions.EncodeShortCastle(Constants.InitialWhiteKingSquare, Constants.WhiteKingShortCastleSquare, (int)Piece.K); 9 | private static readonly int _whiteLongCastle = MoveExtensions.EncodeLongCastle(Constants.InitialWhiteKingSquare, Constants.WhiteKingLongCastleSquare, (int)Piece.K); 10 | private static readonly int _blackShortCastle = MoveExtensions.EncodeShortCastle(Constants.InitialBlackKingSquare, Constants.BlackKingShortCastleSquare, (int)Piece.k); 11 | private static readonly int _blackLongCastle = MoveExtensions.EncodeLongCastle(Constants.InitialBlackKingSquare, Constants.BlackKingLongCastleSquare, (int)Piece.k); 12 | 13 | [Test] 14 | public void WhiteShortCastling() 15 | { 16 | var position = new Position("r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w K - 0 1"); 17 | 18 | Span moveSpan = stackalloc Move[2]; 19 | var index = 0; 20 | 21 | Span attacks = stackalloc BitBoard[12]; 22 | Span attacksBySide = stackalloc BitBoard[2]; 23 | var evaluationContext = new EvaluationContext(attacks, attacksBySide); 24 | 25 | MoveGenerator.GenerateCastlingMoves(ref index, moveSpan, position, ref evaluationContext); 26 | 27 | var move = moveSpan[0]; 28 | Assert.IsTrue(move.IsCastle()); 29 | Assert.AreEqual(_whiteShortCastle, move); 30 | } 31 | 32 | [Test] 33 | public void WhiteLongCastling() 34 | { 35 | var position = new Position("r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w Q - 0 1"); 36 | 37 | Span moveSpan = stackalloc Move[2]; 38 | var index = 0; 39 | 40 | Span attacks = stackalloc BitBoard[12]; 41 | Span attacksBySide = stackalloc BitBoard[2]; 42 | var evaluationContext = new EvaluationContext(attacks, attacksBySide); 43 | 44 | MoveGenerator.GenerateCastlingMoves(ref index, moveSpan, position, ref evaluationContext); 45 | 46 | var move = moveSpan[0]; 47 | Assert.IsTrue(move.IsCastle()); 48 | Assert.AreEqual(_whiteLongCastle, move); 49 | } 50 | 51 | [Test] 52 | public void BlackShortCastling() 53 | { 54 | var position = new Position("r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R b k - 0 1"); 55 | 56 | Span moveSpan = stackalloc Move[2]; 57 | var index = 0; 58 | 59 | Span attacks = stackalloc BitBoard[12]; 60 | Span attacksBySide = stackalloc BitBoard[2]; 61 | var evaluationContext = new EvaluationContext(attacks, attacksBySide); 62 | 63 | MoveGenerator.GenerateCastlingMoves(ref index, moveSpan, position, ref evaluationContext); 64 | 65 | var move = moveSpan[0]; 66 | Assert.IsTrue(move.IsCastle()); 67 | Assert.AreEqual(_blackShortCastle, move); 68 | } 69 | 70 | [Test] 71 | public void BlackLongCastling() 72 | { 73 | var position = new Position("r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R b q - 0 1"); 74 | 75 | Span moveSpan = stackalloc Move[2]; 76 | var index = 0; 77 | 78 | Span attacks = stackalloc BitBoard[12]; 79 | Span attacksBySide = stackalloc BitBoard[2]; 80 | var evaluationContext = new EvaluationContext(attacks, attacksBySide); 81 | 82 | MoveGenerator.GenerateCastlingMoves(ref index, moveSpan, position, ref evaluationContext); 83 | 84 | var move = moveSpan[0]; 85 | Assert.IsTrue(move.IsCastle()); 86 | Assert.AreEqual(_blackLongCastle, move); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Lynx.Benchmark/PVTable_SumVsArrayAccess_Benchmark.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * | Method | Mean | Error | StdDev | Ratio | Allocated | 4 | * |------------ |---------:|---------:|---------:|------:|----------:| 5 | * | Sum | 66.41 us | 0.277 us | 0.245 us | 1.00 | - | 6 | * | ArrayAccess | 57.53 us | 0.546 us | 0.456 us | 0.87 | - | 7 | * 8 | * | Method | Mean | Error | StdDev | Ratio | Allocated | 9 | * |------------ |---------:|---------:|---------:|------:|----------:| 10 | * | Sum | 66.49 us | 0.235 us | 0.219 us | 1.00 | - | 11 | * | ArrayAccess | 58.05 us | 0.470 us | 0.392 us | 0.87 | - | 12 | * 13 | * 14 | * C.Sum() 15 | * L0000: push ebp 16 | * L0001: mov ebp, esp 17 | * L0003: xor eax, eax 18 | * L0005: xor edx, edx 19 | * L0007: mov ecx, edx 20 | * L0009: neg ecx 21 | * L000b: lea eax, [ecx+eax+0x504] 22 | * L0012: inc edx 23 | * L0013: cmp edx, 0x32 24 | * L0016: jl short L0007 25 | * L0018: pop ebp 26 | * L0019: ret 27 | * 28 | * C.ArrayAccess() 29 | * L0000: push ebp 30 | * L0001: mov ebp, esp 31 | * L0003: push edi 32 | * L0004: push esi 33 | * L0005: xor esi, esi 34 | * L0007: xor edi, edi 35 | * L0009: mov ecx, 0x13bdc5e8 36 | * L000e: xor edx, edx 37 | * L0010: call 0x72430560 38 | * L0015: mov eax, [eax] 39 | * L0017: mov edx, eax 40 | * L0019: cmp edi, [edx+4] 41 | * L001c: jae short L002e 42 | * L001e: add esi, [edx+edi*4+8] 43 | * L0022: inc edi 44 | * L0023: cmp edi, 0x32 45 | * L0026: jl short L0017 46 | * L0028: mov eax, esi 47 | * L002a: pop esi 48 | * L002b: pop edi 49 | * L002c: pop ebp 50 | * L002d: ret 51 | * L002e: call 0x72431100 52 | * L0033: int3 53 | */ 54 | 55 | using BenchmarkDotNet.Attributes; 56 | 57 | namespace Lynx.Benchmark; 58 | 59 | public class PVTable_SumVsArrayAccess_Benchmark : BaseBenchmark 60 | { 61 | private const int MaxDepth = 64; 62 | 63 | public PVTable_SumVsArrayAccess_Benchmark() 64 | { 65 | _ = PVTable.Indexes[0]; 66 | } 67 | 68 | [Benchmark(Baseline = true)] 69 | public int Sum() 70 | { 71 | var total = 0; 72 | 73 | for (int i = 0; i < 1000; ++i) 74 | { 75 | for (int depth = 0; depth < MaxDepth; ++depth) 76 | { 77 | total += 1234 + MaxDepth - depth; 78 | } 79 | } 80 | 81 | return total; 82 | } 83 | 84 | [Benchmark] 85 | public int ArrayAccess() 86 | { 87 | var total = 0; 88 | 89 | for (int i = 0; i < 1000; ++i) 90 | { 91 | for (int depth = 0; depth < MaxDepth; ++depth) 92 | { 93 | total += PVTable.Indexes[depth]; 94 | } 95 | } 96 | 97 | return total; 98 | } 99 | 100 | private static class PVTable 101 | { 102 | public static readonly int[] Indexes; 103 | 104 | #pragma warning disable S3963 // "static" fields should be initialized inline 105 | static PVTable() 106 | #pragma warning restore S3963 // "static" fields should be initialized inline 107 | { 108 | Indexes = new int[MaxDepth]; 109 | int previousPVIndex = 0; 110 | Indexes[0] = previousPVIndex; 111 | 112 | for (int depth = 0; depth < MaxDepth - 1; ++depth) 113 | { 114 | Indexes[depth + 1] = previousPVIndex + MaxDepth - depth; 115 | previousPVIndex = Indexes[depth + 1]; 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Lynx.Benchmark/PieceOffset_Boolean_Benchmark.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Pretty much inconclusive 3 | * 4 | * | Method | iterations | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | 5 | * |----------------- |----------- |---------------:|--------------:|--------------:|---------------:|------:|--------:|----------:| 6 | * | ThirdConditional | 1 | 0.0000 ns | 0.0000 ns | 0.0000 ns | 0.0000 ns | ? | ? | - | 7 | * | Branchless | 1 | 0.0053 ns | 0.0116 ns | 0.0108 ns | 0.0000 ns | ? | ? | - | 8 | * | | | | | | | | | | 9 | * | ThirdConditional | 10 | 3.6113 ns | 0.0294 ns | 0.0275 ns | 3.6002 ns | 1.00 | 0.00 | - | 10 | * | Branchless | 10 | 3.6180 ns | 0.0275 ns | 0.0229 ns | 3.6204 ns | 1.00 | 0.01 | - | 11 | * | | | | | | | | | | 12 | * | ThirdConditional | 1000 | 382.4481 ns | 16.1206 ns | 45.9929 ns | 359.7989 ns | 1.00 | 0.00 | - | 13 | * | Branchless | 1000 | 347.2697 ns | 1.0720 ns | 0.8952 ns | 347.3704 ns | 0.85 | 0.04 | - | 14 | * | | | | | | | | | | 15 | * | ThirdConditional | 10000 | 3,395.3388 ns | 10.5327 ns | 9.3370 ns | 3,391.9155 ns | 1.00 | 0.00 | - | 16 | * | Branchless | 10000 | 3,397.4954 ns | 10.6733 ns | 8.3330 ns | 3,397.0459 ns | 1.00 | 0.00 | - | 17 | * | | | | | | | | | | 18 | * | ThirdConditional | 100000 | 34,608.9655 ns | 504.2024 ns | 580.6402 ns | 34,499.9786 ns | 1.00 | 0.00 | - | 19 | * | Branchless | 100000 | 39,079.1962 ns | 1,431.5247 ns | 4,220.8828 ns | 38,997.9370 ns | 1.15 | 0.06 | - | 20 | * 21 | */ 22 | 23 | using BenchmarkDotNet.Attributes; 24 | using System.Runtime.CompilerServices; 25 | 26 | namespace Lynx.Benchmark; 27 | 28 | /// 29 | /// 30 | /// 31 | public static class PieceOffsetByBooleanImplementations 32 | { 33 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 34 | public static int ThirdConditional(bool isWhite) => isWhite ? 0 : 6; 35 | 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | public static int Branchless(bool isWhite) => 6 - (6 * Unsafe.As(ref isWhite)); 38 | } 39 | 40 | public class PieceOffset_Boolean_Benchmark : BaseBenchmark 41 | { 42 | public static IEnumerable Data => [1, 10, 1_000, 10_000, 100_000]; 43 | 44 | [Benchmark(Baseline = true)] 45 | [ArgumentsSource(nameof(Data))] 46 | public int ThirdConditional(int iterations) 47 | { 48 | var result = 0; 49 | for (int i = 0; i < iterations; ++i) 50 | { 51 | result += PieceOffsetByBooleanImplementations.ThirdConditional(true); 52 | result += PieceOffsetByBooleanImplementations.ThirdConditional(false); 53 | } 54 | 55 | return result; 56 | } 57 | 58 | [Benchmark] 59 | [ArgumentsSource(nameof(Data))] 60 | public int Branchless(int iterations) 61 | { 62 | var result = 0; 63 | for (int i = 0; i < iterations; ++i) 64 | { 65 | result += PieceOffsetByBooleanImplementations.Branchless(true); 66 | result += PieceOffsetByBooleanImplementations.Branchless(false); 67 | } 68 | 69 | return result; 70 | } 71 | } -------------------------------------------------------------------------------- /tests/Lynx.Test/ZobristHashGenerationTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | 4 | namespace Lynx.Test; 5 | 6 | public class ZobristHashGenerationTest 7 | { 8 | [Test] 9 | public void Repetition_InitialPosition() 10 | { 11 | var originalPosition = new Position(Constants.InitialPositionFEN); 12 | 13 | var position = new Position(originalPosition); 14 | position.MakeMove(MoveGenerator.GenerateAllMoves(originalPosition).Single(m => m.UCIString() == "g1f3")); 15 | position = new Position(position); 16 | position.MakeMove(MoveGenerator.GenerateAllMoves(position).Single(m => m.UCIString() == "g8f6")); 17 | position = new Position(position); 18 | position.MakeMove(MoveGenerator.GenerateAllMoves(position).Single(m => m.UCIString() == "f3g1")); 19 | position = new Position(position); 20 | position.MakeMove(MoveGenerator.GenerateAllMoves(position).Single(m => m.UCIString() == "f6g8")); 21 | 22 | Assert.AreEqual(originalPosition.UniqueIdentifier, position.UniqueIdentifier); 23 | } 24 | 25 | #pragma warning disable S4144 // Methods should not have identical implementations 26 | 27 | [TestCase("4k3/1P6/8/8/8/8/8/4K3 w - - 0 1", Description = "White promotion")] 28 | [TestCase("4k3/8/8/8/8/8/1p6/4K3 b - - 0 1", Description = "Black promotion")] 29 | [TestCase("rk6/1P6/8/8/8/8/8/4K3 w - - 0 1", Description = "White promotion and capture")] 30 | [TestCase("4k3/8/8/8/8/8/1p6/RK6 b - - 0 1", Description = "Black promotion and capture")] 31 | public void Promotion(string fen) 32 | { 33 | var originalPosition = new Position(fen); 34 | 35 | var fenDictionary = new Dictionary(Constants.MaxNumberOfPseudolegalMovesInAPosition) 36 | { 37 | [originalPosition.UniqueIdentifier] = ("", originalPosition.UniqueIdentifier) 38 | }; 39 | 40 | TransversePosition(originalPosition, fenDictionary); 41 | } 42 | 43 | [Explicit] 44 | [Category(Categories.LongRunning)] 45 | [TestCase(Constants.TrickyTestPositionFEN)] 46 | [TestCase(Constants.KillerTestPositionFEN)] 47 | public void EnPassant(string fen) 48 | { 49 | var originalPosition = new Position(fen); 50 | 51 | var uniqueIdDictionary = new Dictionary(Constants.MaxNumberOfPseudolegalMovesInAPosition) 52 | { 53 | [originalPosition.UniqueIdentifier] = ("", originalPosition.UniqueIdentifier) 54 | }; 55 | 56 | TransversePosition(originalPosition, uniqueIdDictionary); 57 | } 58 | 59 | #pragma warning restore S4144 // Methods should not have identical implementations 60 | 61 | private static void TransversePosition(Position originalPosition, Dictionary fenDictionary, int maxDepth = 10, int depth = 0) 62 | { 63 | foreach (var move in MoveGenerator.GenerateAllMoves(originalPosition)) 64 | { 65 | var newPosition = new Position(originalPosition); 66 | newPosition.MakeMove(move); 67 | if (!newPosition.IsValid()) 68 | { 69 | continue; 70 | } 71 | 72 | if (fenDictionary.TryGetValue(newPosition.UniqueIdentifier, out var pair)) 73 | { 74 | Assert.AreEqual(pair.Hash, newPosition.UniqueIdentifier, $"From {originalPosition.FEN()} using {move}: {newPosition.FEN()}"); 75 | } 76 | else 77 | { 78 | fenDictionary.Add(newPosition.UniqueIdentifier, (move.ToString(), newPosition.UniqueIdentifier)); 79 | } 80 | 81 | if (depth < maxDepth) 82 | { 83 | TransversePosition(newPosition, fenDictionary, maxDepth, ++depth); 84 | } 85 | 86 | Assert.AreEqual(fenDictionary.Count, fenDictionary.Values.Distinct().Count()); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Lynx.Test/Commands/PositionCommandTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using Lynx.UCI.Commands.GUI; 3 | using Moq; 4 | using NUnit.Framework; 5 | using System.Threading.Channels; 6 | 7 | namespace Lynx.Test.Commands; 8 | 9 | /// 10 | /// https://github.com/lynx-chess/Lynx/issues/31 11 | /// 12 | public class PositionCommandTest 13 | { 14 | [TestCase] 15 | public async Task PositionCommandShouldNotTakeIntoAccountInternalState() 16 | { 17 | // Arrange 18 | var engine = new Engine(new Mock>().Object); 19 | engine.NewGame(); 20 | engine.AdjustPosition($"position fen {Constants.InitialPositionFEN} moves e2e4"); 21 | var goCommand = new GoCommand($"go depth {Engine.DefaultMaxDepth}"); 22 | var searchConstraints = TimeManager.CalculateTimeManagement(engine.Game, goCommand); 23 | 24 | using var cts = new CancellationTokenSource(); 25 | var resultTask = Task.Run(() => engine.BestMove(searchConstraints, isPondering: false, cts.Token, CancellationToken.None)); 26 | 27 | await cts.CancelAsync(); 28 | await resultTask; 29 | 30 | // Act 31 | engine.AdjustPosition($"position fen {Constants.InitialPositionFEN} moves d2d4"); 32 | 33 | // Assert 34 | #if DEBUG 35 | Assert.AreEqual(1, engine.Game.MoveHistory.Count); 36 | #endif 37 | 38 | Assert.Pass(); 39 | } 40 | 41 | [TestCase("position startpos moves d2d4 g8f6 g1f3 d7d5 b1c3 e7e6 g2g3 c7c5 e2e3")] 42 | [TestCase(" position startpos moves d2d4 g8f6 g1f3 d7d5 b1c3 e7e6 g2g3 c7c5 e2e3 ")] 43 | [TestCase("position fen rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 moves d2d4 g8f6 g1f3 d7d5 b1c3 e7e6 g2g3 c7c5 e2e3")] 44 | [TestCase(" position fen rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 moves d2d4 g8f6 g1f3 d7d5 b1c3 e7e6 g2g3 c7c5 e2e3 ")] 45 | public void ParseGame_Spaces(string positionCommand) 46 | { 47 | var parsedGame = new Game(Constants.InitialPositionFEN); 48 | parsedGame.ParsePositionCommand(positionCommand); 49 | 50 | #if DEBUG 51 | Assert.AreEqual("d2d4", parsedGame.MoveHistory[0].UCIString()); 52 | Assert.AreEqual("g8f6", parsedGame.MoveHistory[1].UCIString()); 53 | Assert.AreEqual("g1f3", parsedGame.MoveHistory[2].UCIString()); 54 | Assert.AreEqual("d7d5", parsedGame.MoveHistory[3].UCIString()); 55 | Assert.AreEqual("b1c3", parsedGame.MoveHistory[4].UCIString()); 56 | Assert.AreEqual("e7e6", parsedGame.MoveHistory[5].UCIString()); 57 | Assert.AreEqual("g2g3", parsedGame.MoveHistory[6].UCIString()); 58 | Assert.AreEqual("c7c5", parsedGame.MoveHistory[7].UCIString()); 59 | Assert.AreEqual("e2e3", parsedGame.MoveHistory[8].UCIString()); 60 | Assert.AreEqual("rnbqkb1r/pp3ppp/4pn2/2pp4/3P4/2N1PNP1/PPP2P1P/R1BQKB1R b KQkq - 0 5", parsedGame.CurrentPosition.FEN(parsedGame.HalfMovesWithoutCaptureOrPawnMove, (parsedGame.MoveHistory.Count / 2) + (parsedGame.MoveHistory.Count % 2))); 61 | #endif 62 | Assert.AreEqual("rnbqkb1r/pp3ppp/4pn2/2pp4/3P4/2N1PNP1/PPP2P1P/R1BQKB1R b KQkq - 0 5", parsedGame.CurrentPosition.FEN(parsedGame.HalfMovesWithoutCaptureOrPawnMove, (parsedGame.PositionHashHistoryLength() / 2) + (parsedGame.PositionHashHistoryLength() % 2))); 63 | 64 | Assert.Pass(); 65 | } 66 | 67 | /// 68 | /// 296 moves https://lichess.org/RViT3UWL2yy0 69 | /// 70 | [Test] 71 | public void ParseGame_Long() 72 | { 73 | var parsedGame = new Game(Constants.InitialPositionFEN); 74 | parsedGame.ParsePositionCommand(Constants.LongPositionCommand); 75 | 76 | Assert.AreNotEqual(Constants.InitialPositionFEN, parsedGame.CurrentPosition); 77 | Assert.Greater(parsedGame.PositionHashHistoryLength(), 500); 78 | 79 | #if DEBUG 80 | Assert.Greater(parsedGame.MoveHistory.Count, 590); 81 | #endif 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Lynx.Test/PSQTTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | 4 | using static Lynx.EvaluationPSQTs; 5 | using static Lynx.TunableEvalParameters; 6 | 7 | namespace Lynx.Test; 8 | 9 | public class PSQTTest 10 | { 11 | [Test] 12 | public void PackedEvaluation() 13 | { 14 | short[][][] mgFriend = 15 | [ 16 | MiddleGamePawnTable, 17 | MiddleGameKnightTable, 18 | MiddleGameBishopTable, 19 | MiddleGameRookTable, 20 | MiddleGameQueenTable, 21 | MiddleGameKingTable 22 | ]; 23 | 24 | short[][][] egFriend = 25 | [ 26 | EndGamePawnTable, 27 | EndGameKnightTable, 28 | EndGameBishopTable, 29 | EndGameRookTable, 30 | EndGameQueenTable, 31 | EndGameKingTable 32 | ]; 33 | 34 | short[][][] mgEnemy = 35 | [ 36 | MiddleGameEnemyPawnTable, 37 | MiddleGameEnemyKnightTable, 38 | MiddleGameEnemyBishopTable, 39 | MiddleGameEnemyRookTable, 40 | MiddleGameEnemyQueenTable, 41 | MiddleGameEnemyKingTable 42 | ]; 43 | 44 | short[][][] egEnemy = 45 | [ 46 | EndGameEnemyPawnTable, 47 | EndGameEnemyKnightTable, 48 | EndGameEnemyBishopTable, 49 | EndGameEnemyRookTable, 50 | EndGameEnemyQueenTable, 51 | EndGameEnemyKingTable 52 | ]; 53 | 54 | for (int friendBucket = 0; friendBucket < PSQTBucketCount; ++friendBucket) 55 | { 56 | for (int enemyBucket = 0; enemyBucket < PSQTBucketCount; ++enemyBucket) 57 | { 58 | for (int piece = (int)Piece.P; piece <= (int)Piece.k; ++piece) 59 | { 60 | for (int sq = 0; sq < 64; ++sq) 61 | { 62 | int mg, eg; 63 | 64 | if (piece < (int)Piece.p) // white piece 65 | { 66 | mg = 67 | MiddleGamePieceValues[0][friendBucket][piece] + mgFriend[piece][friendBucket][sq] 68 | + MiddleGamePieceValues[1][enemyBucket][piece] + mgEnemy[piece][enemyBucket][sq]; 69 | 70 | eg = 71 | EndGamePieceValues[0][friendBucket][piece] + egFriend[piece][friendBucket][sq] 72 | + EndGamePieceValues[1][enemyBucket][piece] + egEnemy[piece][enemyBucket][sq]; 73 | } 74 | else // black piece 75 | { 76 | int basePiece = piece - 6; 77 | // Mirror square and subtract (equivalent to adding the pre-negated table) 78 | var mirror = sq ^ 56; 79 | 80 | mg = 81 | MiddleGamePieceValues[0][friendBucket][piece] - mgFriend[basePiece][friendBucket][mirror] 82 | + MiddleGamePieceValues[1][enemyBucket][piece] - mgEnemy[basePiece][enemyBucket][mirror]; 83 | 84 | eg = 85 | EndGamePieceValues[0][friendBucket][piece] - egFriend[basePiece][friendBucket][mirror] 86 | + EndGamePieceValues[1][enemyBucket][piece] - egEnemy[basePiece][enemyBucket][mirror]; 87 | } 88 | 89 | var packed = PSQT(friendBucket, enemyBucket, piece, sq); 90 | Assert.AreEqual(mg, Utils.UnpackMG(packed), $"MG mismatch piece {piece} sq {sq} fb {friendBucket} eb {enemyBucket}"); 91 | Assert.AreEqual(eg, Utils.UnpackEG(packed), $"EG mismatch piece {piece} sq {sq} fb {friendBucket} eb {enemyBucket}"); 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Lynx.Benchmark/PEXT_Benchmark.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * BenchmarkDotNet v0.13.9+228a464e8be6c580ad9408e98f18813f6407fb5a, Ubuntu 22.04.3 LTS (Jammy Jellyfish) 4 | * Intel Xeon Platinum 8171M CPU 2.60GHz, 1 CPU, 2 logical and 2 physical cores 5 | * .NET SDK 8.0.100-rc.2.23502.2 6 | * [Host] : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2 7 | * DefaultJob : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2 8 | * 9 | * 10 | * | Method | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio | 11 | * |------------- |---------:|--------:|--------:|------:|----------:|------------:| 12 | * | MagicNumbers | 378.1 ns | 6.19 ns | 5.79 ns | 1.00 | - | NA | 13 | * | PEXT | 229.7 ns | 2.79 ns | 2.61 ns | 0.61 | - | NA | 14 | * 15 | * BenchmarkDotNet v0.13.9+228a464e8be6c580ad9408e98f18813f6407fb5a, Windows 10 (10.0.20348.2031) (Hyper-V) 16 | * Intel Xeon CPU E5-2673 v4 2.30GHz, 1 CPU, 2 logical and 2 physical cores 17 | * .NET SDK 8.0.100-rc.2.23502.2 18 | * [Host] : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2 19 | * DefaultJob : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2 20 | * 21 | * 22 | * | Method | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | 23 | * |------------- |---------:|--------:|---------:|------:|--------:|----------:|------------:| 24 | * | MagicNumbers | 408.9 ns | 8.14 ns | 13.59 ns | 1.00 | 0.00 | - | NA | 25 | * | PEXT | 326.3 ns | 6.46 ns | 7.93 ns | 0.79 | 0.03 | - | NA | 26 | * 27 | * BenchmarkDotNet v0.13.9+228a464e8be6c580ad9408e98f18813f6407fb5a, macOS Monterey 12.6.9 (21G726) [Darwin 1.6.0] 28 | * Intel Core i7-8700B CPU 3.20GHz (Max: 3.19GHz) (Coffee Lake), 1 CPU, 4 logical and 4 physical cores 29 | * .NET SDK 8.0.100-rc.2.23502.2 30 | * [Host] : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2 31 | * DefaultJob : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2 32 | * 33 | * 34 | * | Method | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | 35 | * |------------- |---------:|---------:|---------:|------:|--------:|----------:|------------:| 36 | * | MagicNumbers | 436.3 ns | 28.75 ns | 84.33 ns | 1.00 | 0.00 | - | NA | 37 | * | PEXT | 274.5 ns | 20.23 ns | 58.69 ns | 0.66 | 0.19 | - | NA | 38 | * 39 | */ 40 | 41 | using BenchmarkDotNet.Attributes; 42 | using Lynx.Model; 43 | 44 | namespace Lynx.Benchmark; 45 | public class PEXTBenchmark_Benchmark : BaseBenchmark 46 | { 47 | private readonly Position _position = new(Constants.TrickyTestPositionFEN); 48 | 49 | [Benchmark(Baseline = true)] 50 | public ulong MagicNumbers() 51 | { 52 | ulong result = default; 53 | 54 | for (int i = 0; i < 64; ++i) 55 | { 56 | result |= MagicNumbersRookAttacks(i, _position.OccupancyBitBoards[0]); 57 | result |= MagicNumbersBishopAttacks(i, _position.OccupancyBitBoards[0]); 58 | } 59 | 60 | return result; 61 | } 62 | 63 | [Benchmark] 64 | public ulong PEXT() 65 | { 66 | ulong result = default; 67 | 68 | for (int i = 0; i < 64; ++i) 69 | { 70 | result |= PEXTRookAttacks(i, _position.OccupancyBitBoards[0]); 71 | result |= PEXTBishopAttacks(i, _position.OccupancyBitBoards[0]); 72 | } 73 | 74 | return result; 75 | } 76 | 77 | private static BitBoard MagicNumbersRookAttacks(int squareIndex, BitBoard occupancy) => Attacks.MagicNumbersRookAttacks(squareIndex, occupancy); 78 | 79 | private static BitBoard PEXTRookAttacks(int squareIndex, BitBoard occupancy) => Attacks.RookAttacks(squareIndex, occupancy); 80 | 81 | private static BitBoard MagicNumbersBishopAttacks(int squareIndex, BitBoard occupancy) => Attacks.MagicNumbersBishopAttacks(squareIndex, occupancy); 82 | 83 | private static BitBoard PEXTBishopAttacks(int squareIndex, BitBoard occupancy) => Attacks.BishopAttacks(squareIndex, occupancy); 84 | } 85 | -------------------------------------------------------------------------------- /src/Lynx.Benchmark/ResetLS1BvsWithoutLS1B_Benchmark.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Not much noticeable difference with this benchmark 3 | * 4 | * | Method | position | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | 5 | * |------------ |-------------------- |---------:|---------:|---------:|---------:|------:|--------:|----------:| 6 | * | ResetLS1B | Lynx.Model.Position | 14.58 ns | 0.503 ns | 1.403 ns | 14.02 ns | 1.00 | 0.00 | - | 7 | * | ResetLS1B | Lynx.Model.Position | 14.79 ns | 0.593 ns | 1.683 ns | 14.02 ns | 1.02 | 0.13 | - | 8 | * | ResetLS1B | Lynx.Model.Position | 13.92 ns | 0.316 ns | 0.351 ns | 13.87 ns | 0.96 | 0.08 | - | 9 | * | ResetLS1B | Lynx.Model.Position | 13.58 ns | 0.310 ns | 0.369 ns | 13.52 ns | 0.94 | 0.08 | - | 10 | * | ResetLS1B | Lynx.Model.Position | 15.44 ns | 0.808 ns | 2.332 ns | 14.21 ns | 1.07 | 0.19 | - | 11 | * | ResetLS1B | Lynx.Model.Position | 13.87 ns | 0.317 ns | 0.445 ns | 13.84 ns | 0.95 | 0.10 | - | 12 | * | WithoutLS1B | Lynx.Model.Position | 13.64 ns | 0.315 ns | 0.518 ns | 13.49 ns | 0.94 | 0.09 | - | 13 | * | WithoutLS1B | Lynx.Model.Position | 14.64 ns | 0.647 ns | 1.856 ns | 13.75 ns | 1.01 | 0.16 | - | 14 | * | WithoutLS1B | Lynx.Model.Position | 14.12 ns | 0.275 ns | 0.679 ns | 13.97 ns | 0.97 | 0.10 | - | 15 | * | WithoutLS1B | Lynx.Model.Position | 14.11 ns | 0.261 ns | 0.310 ns | 14.06 ns | 0.98 | 0.07 | - | 16 | * | WithoutLS1B | Lynx.Model.Position | 14.05 ns | 0.315 ns | 0.294 ns | 14.00 ns | 0.98 | 0.07 | - | 17 | * | WithoutLS1B | Lynx.Model.Position | 14.08 ns | 0.303 ns | 0.284 ns | 14.10 ns | 0.98 | 0.06 | - | 18 | * 19 | */ 20 | 21 | using BenchmarkDotNet.Attributes; 22 | using Lynx.Model; 23 | 24 | namespace Lynx.Benchmark; 25 | 26 | public static class BenchmarkExtensions 27 | { 28 | public static void ResetLS1BBenchmark(this ref BitBoard board) => board &= (board - 1); 29 | public static BitBoard WithoutLS1BBenchmark(this BitBoard board) => board & (board - 1); 30 | } 31 | 32 | public class ResetLS1BvsWithoutLS1B_Benchmark : BaseBenchmark 33 | { 34 | public static IEnumerable Data => 35 | [ 36 | new Position(Constants.InitialPositionFEN), 37 | new Position(Constants.TrickyTestPositionFEN), 38 | new Position(Constants.TrickyTestPositionReversedFEN), 39 | new Position(Constants.CmkTestPositionFEN), 40 | new Position(Constants.ComplexPositionFEN), 41 | new Position(Constants.KillerTestPositionFEN), 42 | ]; 43 | 44 | [Benchmark(Baseline = true)] 45 | [ArgumentsSource(nameof(Data))] 46 | public ulong ResetLS1B(Position position) 47 | { 48 | ulong counter = 0; 49 | 50 | for (int i = 0; i < position.PieceBitBoards.Length; ++i) 51 | { 52 | var bitboard = position.PieceBitBoards[i]; 53 | bitboard.ResetLS1BBenchmark(); 54 | counter += bitboard; 55 | } 56 | 57 | for (int i = 0; i < position.OccupancyBitBoards.Length; ++i) 58 | { 59 | var bitboard = position.OccupancyBitBoards[i]; 60 | bitboard.ResetLS1BBenchmark(); 61 | counter += bitboard; 62 | } 63 | 64 | return counter; 65 | } 66 | 67 | [Benchmark] 68 | [ArgumentsSource(nameof(Data))] 69 | public ulong WithoutLS1B(Position position) 70 | { 71 | ulong counter = 0; 72 | 73 | for (int i = 0; i < position.PieceBitBoards.Length; ++i) 74 | { 75 | var bitboard = position.PieceBitBoards[i]; 76 | counter += bitboard.WithoutLS1BBenchmark(); 77 | } 78 | 79 | for (int i = 0; i < position.OccupancyBitBoards.Length; ++i) 80 | { 81 | var bitboard = position.OccupancyBitBoards[i]; 82 | counter += bitboard.WithoutLS1BBenchmark(); 83 | } 84 | 85 | return counter; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Lynx.Benchmark/ZobristHash_EnPassant_Benchmark.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * BenchmarkDotNet v0.13.11, Ubuntu 22.04.3 LTS (Jammy Jellyfish) 4 | * AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores 5 | * .NET SDK 8.0.100 6 | * [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 7 | * DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 8 | * 9 | * | Method | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | 10 | * |--------- |----------:|----------:|----------:|------:|--------:|----------:|------------:| 11 | * | Module | 0.3307 ns | 0.0075 ns | 0.0066 ns | 1.00 | 0.00 | - | NA | 12 | * | AndTrick | 0.3286 ns | 0.0071 ns | 0.0056 ns | 0.99 | 0.03 | - | NA | 13 | * 14 | * 15 | * BenchmarkDotNet v0.13.11, Windows 10 (10.0.20348.2159) (Hyper-V) 16 | * AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores 17 | * .NET SDK 8.0.100 18 | * [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 19 | * DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 20 | * | Method | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio | 21 | * |--------- |----------:|----------:|----------:|------:|----------:|------------:| 22 | * | Module | 0.5209 ns | 0.0014 ns | 0.0013 ns | 1.00 | - | NA | 23 | * | AndTrick | 0.4598 ns | 0.0018 ns | 0.0014 ns | 0.88 | - | NA | 24 | * 25 | * 26 | * BenchmarkDotNet v0.13.11, macOS Monterey 12.7.2 (21G1974) [Darwin 21.6.0] 27 | * Intel Xeon CPU E5-1650 v2 3.50GHz (Max: 3.34GHz), 1 CPU, 3 logical and 3 physical cores 28 | * .NET SDK 8.0.100 29 | * [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX 30 | * DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX 31 | * 32 | * | Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | 33 | * |--------- |----------:|----------:|----------:|----------:|------:|--------:|----------:|------------:| 34 | * | Module | 0.5491 ns | 0.0484 ns | 0.1092 ns | 0.5070 ns | 1.00 | 0.00 | - | NA | 35 | * | AndTrick | 0.7057 ns | 0.0292 ns | 0.0287 ns | 0.6935 ns | 1.18 | 0.27 | - | NA | 36 | * 37 | */ 38 | 39 | using BenchmarkDotNet.Attributes; 40 | using Lynx.Model; 41 | using System.Runtime.CompilerServices; 42 | 43 | namespace Lynx.Benchmark; 44 | public class ZobristHash_EnPassant_Benchmark : BaseBenchmark 45 | { 46 | private static readonly long[,] _table = Initialize(); 47 | 48 | [Benchmark(Baseline = true)] 49 | public long Module() => Module((int)BoardSquare.c6); 50 | 51 | [Benchmark] 52 | public long AndTrick() => AndTrick((int)BoardSquare.c6); 53 | 54 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 55 | private static long Module(int enPassantSquare) 56 | { 57 | if (enPassantSquare == (int)BoardSquare.noSquare) 58 | { 59 | return default; 60 | } 61 | 62 | var file = enPassantSquare % 8; 63 | 64 | return _table[file, (int)Piece.P]; 65 | } 66 | 67 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 68 | private static long AndTrick(int enPassantSquare) 69 | { 70 | if (enPassantSquare == (int)BoardSquare.noSquare) 71 | { 72 | return default; 73 | } 74 | 75 | var file = enPassantSquare & 0x07; 76 | 77 | return _table[file, (int)Piece.P]; 78 | } 79 | 80 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 81 | private static long[,] Initialize() 82 | { 83 | var zobristTable = new long[64, 12]; 84 | var randomInstance = new Random(int.MaxValue); 85 | 86 | for (int squareIndex = 0; squareIndex < 64; ++squareIndex) 87 | { 88 | for (int pieceIndex = 0; pieceIndex < 12; ++pieceIndex) 89 | { 90 | zobristTable[squareIndex, pieceIndex] = randomInstance.NextInt64(); 91 | } 92 | } 93 | 94 | return zobristTable; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/Lynx.Test/Model/MoveScoreTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | 4 | namespace Lynx.Test.Model; 5 | 6 | public class MoveScoreTest : BaseTest 7 | { 8 | /// 9 | /// 'Tricky position' 10 | /// 8 r . . . k . . r 11 | /// 7 p . p p q p b . 12 | /// 6 b n . . p n p . 13 | /// 5 . . . P N . . . 14 | /// 4 . p . . P . . . 15 | /// 3 . . N . . Q . p 16 | /// 2 P P P B B P P P 17 | /// 1 R . . . K . . R 18 | /// a b c d e f g h 19 | /// This tests indirectly 20 | /// 21 | [TestCase(Constants.TrickyTestPositionFEN)] 22 | public void MoveScore(string fen) 23 | { 24 | var engine = GetEngine(fen); 25 | 26 | var allMoves = MoveGenerator.GenerateAllMoves(engine.Game.CurrentPosition).OrderByDescending(move => 27 | { 28 | Span attacks = stackalloc BitBoard[12]; 29 | Span attacksBySide = stackalloc BitBoard[2]; 30 | var evaluationContext = new EvaluationContext(attacks, attacksBySide); 31 | return engine.ScoreMove(engine.Game.CurrentPosition, move, default, ref evaluationContext); 32 | }).ToList(); 33 | 34 | Assert.AreEqual("e2a6", allMoves[0].UCIString()); // BxB 35 | Assert.AreEqual("d5e6", allMoves[1].UCIString()); // PxP 36 | Assert.AreEqual("g2h3", allMoves[2].UCIString()); // PxP 37 | Assert.AreEqual("f3f6", allMoves[3].UCIString()); // QxN 38 | Assert.AreEqual("e5d7", allMoves[4].UCIString()); // NxP 39 | Assert.AreEqual("e5f7", allMoves[5].UCIString()); // NxP 40 | Assert.AreEqual("e5g6", allMoves[6].UCIString()); // NxP 41 | Assert.AreEqual("f3h3", allMoves[7].UCIString()); // QxP 42 | 43 | Span attacks = stackalloc BitBoard[12]; 44 | Span attacksBySide = stackalloc BitBoard[2]; 45 | var evaluationContext = new EvaluationContext(attacks, attacksBySide); 46 | 47 | foreach (var move in allMoves.Where(move => move.CapturedPiece() == (int)Piece.None && !move.IsCastle())) 48 | { 49 | Assert.AreEqual(EvaluationConstants.BaseMoveScore, engine.ScoreMove(engine.Game.CurrentPosition, move, default, ref evaluationContext)); 50 | } 51 | } 52 | 53 | /// 54 | /// Only one capture, en passant, both sides 55 | /// 8 r n b q k b n r 56 | /// 7 p p p . p p p p 57 | /// 6 . . . . . . . . 58 | /// 5 . . . p P . . . 59 | /// 4 . . . . . . . . 60 | /// 3 . . . . . . . . 61 | /// 2 P P P P . P P P 62 | /// 1 R N B Q K B N R 63 | /// a b c d e f g h 64 | /// 65 | [TestCase("rnbqkbnr/ppp1pppp/8/3pP3/8/8/PPPP1PPP/RNBQKBNR w KQkq d6 0 1", "e5d6")] 66 | [TestCase("rnbqkbnr/ppp1pppp/8/8/3pP3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1", "d4e3")] 67 | public void MoveScoreEnPassant(string fen, string moveWithHighestScore) 68 | { 69 | var engine = GetEngine(fen); 70 | 71 | var allMoves = MoveGenerator.GenerateAllMoves(engine.Game.CurrentPosition).OrderByDescending(move => 72 | { 73 | Span attacks = stackalloc BitBoard[12]; 74 | Span attacksBySide = stackalloc BitBoard[2]; 75 | var evaluationContext = new EvaluationContext(attacks, attacksBySide); 76 | return engine.ScoreMove(engine.Game.CurrentPosition, move, default, ref evaluationContext); 77 | }).ToList(); 78 | 79 | Assert.AreEqual(moveWithHighestScore, allMoves[0].UCIString()); 80 | Span attacks = stackalloc BitBoard[12]; 81 | Span attacksBySide = stackalloc BitBoard[2]; 82 | var evaluationContext = new EvaluationContext(attacks, attacksBySide); 83 | Assert.AreEqual(EvaluationConstants.GoodCaptureMoveBaseScoreValue + EvaluationConstants.MostValueableVictimLeastValuableAttacker[0][6], engine.ScoreMove(engine.Game.CurrentPosition, allMoves[0], default, ref evaluationContext)); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Lynx.ConstantsGenerator/PawnIslandsGenerator.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | 3 | namespace Lynx.ConstantsGenerator; 4 | 5 | public static class PawnIslandsGenerator 6 | { 7 | /// 8 | /// Result of the generation 9 | /// 10 | public static ReadOnlySpan PawnIslandsCount => 11 | [ 12 | 0, 1, 1, 1, 1, 2, 1, 1, 1, 2, 13 | 2, 2, 1, 2, 1, 1, 1, 2, 2, 2, 14 | 2, 3, 2, 2, 1, 2, 2, 2, 1, 2, 15 | 1, 1, 1, 2, 2, 2, 2, 3, 2, 2, 16 | 2, 3, 3, 3, 2, 3, 2, 2, 1, 2, 17 | 2, 2, 2, 3, 2, 2, 1, 2, 2, 2, 18 | 1, 2, 1, 1, 1, 2, 2, 2, 2, 3, 19 | 2, 2, 2, 3, 3, 3, 2, 3, 2, 2, 20 | 2, 3, 3, 3, 3, 4, 3, 3, 2, 3, 21 | 3, 3, 2, 3, 2, 2, 1, 2, 2, 2, 22 | 2, 3, 2, 2, 2, 3, 3, 3, 2, 3, 23 | 2, 2, 1, 2, 2, 2, 2, 3, 2, 2, 24 | 1, 2, 2, 2, 1, 2, 1, 1, 1, 2, 25 | 2, 2, 2, 3, 2, 2, 2, 3, 3, 3, 26 | 2, 3, 2, 2, 2, 3, 3, 3, 3, 4, 27 | 3, 3, 2, 3, 3, 3, 2, 3, 2, 2, 28 | 2, 3, 3, 3, 3, 4, 3, 3, 3, 4, 29 | 4, 4, 3, 4, 3, 3, 2, 3, 3, 3, 30 | 3, 4, 3, 3, 2, 3, 3, 3, 2, 3, 31 | 2, 2, 1, 2, 2, 2, 2, 3, 2, 2, 32 | 2, 3, 3, 3, 2, 3, 2, 2, 2, 3, 33 | 3, 3, 3, 4, 3, 3, 2, 3, 3, 3, 34 | 2, 3, 2, 2, 1, 2, 2, 2, 2, 3, 35 | 2, 2, 2, 3, 3, 3, 2, 3, 2, 2, 36 | 1, 2, 2, 2, 2, 3, 2, 2, 1, 2, 37 | 2, 2, 1, 2, 1, 38 | 1 39 | ]; 40 | 41 | /// 42 | /// Used to generate 43 | /// 44 | public static void GeneratePawnIslands() 45 | { 46 | Span result = stackalloc int[byte.MaxValue]; 47 | 48 | for (byte n = byte.MinValue; n < byte.MaxValue; ++n) 49 | { 50 | #pragma warning disable S3353 // Unchanged local variables should be "const" - FP https://community.sonarsource.com/t/fp-s3353-value-modified-in-ref-extension-method/132389 51 | BitBoard bitboard = 0; 52 | #pragma warning restore S3353 // Unchanged local variables should be "const" 53 | 54 | for (int file = 0; file < 8; ++file) 55 | { 56 | var pawnInFile = n.GetBit(file); 57 | 58 | if (pawnInFile) 59 | { 60 | bitboard.SetBit(file); 61 | } 62 | } 63 | 64 | result[n] = IdentifyIslands(bitboard); 65 | } 66 | 67 | Console.Write("\t[\n\t\t"); 68 | 69 | for (int i = 0; i < result.Length; ++i) 70 | { 71 | Console.Write(result[i]); 72 | 73 | if ((i + 1) % 10 == 0) 74 | { 75 | Console.Write($",{Environment.NewLine}\t\t"); 76 | } 77 | else 78 | { 79 | Console.Write(", "); 80 | } 81 | } 82 | 83 | // Manual last bit, all pawns 84 | Console.WriteLine("\n\t\t1"); 85 | 86 | Console.Write("\t]"); 87 | } 88 | 89 | private static bool GetBit(this byte board, int file) 90 | { 91 | return (board & (1 << file)) != default; 92 | } 93 | 94 | public static int IdentifyIslands(BitBoard pawns) 95 | { 96 | const int n = 1; 97 | 98 | Span files = stackalloc int[8]; 99 | 100 | while (pawns != default) 101 | { 102 | var squareIndex = pawns.GetLS1BIndex(); 103 | pawns.ResetLS1B(); 104 | 105 | files[Constants.File[squareIndex]] = n; 106 | } 107 | 108 | var islandCount = 0; 109 | var isIsland = false; 110 | 111 | for (int file = 0; file < files.Length; ++file) 112 | { 113 | if (files[file] == n) 114 | { 115 | if (!isIsland) 116 | { 117 | isIsland = true; 118 | ++islandCount; 119 | } 120 | } 121 | else 122 | { 123 | isIsland = false; 124 | } 125 | } 126 | 127 | return islandCount; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Lynx.Benchmark/IsDarkSquare_IsLightSquare_Benchmark.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * BenchmarkDotNet v0.13.12, Ubuntu 22.04.3 LTS (Jammy Jellyfish) 4 | * AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores 5 | * .NET SDK 8.0.101 6 | * [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 7 | * DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 8 | * 9 | * | Method | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio | 10 | * |-------------- |---------:|---------:|---------:|------:|----------:|------------:| 11 | * | GetBit | 48.69 ns | 0.211 ns | 0.197 ns | 1.00 | - | NA | 12 | * | Lookup | 40.66 ns | 0.122 ns | 0.114 ns | 0.84 | - | NA | 13 | * | AntiDiagonals | 53.32 ns | 0.111 ns | 0.099 ns | 1.09 | - | NA | 14 | * 15 | * 16 | * BenchmarkDotNet v0.13.12, Windows 10 (10.0.20348.2159) (Hyper-V) 17 | * AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores 18 | * .NET SDK 8.0.101 19 | * [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 20 | * DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 21 | * 22 | * | Method | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio | 23 | * |-------------- |---------:|---------:|---------:|------:|----------:|------------:| 24 | * | GetBit | 48.14 ns | 0.164 ns | 0.128 ns | 1.00 | - | NA | 25 | * | Lookup | 40.85 ns | 0.020 ns | 0.016 ns | 0.85 | - | NA | 26 | * | AntiDiagonals | 52.64 ns | 0.048 ns | 0.040 ns | 1.09 | - | NA | 27 | * 28 | * 29 | * BenchmarkDotNet v0.13.12, macOS Monterey 12.7.2 (21G1974) [Darwin 21.6.0] 30 | * Intel Xeon CPU E5-1650 v2 3.50GHz (Max: 3.34GHz), 1 CPU, 3 logical and 3 physical cores 31 | * .NET SDK 8.0.101 32 | * [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX 33 | * DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX 34 | * 35 | * | Method | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio | 36 | * |-------------- |---------:|---------:|---------:|------:|----------:|------------:| 37 | * | GetBit | 76.90 ns | 0.209 ns | 0.174 ns | 1.00 | - | NA | 38 | * | Lookup | 60.36 ns | 0.273 ns | 0.228 ns | 0.78 | - | NA | 39 | * | AntiDiagonals | 65.77 ns | 0.337 ns | 0.299 ns | 0.86 | - | NA | 40 | * 41 | */ 42 | 43 | using BenchmarkDotNet.Attributes; 44 | using Lynx.Model; 45 | using System.Runtime.CompilerServices; 46 | 47 | namespace Lynx.Benchmark; 48 | public class IsDarkSquare_IsLightSquare_Benchmark : BaseBenchmark 49 | { 50 | [Benchmark(Baseline = true)] 51 | public bool GetBit() 52 | { 53 | bool result = false; 54 | for (int i = 0; i < 64; ++i) 55 | { 56 | result ^= IsLightSquare_GetBit(i); 57 | } 58 | 59 | return result; 60 | } 61 | 62 | [Benchmark] 63 | public bool Lookup() 64 | { 65 | bool result = false; 66 | for (int i = 0; i < 64; ++i) 67 | { 68 | result ^= IsLightSquare_Lookup(i); 69 | } 70 | 71 | return result; 72 | } 73 | 74 | [Benchmark] 75 | public bool AntiDiagonals() 76 | { 77 | bool result = false; 78 | for (int i = 0; i < 64; ++i) 79 | { 80 | result ^= IsLightSquare_AntiDiagonals(i); 81 | } 82 | 83 | return result; 84 | } 85 | 86 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 87 | private static bool IsLightSquare_GetBit(int square) 88 | { 89 | return Masks.LightSquaresMask.GetBit(square); 90 | } 91 | 92 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 93 | private static bool IsLightSquare_Lookup(int square) 94 | { 95 | return ((Masks.LightSquaresMask >> square) & 1) != 0; 96 | } 97 | 98 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 99 | private static bool IsLightSquare_AntiDiagonals(int square) 100 | { 101 | return (((9 * square) + 8) & 8) != 0; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/Lynx.Test/PregeneratedAttacks/BishopAttacksTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.Model; 2 | using NUnit.Framework; 3 | using BS = Lynx.Model.BoardSquare; 4 | 5 | namespace Lynx.Test.PregeneratedAttacks; 6 | 7 | public class BishopAttacksTest 8 | { 9 | [TestCase(BS.a8, new BS[] { }, new[] { BS.b7, BS.c6, BS.d5, BS.e4, BS.f3, BS.g2, BS.h1 })] 10 | [TestCase(BS.a8, new[] { BS.g2 }, new[] { BS.b7, BS.c6, BS.d5, BS.e4, BS.f3, BS.g2 })] 11 | [TestCase(BS.a8, new[] { BS.b7 }, new[] { BS.b7 })] 12 | 13 | [TestCase(BS.h1, new BS[] { }, new[] { BS.g2, BS.f3, BS.e4, BS.d5, BS.c6, BS.b7, BS.a8 })] 14 | [TestCase(BS.h1, new[] { BS.c6 }, new[] { BS.g2, BS.f3, BS.e4, BS.d5, BS.c6 })] 15 | [TestCase(BS.h1, new[] { BS.g2 }, new[] { BS.g2, })] 16 | 17 | [TestCase(BS.a1, new BS[] { }, new[] { BS.b2, BS.c3, BS.d4, BS.e5, BS.f6, BS.g7, BS.h8 })] 18 | [TestCase(BS.a1, new[] { BS.g7 }, new[] { BS.b2, BS.c3, BS.d4, BS.e5, BS.f6, BS.g7 })] 19 | [TestCase(BS.a1, new[] { BS.b2 }, new[] { BS.b2 })] 20 | 21 | [TestCase(BS.h8, new BS[] { }, new[] { BS.g7, BS.f6, BS.e5, BS.d4, BS.c3, BS.b2, BS.a1 })] 22 | [TestCase(BS.h8, new[] { BS.b2 }, new[] { BS.g7, BS.f6, BS.e5, BS.d4, BS.c3, BS.b2 })] 23 | [TestCase(BS.h8, new[] { BS.g7 }, new[] { BS.g7, })] 24 | 25 | [TestCase(BS.d4, new[] { BS.a7, BS.a7, BS.g7, BS.b2, BS.e3 }, new[] { BS.a7, BS.b6, BS.c5, BS.e3, BS.b2, BS.c3, BS.e5, BS.f6, BS.g7 })] 26 | public void GenerateBishopAttacksOnTheFly(BS bishopSquare, BS[] occupiedSquares, BS[] attackedSquares) 27 | { 28 | // Arrange 29 | var occupancy = BitBoardExtensions.Initialize(occupiedSquares); 30 | 31 | // Act 32 | var attacks = AttackGenerator.GenerateBishopAttacksOnTheFly((int)bishopSquare, occupancy); 33 | 34 | // Assert 35 | ValidateAttacks(attackedSquares, attacks); 36 | 37 | static void ValidateAttacks(BS[] attackedSquares, BitBoard attacks) 38 | { 39 | foreach (var attackedSquare in attackedSquares) 40 | { 41 | Assert.True(attacks.GetBit(attackedSquare)); 42 | attacks.PopBit(attackedSquare); 43 | } 44 | 45 | Assert.AreEqual(default(BitBoard), attacks); 46 | } 47 | } 48 | 49 | /// 50 | /// Implicitly tests and 51 | /// 52 | [TestCase(BS.a8, new BS[] { }, new[] { BS.b7, BS.c6, BS.d5, BS.e4, BS.f3, BS.g2, BS.h1 })] 53 | [TestCase(BS.a8, new[] { BS.g2 }, new[] { BS.b7, BS.c6, BS.d5, BS.e4, BS.f3, BS.g2 })] 54 | [TestCase(BS.a8, new[] { BS.b7 }, new[] { BS.b7 })] 55 | 56 | [TestCase(BS.h1, new BS[] { }, new[] { BS.g2, BS.f3, BS.e4, BS.d5, BS.c6, BS.b7, BS.a8 })] 57 | [TestCase(BS.h1, new[] { BS.c6 }, new[] { BS.g2, BS.f3, BS.e4, BS.d5, BS.c6 })] 58 | [TestCase(BS.h1, new[] { BS.g2 }, new[] { BS.g2, })] 59 | 60 | [TestCase(BS.a1, new BS[] { }, new[] { BS.b2, BS.c3, BS.d4, BS.e5, BS.f6, BS.g7, BS.h8 })] 61 | [TestCase(BS.a1, new[] { BS.g7 }, new[] { BS.b2, BS.c3, BS.d4, BS.e5, BS.f6, BS.g7 })] 62 | [TestCase(BS.a1, new[] { BS.b2 }, new[] { BS.b2 })] 63 | 64 | [TestCase(BS.h8, new BS[] { }, new[] { BS.g7, BS.f6, BS.e5, BS.d4, BS.c3, BS.b2, BS.a1 })] 65 | [TestCase(BS.h8, new[] { BS.b2 }, new[] { BS.g7, BS.f6, BS.e5, BS.d4, BS.c3, BS.b2 })] 66 | [TestCase(BS.h8, new[] { BS.g7 }, new[] { BS.g7, })] 67 | 68 | [TestCase(BS.d4, new[] { BS.a7, BS.a7, BS.g7, BS.b2, BS.e3 }, new[] { BS.a7, BS.b6, BS.c5, BS.e3, BS.b2, BS.c3, BS.e5, BS.f6, BS.g7 })] 69 | public void GetBishopAttacks(BS bishopSquare, BS[] occupiedSquares, BS[] attackedSquares) 70 | { 71 | // Arrange 72 | var occupancy = BitBoardExtensions.Initialize(occupiedSquares); 73 | 74 | // Act 75 | var attacks = Attacks.BishopAttacks((int)bishopSquare, occupancy); 76 | Assert.AreEqual(Attacks.MagicNumbersBishopAttacks((int)bishopSquare, occupancy), attacks); 77 | 78 | // Assert 79 | foreach (var attackedSquare in attackedSquares) 80 | { 81 | Assert.True(attacks.GetBit(attackedSquare)); 82 | attacks.PopBit(attackedSquare); 83 | } 84 | 85 | Assert.AreEqual(default(BitBoard), attacks); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Lynx/Model/TranspositionTableElement.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Lynx.Model; 5 | 6 | #pragma warning disable S4022 // Enumerations should have "Int32" storage - size matters 7 | public enum NodeType : byte 8 | #pragma warning restore S4022 // Enumerations should have "Int32" storage 9 | { 10 | Unknown, // It needs to be 0 because of default struct initialization 11 | 12 | Exact, 13 | 14 | /// 15 | /// UpperBound 16 | /// 17 | Alpha, 18 | 19 | /// 20 | /// LowerBound 21 | /// 22 | Beta 23 | } 24 | 25 | /// 26 | /// 10 bytes 27 | /// 28 | public struct TranspositionTableElement 29 | { 30 | private ushort _key; // 2 bytes 31 | 32 | private ShortMove _move; // 2 bytes 33 | 34 | private short _score; // 2 bytes 35 | 36 | private short _staticEval; // 2 bytes 37 | 38 | private byte _depth; // 1 byte 39 | 40 | /// 41 | /// 1 byte 42 | /// Binary move bits Hexadecimal 43 | /// 0000 0001 0x1 Was PV (0-1) 44 | /// 0000 0110 0x6 NodeType (0-3) 45 | /// 46 | private byte _type_WasPv; 47 | 48 | private const int NodeTypeOffset = 1; 49 | 50 | /// 51 | /// 16 MSB of Position's Zobrist key 52 | /// 53 | public readonly ushort Key 54 | { 55 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 56 | get => _key; 57 | } 58 | 59 | /// 60 | /// Best move found in the position. 0 if the search failed low (score <= alpha) 61 | /// 62 | public readonly ShortMove Move 63 | { 64 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 65 | get => _move; 66 | } 67 | 68 | /// 69 | /// Position's score 70 | /// 71 | public readonly int Score 72 | { 73 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 74 | get => _score; 75 | } 76 | 77 | /// 78 | /// Position's static evaluation 79 | /// 80 | public readonly int StaticEval 81 | { 82 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 83 | get => _staticEval; 84 | } 85 | 86 | /// 87 | /// How deep the recorded search went. For us this numberis targetDepth - ply 88 | /// 89 | public readonly int Depth 90 | { 91 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 92 | get => _depth; 93 | } 94 | 95 | /// 96 | /// Node (position) type: 97 | /// : == , 98 | /// : <= , 99 | /// : >= 100 | /// 101 | public readonly NodeType Type 102 | { 103 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 104 | get => (NodeType)((_type_WasPv & 0xE) >> NodeTypeOffset); 105 | } 106 | 107 | public readonly bool WasPv 108 | { 109 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 110 | get => (_type_WasPv & 0x1) == 1; 111 | } 112 | 113 | /// 114 | /// Struct size in bytes 115 | /// 116 | public static ulong Size 117 | { 118 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 119 | get => (ulong)Marshal.SizeOf(); 120 | } 121 | 122 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 123 | public void Update(ushort key, int score, int staticEval, int depth, NodeType nodeType, int wasPv, Move? move) 124 | { 125 | _key = key; 126 | _score = (short)score; 127 | _staticEval = (short)staticEval; 128 | _depth = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref depth, 1))[0]; 129 | _type_WasPv = (byte)(wasPv | ((int)nodeType << NodeTypeOffset)); 130 | _move = move != null ? (ShortMove)move : Move; // Suggested by cj5716 instead of 0. https://github.com/lynx-chess/Lynx/pull/462 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/Lynx.Test/PawnIslandsTest.cs: -------------------------------------------------------------------------------- 1 | using Lynx.ConstantsGenerator; 2 | using Lynx.Model; 3 | using NUnit.Framework; 4 | using System.Numerics; 5 | 6 | namespace Lynx.Test; 7 | 8 | public class PawnIslandsTest 9 | { 10 | [TestCase("5k1K/8/8/8/8/8/8/8 w - - 0 1", 0)] 11 | 12 | [TestCase("5k1K/8/8/8/8/8/PPPPPPPP/8 w - - 0 1", 1)] 13 | 14 | [TestCase("5k1K/8/8/8/8/8/1PPPPPPP/8 w - - 0 1", 1)] 15 | [TestCase("5k1K/8/8/8/8/8/PPPPPPP1/8 w - - 0 1", 1)] 16 | [TestCase("5k1K/8/8/8/8/8/1PPPPPP1/8 w - - 0 1", 1)] 17 | 18 | [TestCase("5k1K/8/8/8/8/8/2PPPPPP/8 w - - 0 1", 1)] 19 | [TestCase("5k1K/8/8/8/8/8/PPPPPP2/8 w - - 0 1", 1)] 20 | [TestCase("5k1K/8/8/8/8/8/2PPP2/8 w - - 0 1", 1)] 21 | 22 | [TestCase("5k1K/8/8/8/8/8/3PPPPP/8 w - - 0 1", 1)] 23 | [TestCase("5k1K/8/8/8/8/8/PPPPP3/8 w - - 0 1", 1)] 24 | [TestCase("5k1K/8/8/8/8/8/3PP3/8 w - - 0 1", 1)] 25 | 26 | [TestCase("5k1K/8/8/8/8/8/P7/8 w - - 0 1", 1)] 27 | [TestCase("5k1K/8/8/8/8/8/1P6/8 w - - 0 1", 1)] 28 | [TestCase("5k1K/8/8/8/8/8/6P1/8 w - - 0 1", 1)] 29 | [TestCase("5k1K/8/8/8/8/8/2P5/8 w - - 0 1", 1)] 30 | [TestCase("5k1K/8/8/8/8/8/5P2/8 w - - 0 1", 1)] 31 | [TestCase("5k1K/8/8/8/8/8/3P4/8 w - - 0 1", 1)] 32 | [TestCase("5k1K/8/8/8/8/8/4P3/8 w - - 0 1", 1)] 33 | 34 | [TestCase("5k1K/8/8/8/8/8/PP6/8 w - - 0 1", 1)] 35 | [TestCase("5k1K/8/8/8/8/8/6PP/8 w - - 0 1", 1)] 36 | [TestCase("5k1K/8/8/8/8/8/3PP3/8 w - - 0 1", 1)] 37 | [TestCase("5k1K/8/8/8/8/8/4P3/8 w - - 0 1", 1)] 38 | 39 | [TestCase("5k1K/8/8/8/8/8/P1PPPPPP/8 w - - 0 1", 2)] 40 | [TestCase("5k1K/8/8/8/8/8/P1P1PPPP/8 w - - 0 1", 3)] 41 | [TestCase("5k1K/8/8/8/8/8/P1P1P1PP/8 w - - 0 1", 4)] 42 | [TestCase("5k1K/8/8/8/8/8/P1P1P2P/8 w - - 0 1", 4)] 43 | [TestCase("5k1K/8/8/8/8/8/P1P1P1P1/8 w - - 0 1", 4)] 44 | 45 | [TestCase("5k1K/8/8/8/8/8/P1P5/8 w - - 0 1", 2)] 46 | [TestCase("5k1K/2P5/2P5/2P5/2P5/2P5/P1P5/8 w - - 0 1", 2)] 47 | [TestCase("5k1K/8/8/8/8/8/P6P/8 w - - 0 1", 2)] 48 | public void PawnIslandsCount(string fen, int expectedPawnIslands) 49 | { 50 | var pieces = FENParser.ParseFEN(fen).PieceBitBoards; 51 | BitBoard whitePawns = pieces[(int)Piece.P]; 52 | 53 | // Original method test 54 | var pawnIslands = PawnIslandsGenerator.IdentifyIslands(whitePawns); 55 | Assert.AreEqual(expectedPawnIslands, pawnIslands, "Error in the original method"); 56 | 57 | // Generator test 58 | pawnIslands = CountPawnIslands(whitePawns); 59 | Assert.AreEqual(expectedPawnIslands, pawnIslands, "Error in the generator"); 60 | 61 | // Generator test 62 | pawnIslands = CountPawnIslandsImproved(whitePawns); 63 | Assert.AreEqual(expectedPawnIslands, pawnIslands, "Error in the improved method"); 64 | 65 | var pawnIslandsBonus = Position.PawnIslands(whitePawns, pieces[(int)Piece.p]); 66 | Assert.AreEqual(EvaluationParams.PawnIslandsBonus[expectedPawnIslands] - EvaluationParams.PawnIslandsBonus[0], pawnIslandsBonus, "Error in the Position implementation"); 67 | } 68 | 69 | private static int CountPawnIslands(BitBoard pawns) 70 | { 71 | int pawnFileBitBoard = 0; 72 | 73 | while (pawns != 0) 74 | { 75 | pawns = pawns.WithoutLS1B(out var squareIndex); 76 | 77 | // BitBoard.SetBit equivalent but for byte instead of ulong 78 | pawnFileBitBoard |= (1 << Constants.File[squareIndex]); 79 | } 80 | 81 | return PawnIslandsGenerator.PawnIslandsCount[pawnFileBitBoard]; 82 | } 83 | 84 | private static int CountPawnIslandsImproved(BitBoard pawns) 85 | { 86 | byte pawnFileBitBoard = 0; 87 | 88 | while (pawns != 0) 89 | { 90 | pawns = pawns.WithoutLS1B(out var squareIndex); 91 | 92 | // BitBoard.SetBit equivalent but for byte instead of ulong 93 | pawnFileBitBoard |= (byte)(1 << (squareIndex % 8)); 94 | } 95 | 96 | int shifted = pawnFileBitBoard << 1; 97 | 98 | // Treat shifted’s MSB as 0 implicitly 99 | int starts = pawnFileBitBoard & (~shifted); 100 | 101 | return BitOperations.PopCount((uint)starts); 102 | } 103 | } 104 | --------------------------------------------------------------------------------