├── network.txt ├── Resources └── logo.png ├── .gitattributes ├── Logic ├── Datagen │ ├── GameResult.cs │ ├── DatagenParameters.cs │ ├── OutputFormat.cs │ ├── PlaintextDataFormat.cs │ ├── BulletDataFormat.cs │ ├── ProgressBroker.cs │ ├── BulletFormatEntry.cs │ └── Rescorer.cs ├── Magic │ └── MagicSquare.cs ├── NN │ ├── BucketCache.cs │ ├── NetworkUpdate.cs │ ├── Accumulator.cs │ └── NetContainer.cs ├── Search │ ├── SearchConstants.cs │ ├── History │ │ ├── StatEntry.cs │ │ ├── ICorrectionTable.cs │ │ ├── CorrectionTables.cs │ │ ├── MainHistoryTable.cs │ │ ├── CaptureHistoryTable.cs │ │ ├── PlyHistoryTable.cs │ │ ├── ContinuationHistory.cs │ │ └── PieceToHistory.cs │ ├── EvaluationConstants.cs │ ├── Evaluation.cs │ ├── SearchStackEntry.cs │ ├── WDL.cs │ ├── SearchInformation.cs │ ├── Ordering │ │ ├── HistoryTable.cs │ │ └── MoveOrdering.cs │ ├── SearchOptions.cs │ └── TimeManager.cs ├── Util │ ├── EvalFileHandler.cs │ ├── Zstd.cs │ ├── IsAOTHandler.cs │ ├── ExceptionHandling.cs │ ├── HorsieBindings.cs │ ├── FishBench.cs │ ├── SearchBench.cs │ └── Interop.cs ├── Threads │ ├── ThreadSetup.cs │ └── ConditionVariable.cs ├── Book │ └── PolyglotEntry.cs ├── Data │ ├── RootMove.cs │ ├── ScoredMove.cs │ ├── RunOptions.cs │ ├── Enums.cs │ └── Move.cs ├── Transposition │ ├── TTCluster.cs │ ├── Cuckoo.cs │ ├── TTEntry.cs │ ├── Zobrist.cs │ └── TranspositionTable.cs ├── Core │ ├── StateInfo.cs │ └── Bitboard.cs └── UCI │ └── UCIOption.cs ├── Bindings ├── defs.h ├── arch.h ├── simd.h └── SIMD.cpp ├── .gitignore ├── Properties ├── PublishProfiles │ ├── Release-win64.pubxml │ ├── Release-win64-dev.pubxml │ ├── Release-win64-pext.pubxml │ └── Release-win64-aot.pubxml ├── GlobalUsings.cs ├── Resources.Designer.cs ├── GlobalSuppressions.cs └── Resources.resx ├── .github └── workflows │ ├── makefile.yml │ ├── ci.yml │ ├── debug.yml │ └── dotnet-build.yml ├── LICENSE.md ├── README.md └── Makefile /network.txt: -------------------------------------------------------------------------------- 1 | net-015-2048x16x32-132qb-z -------------------------------------------------------------------------------- /Resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamt19/Lizard/HEAD/Resources/logo.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | *.cshtml text diff=html 5 | *.csx text diff=csharp 6 | *.sln text eol=crlf 7 | *.csproj text eol=crlf -------------------------------------------------------------------------------- /Logic/Datagen/GameResult.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Lizard.Logic.Datagen 3 | { 4 | public enum GameResult 5 | { 6 | WhiteWin = 2, 7 | Draw = 1, 8 | BlackWin = 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Logic/Magic/MagicSquare.cs: -------------------------------------------------------------------------------- 1 | namespace Lizard.Logic.Magic 2 | { 3 | public unsafe struct MagicSquare 4 | { 5 | public ulong mask; 6 | public ulong number; 7 | public ulong* attacks; 8 | public int shift; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Bindings/defs.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | using nuint = std::size_t; 8 | 9 | using i8 = std::int8_t; 10 | using i16 = std::int16_t; 11 | using i32 = std::int32_t; 12 | using i64 = std::int64_t; 13 | 14 | using u8 = std::uint8_t; 15 | using u16 = std::uint16_t; 16 | using u32 = std::uint32_t; 17 | using u64 = std::uint64_t; 18 | -------------------------------------------------------------------------------- /Logic/NN/BucketCache.cs: -------------------------------------------------------------------------------- 1 | namespace Lizard.Logic.NN 2 | { 3 | public unsafe struct BucketCache 4 | { 5 | /// 6 | /// 2 Boards, 1 for each perspective 7 | /// 8 | public Bitboard[] Boards = new Bitboard[ColorNB]; 9 | public Accumulator Accumulator = new Accumulator(); 10 | 11 | public BucketCache() { } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This .gitignore file was automatically created by Microsoft(R) Visual Studio. 3 | ################################################################################ 4 | 5 | /bin 6 | /.vs/Lizard 7 | /obj 8 | /TB_Files 9 | /Logic/Tablebase 10 | /Lizard.csproj.user 11 | /Properties/PublishProfiles/*.user 12 | /Properties/launchSettings.json 13 | /*.exe 14 | /*.pdb 15 | /*.bin 16 | /*.nnue 17 | /data 18 | /.vs/ProjectEvaluation 19 | *.dll 20 | *.so 21 | -------------------------------------------------------------------------------- /Logic/Search/SearchConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Lizard.Logic.Search 2 | { 3 | public static class SearchConstants 4 | { 5 | public const int AlphaStart = -ScoreMate; 6 | public const int BetaStart = ScoreMate; 7 | 8 | 9 | /// 10 | /// The maximum amount of time to search, in milliseconds. 11 | /// 12 | public const int MaxSearchTime = int.MaxValue - 1; 13 | public const ulong MaxSearchNodes = ulong.MaxValue - 1; 14 | 15 | 16 | public const int DefaultMovesToGo = 20; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Logic/Util/EvalFileHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Lizard.Logic.Util 2 | { 3 | 4 | // https://stackoverflow.com/questions/49522751/how-to-read-get-a-propertygroup-value-from-a-csproj-file-using-c-sharp-in-a-ne 5 | 6 | [System.AttributeUsage(System.AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)] 7 | sealed class EvalFileAttribute : System.Attribute 8 | { 9 | public string EvalFile { get; } 10 | public EvalFileAttribute(string evalFile) 11 | { 12 | this.EvalFile = evalFile; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Logic/Search/History/StatEntry.cs: -------------------------------------------------------------------------------- 1 | 2 | using static Lizard.Logic.Search.History.HistoryTable; 3 | 4 | namespace Lizard.Logic.Search.History 5 | { 6 | public readonly struct StatEntry(short v) 7 | { 8 | public readonly short Value = v; 9 | 10 | public static implicit operator short(StatEntry entry) => entry.Value; 11 | public static implicit operator StatEntry(short s) => new(s); 12 | public static StatEntry operator <<(StatEntry entry, int bonus) => (StatEntry)(entry + (bonus - (entry * Math.Abs(bonus) / NormalClamp))); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Properties/PublishProfiles/Release-win64.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | x64 9 | bin\Publish\win64 10 | FileSystem 11 | <_TargetId>Folder 12 | net8.0 13 | win-x64 14 | true 15 | 16 | Lizard-$(RuntimeIdentifier) 17 | 18 | -------------------------------------------------------------------------------- /Properties/PublishProfiles/Release-win64-dev.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | x64 9 | bin\Publish\win64-dev 10 | FileSystem 11 | <_TargetId>Folder 12 | net8.0 13 | win-x64 14 | true 15 | DEV 16 | Lizard-$(RuntimeIdentifier)-dev 17 | 18 | -------------------------------------------------------------------------------- /Properties/PublishProfiles/Release-win64-pext.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | x64 9 | bin\Publish\win64-pext 10 | FileSystem 11 | <_TargetId>Folder 12 | net8.0 13 | win-x64 14 | true 15 | PEXT 16 | Lizard-$(RuntimeIdentifier)-pext 17 | 18 | -------------------------------------------------------------------------------- /Logic/Threads/ThreadSetup.cs: -------------------------------------------------------------------------------- 1 | namespace Lizard.Logic.Threads 2 | { 3 | public class ThreadSetup 4 | { 5 | public string StartFEN; 6 | public List SetupMoves; 7 | public List UCISearchMoves; 8 | 9 | public ThreadSetup(List setupMoves) : this(InitialFEN, setupMoves, new List()) { } 10 | public ThreadSetup(string fen = InitialFEN) : this(fen, new List(), new List()) { } 11 | 12 | public ThreadSetup(string fen, List setupMoves, List uciSearchMoves) 13 | { 14 | this.StartFEN = fen; 15 | this.SetupMoves = setupMoves; 16 | this.UCISearchMoves = uciSearchMoves; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Logic/Datagen/DatagenParameters.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Lizard.Logic.Datagen 3 | { 4 | public static class DatagenParameters 5 | { 6 | public const int HashSize = 8; 7 | 8 | public const int MinOpeningPly = 8; 9 | public const int MaxOpeningPly = 9; 10 | 11 | public const int SoftNodeLimit = 5000; 12 | public const int DepthLimit = 24; 13 | 14 | public const int WritableDataLimit = 512; 15 | 16 | public const int AdjudicateMoves = 4; 17 | public const int AdjudicateScore = 3000; 18 | public const int MaxFilteringScore = 6000; 19 | 20 | public const int MaxOpeningScore = 1200; 21 | public const int MaxScrambledOpeningScore = 600; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Logic/Search/EvaluationConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Lizard.Logic.Search 2 | { 3 | public static unsafe class EvaluationConstants 4 | { 5 | public const short ScoreNone = 32760; 6 | public const int ScoreInfinite = 31200; 7 | public const int ScoreMate = 30000; 8 | public const int ScoreDraw = 0; 9 | 10 | public const int ScoreTTWin = ScoreMate - 512; 11 | public const int ScoreTTLoss = -ScoreTTWin; 12 | 13 | public const int ScoreMateMax = ScoreMate - 256; 14 | public const int ScoreMatedMax = -ScoreMateMax; 15 | 16 | public const int MaxNormalScore = ScoreTTWin - 1; 17 | 18 | public const int ScoreAssuredWin = 20000; 19 | public const int ScoreWin = 10000; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Logic/Datagen/OutputFormat.cs: -------------------------------------------------------------------------------- 1 | namespace Lizard.Logic.Datagen 2 | { 3 | public interface TOutputFormat 4 | { 5 | public int Score { get; set; } 6 | public GameResult Result { get; set; } 7 | public void SetResult(GameResult gr); 8 | public void SetSTM(int stm); 9 | public string GetWritableTextData(); 10 | public void Fill(Position pos, Move bestMove, int score); 11 | 12 | public static string ResultToMarlin(GameResult gr) 13 | { 14 | return gr switch 15 | { 16 | GameResult.WhiteWin => "1.0", 17 | GameResult.Draw => "0.5", 18 | GameResult.BlackWin => "0.0", 19 | _ => "0.5", 20 | }; 21 | } 22 | } 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Properties/PublishProfiles/Release-win64-aot.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | x64 9 | bin\Publish\win64-aot 10 | FileSystem 11 | <_TargetId>Folder 12 | net8.0 13 | win-x64 14 | true 15 | PUBLISH_AOT 16 | False 17 | True 18 | Lizard-$(RuntimeIdentifier)-aot 19 | 20 | -------------------------------------------------------------------------------- /Logic/Util/Zstd.cs: -------------------------------------------------------------------------------- 1 | 2 | using ZstdSharp; 3 | 4 | namespace Lizard.Logic.Util 5 | { 6 | public static class Zstd 7 | { 8 | private const int ZSTD_HEADER = -47205080; 9 | 10 | public static bool IsCompressed(Stream stream) 11 | { 12 | BinaryReader br = new BinaryReader(stream); 13 | int headerMaybe = br.ReadInt32(); 14 | br.BaseStream.Seek(0, SeekOrigin.Begin); 15 | 16 | return (headerMaybe == ZSTD_HEADER); 17 | } 18 | 19 | public static MemoryStream Decompress(Stream stream, byte[] buff) 20 | { 21 | var zstStream = new DecompressionStream(stream); 22 | MemoryStream memStream = new MemoryStream(buff); 23 | zstStream.CopyTo(memStream); 24 | memStream.Seek(0, SeekOrigin.Begin); 25 | 26 | return memStream; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Logic/Search/History/ICorrectionTable.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Lizard.Logic.Search.History 4 | { 5 | public unsafe abstract class ICorrectionTable 6 | { 7 | protected readonly StatEntry* _History; 8 | 9 | public readonly int TableSize; 10 | public readonly int TableCount; 11 | 12 | private int TableElements => TableSize * TableCount; 13 | 14 | protected ICorrectionTable(int size = 16384, int tables = ColorNB) 15 | { 16 | TableSize = size; 17 | TableCount = tables; 18 | _History = (StatEntry*)AlignedAllocZeroed((nuint)(sizeof(StatEntry) * TableElements), AllocAlignment); 19 | } 20 | 21 | public void Dispose() => NativeMemory.AlignedFree(_History); 22 | public void Clear() => NativeMemory.Clear(_History, (nuint)(sizeof(StatEntry) * TableElements)); 23 | } 24 | } -------------------------------------------------------------------------------- /Logic/Util/IsAOTHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Lizard.Logic.Util 4 | { 5 | 6 | // https://stackoverflow.com/questions/49522751/how-to-read-get-a-propertygroup-value-from-a-csproj-file-using-c-sharp-in-a-ne 7 | 8 | [System.AttributeUsage(System.AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)] 9 | sealed class IsAOTAttribute : System.Attribute 10 | { 11 | public bool _IsAOT { get; } 12 | public IsAOTAttribute(string str) 13 | { 14 | this._IsAOT = (str.EqualsIgnoreCase("true")); 15 | } 16 | 17 | public static bool IsAOT() 18 | { 19 | bool retVal = false; 20 | try 21 | { 22 | retVal = Assembly.GetEntryAssembly().GetCustomAttribute()._IsAOT; 23 | } 24 | catch { } 25 | 26 | return retVal; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Logic/Search/History/CorrectionTables.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Lizard.Logic.Search.History 3 | { 4 | 5 | public unsafe class PawnCorrectionTable : ICorrectionTable 6 | { 7 | public ref StatEntry this[Position pos, int pc] => ref _History[CorrectionIndex(pos, pc)]; 8 | 9 | public int CorrectionIndex(Position pos, int pc) 10 | { 11 | return (pc * TableSize) + (int)((pos.PawnHash) & ((ulong)TableSize - 1)); 12 | } 13 | } 14 | 15 | 16 | /// Idea from Starzix: 17 | /// https://zzzzz151.pythonanywhere.com/test/729/ 18 | public unsafe class NonPawnCorrectionTable : ICorrectionTable 19 | { 20 | public ref StatEntry this[Position pos, int pc, int side] => ref _History[CorrectionIndex(pos, pc, side)]; 21 | 22 | public int CorrectionIndex(Position pos, int pc, int side) 23 | { 24 | return (pc * TableSize) + (int)((pos.NonPawnHash(side)) & ((ulong)TableSize - 1)); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/makefile.yml: -------------------------------------------------------------------------------- 1 | name: Makefile CI 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | DOTNET_VERSION: 9.0.x 7 | 8 | jobs: 9 | 10 | build-and-publish: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest, macOS-latest, macos-14] 16 | include: 17 | - os: ubuntu-latest 18 | runtime-identifier: linux-x64 19 | - os: windows-latest 20 | runtime-identifier: win-x64 21 | - os: macOS-latest 22 | runtime-identifier: osx-x64 23 | - os: macos-14 24 | runtime-identifier: osx-arm64 25 | fail-fast: false 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - name: Setup .NET 31 | uses: actions/setup-dotnet@v4 32 | with: 33 | dotnet-version: ${{ env.DOTNET_VERSION }} 34 | 35 | - name: Run Make 36 | run: make 37 | 38 | - name: Check Compiler 39 | run: ./Lizard compiler 40 | 41 | - name: Run Bench 42 | run: ./Lizard bench 43 | -------------------------------------------------------------------------------- /Logic/Search/History/MainHistoryTable.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Lizard.Logic.Search.History 4 | { 5 | public readonly unsafe struct MainHistoryTable 6 | { 7 | private readonly StatEntry* _History; 8 | private const int MainHistoryElements = ColorNB * SquareNB * SquareNB; 9 | 10 | public MainHistoryTable() 11 | { 12 | _History = AlignedAllocZeroed(MainHistoryElements); 13 | } 14 | 15 | public StatEntry this[int idx] 16 | { 17 | get => _History[idx]; 18 | set => _History[idx] = value; 19 | } 20 | 21 | public StatEntry this[int pc, Move m] 22 | { 23 | get => _History[HistoryIndex(pc, m)]; 24 | set => _History[HistoryIndex(pc, m)] = value; 25 | } 26 | 27 | public static int HistoryIndex(int pc, Move m) => (pc * 4096) + m.MoveMask; 28 | 29 | public void Dispose() => NativeMemory.AlignedFree(_History); 30 | public void Clear() => NativeMemory.Clear(_History, (nuint)sizeof(StatEntry) * MainHistoryElements); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Liam McGuire 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. -------------------------------------------------------------------------------- /Logic/Search/History/CaptureHistoryTable.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Lizard.Logic.Search.History 4 | { 5 | public readonly unsafe struct CaptureHistoryTable 6 | { 7 | private readonly StatEntry* _History; 8 | private const int CaptureHistoryElements = 2 * 6 * 64 * 6; 9 | 10 | public CaptureHistoryTable() 11 | { 12 | _History = AlignedAllocZeroed(CaptureHistoryElements); 13 | } 14 | 15 | public StatEntry this[int idx] 16 | { 17 | get => _History[idx]; 18 | set => _History[idx] = value; 19 | } 20 | 21 | public StatEntry this[int pc, int pt, int toSquare, int capturedPt] 22 | { 23 | get => _History[HistoryIndex(pc, pt, toSquare, capturedPt)]; 24 | set => _History[HistoryIndex(pc, pt, toSquare, capturedPt)] = value; 25 | } 26 | 27 | public static int HistoryIndex(int pc, int pt, int toSquare, int capturedPt) => (capturedPt * 768) + (toSquare * 12) + (pt + (pc * 6)); 28 | 29 | public void Dispose() => NativeMemory.AlignedFree(_History); 30 | public void Clear() => NativeMemory.Clear(_History, (nuint)sizeof(StatEntry) * CaptureHistoryElements); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | DOTNET_VERSION: 9.0.x 8 | 9 | jobs: 10 | 11 | build-and-publish: 12 | runs-on: ${{ matrix.os }} 13 | 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, windows-latest, macOS-latest, macos-14] 17 | include: 18 | - os: ubuntu-latest 19 | runtime-identifier: linux-x64 20 | - os: windows-latest 21 | runtime-identifier: win-x64 22 | - os: macOS-latest 23 | runtime-identifier: osx-x64 24 | - os: macos-14 25 | runtime-identifier: osx-arm64 26 | fail-fast: false 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - name: Setup .NET 32 | uses: actions/setup-dotnet@v4 33 | with: 34 | dotnet-version: ${{ env.DOTNET_VERSION }} 35 | 36 | - name: Run Make 37 | run: make 38 | 39 | - name: Upload Lizard-${{ github.run_number }}-${{ matrix.runtime-identifier }} artifact 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: Lizard-${{ github.run_number }}-${{ matrix.runtime-identifier }} 43 | path: | 44 | ./Lizard 45 | ./Lizard.exe 46 | if-no-files-found: error 47 | -------------------------------------------------------------------------------- /Logic/Book/PolyglotEntry.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Text; 3 | 4 | namespace LTChess.Logic.Book 5 | { 6 | [StructLayout(LayoutKind.Sequential)] 7 | public struct PolyglotEntry 8 | { 9 | public const int EntrySize = 16; 10 | 11 | // The format is 3 bits for file, then 3 bits for row. 12 | // This is 111111_2 = 0x3F = 63 13 | private const int MoveMask = 0b111111; 14 | 15 | public ulong Key; 16 | public ushort RawMove; 17 | public ushort Weight; 18 | public uint Learn; 19 | 20 | public int ToSquare => RawMove & MoveMask; 21 | public int FromSquare => (RawMove >> 6) & MoveMask; 22 | public int PromotionTo => (RawMove >> 12) & 0b111; 23 | 24 | public override string ToString() 25 | { 26 | StringBuilder sb = new StringBuilder(); 27 | 28 | sb.Append("Key: " + Key.ToString("X") + ", Move: " + IndexToString(FromSquare) + IndexToString(ToSquare)); 29 | 30 | if (PromotionTo != 0) 31 | { 32 | sb.Append(PieceToFENChar(PromotionTo)); 33 | } 34 | 35 | sb.Append(", Weight: " + Weight + ", Learn: " + Learn); 36 | 37 | return sb.ToString(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Logic/Data/RootMove.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Lizard.Logic.Data 4 | { 5 | public class RootMove : IComparable 6 | { 7 | public Move Move; 8 | 9 | public int Score; 10 | public int PreviousScore; 11 | public int AverageScore; 12 | public int Depth; 13 | 14 | public Move[] PV; 15 | public int PVLength; 16 | 17 | public RootMove(Move move, int score = -ScoreInfinite) 18 | { 19 | Move = move; 20 | Score = score; 21 | PreviousScore = score; 22 | AverageScore = score; 23 | Depth = 0; 24 | 25 | PV = new Move[MaxPly]; 26 | PV[0] = move; 27 | PVLength = 1; 28 | } 29 | 30 | [MethodImpl(Inline)] 31 | public int CompareTo(RootMove other) 32 | { 33 | if (Score != other.Score) 34 | { 35 | return Score.CompareTo(other.Score); 36 | } 37 | else 38 | { 39 | return PreviousScore.CompareTo(other.PreviousScore); 40 | } 41 | } 42 | 43 | public override string ToString() 44 | { 45 | return Move.ToString() + ": " + Score + ", Avg: " + AverageScore; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Logic/Datagen/PlaintextDataFormat.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Lizard.Logic.Datagen 3 | { 4 | public unsafe struct PlaintextDataFormat : TOutputFormat 5 | { 6 | public const int BufferSize = 92; 7 | public fixed char FEN[BufferSize]; 8 | 9 | public Move BestMove { get; set; } 10 | public int Score { get; set; } 11 | public GameResult Result { get; set; } 12 | 13 | public void SetResult(GameResult gr) => Result = gr; 14 | 15 | public void SetSTM(int stm) 16 | { 17 | throw new NotImplementedException(); 18 | } 19 | 20 | 21 | 22 | public string GetWritableTextData() 23 | { 24 | fixed (char* fen = FEN) 25 | { 26 | var fenStr = new string(fen); 27 | return $"{fenStr} | {Score} | {TOutputFormat.ResultToMarlin(Result)}"; 28 | } 29 | } 30 | 31 | 32 | 33 | public void Fill(Position pos, Move bestMove, int score) 34 | { 35 | var posSpan = pos.GetFEN().AsSpan(); 36 | 37 | fixed (char* fen = FEN) 38 | { 39 | var fenSpan = new Span(fen, BufferSize); 40 | fenSpan.Clear(); 41 | 42 | posSpan.CopyTo(fenSpan); 43 | } 44 | 45 | Score = score; 46 | BestMove = bestMove; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Properties/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Thanks C# 10! 2 | 3 | global using Lizard.Logic.Core; 4 | global using Lizard.Logic.Data; 5 | global using Lizard.Logic.Search; 6 | global using Lizard.Logic.Transposition; 7 | global using Lizard.Logic.UCI; 8 | global using Lizard.Logic.Util; 9 | 10 | global using static Lizard.Logic.Data.Bound; 11 | global using static Lizard.Logic.Data.Color; 12 | global using static Lizard.Logic.Data.Piece; 13 | global using static Lizard.Logic.Data.PrecomputedData; 14 | global using static Lizard.Logic.Data.RunOptions; 15 | global using static Lizard.Logic.Data.Squares; 16 | global using static Lizard.Logic.Magic.MagicBitboards; 17 | global using static Lizard.Logic.Search.Evaluation; 18 | global using static Lizard.Logic.Search.EvaluationConstants; 19 | global using static Lizard.Logic.Search.SearchConstants; 20 | global using static Lizard.Logic.Search.SearchOptions; 21 | global using static Lizard.Logic.Threads.SearchThreadPool; 22 | global using static Lizard.Logic.Util.ExceptionHandling; 23 | global using static Lizard.Logic.Util.Interop; 24 | global using static Lizard.Logic.Util.Utilities; 25 | 26 | global using Color = Lizard.Logic.Data.Color; 27 | global using Debug = System.Diagnostics.Debug; 28 | global using Stopwatch = System.Diagnostics.Stopwatch; 29 | global using MethodImplOptions = System.Runtime.CompilerServices.MethodImplOptions; 30 | global using Unsafe = System.Runtime.CompilerServices.Unsafe; 31 | -------------------------------------------------------------------------------- /Logic/Data/ScoredMove.cs: -------------------------------------------------------------------------------- 1 | namespace Lizard.Logic.Data 2 | { 3 | /// 4 | /// Contains a and a score. 5 | /// 6 | public struct ScoredMove 7 | { 8 | /// 9 | /// CS0199: Fields of static readonly field 'name' cannot be passed ref or out (except in a static constructor) 10 | /// 11 | /// Since a ScoredMove needs a ref Move, the compiler doesn't let it use 12 | /// because it wasn't created in the same static constructor as the invisible one below. 13 | /// 14 | private static readonly Move CS0199_NullMove = new Move(); 15 | public static readonly ScoredMove Null = new ScoredMove(ref CS0199_NullMove); 16 | 17 | public Move Move = Move.Null; 18 | public int Score = ScoreNone; 19 | 20 | public ScoredMove(ref Move m, int score = 0) 21 | { 22 | this.Move = m; 23 | this.Score = score; 24 | } 25 | 26 | public override string ToString() 27 | { 28 | return Move.ToString() + ", " + Score; 29 | } 30 | 31 | public static bool operator <(ScoredMove a, ScoredMove b) => a.Score < b.Score; 32 | public static bool operator >(ScoredMove a, ScoredMove b) => a.Score > b.Score; 33 | 34 | public bool Equals(ScoredMove other) 35 | { 36 | return Score == other.Score && Move.Equals(other.Move); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Logic/Search/Evaluation.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Lizard.Logic.Search 4 | { 5 | public static unsafe class Evaluation 6 | { 7 | [MethodImpl(Inline)] 8 | public static int MakeDrawScore(ulong nodes) 9 | { 10 | return -1 + (int)(nodes & 2); 11 | } 12 | 13 | [MethodImpl(Inline)] 14 | public static int MakeMateScore(int ply) 15 | { 16 | return -ScoreMate + ply; 17 | } 18 | 19 | public static bool IsScoreMate(int score) 20 | { 21 | return Math.Abs(Math.Abs(score) - ScoreMate) < MaxDepth; 22 | } 23 | 24 | [MethodImpl(Inline)] 25 | public static int GetPieceValue(int pt) 26 | { 27 | return pt switch 28 | { 29 | Pawn => ValuePawn, 30 | Knight => ValueKnight, 31 | Bishop => ValueBishop, 32 | Rook => ValueRook, 33 | Queen => ValueQueen, 34 | _ => 0, 35 | }; 36 | } 37 | 38 | [MethodImpl(Inline)] 39 | public static int GetSEEValue(int pt) 40 | { 41 | return pt switch 42 | { 43 | Pawn => SEEValuePawn, 44 | Knight => SEEValueKnight, 45 | Bishop => SEEValueBishop, 46 | Rook => SEEValueRook, 47 | Queen => SEEValueQueen, 48 | _ => 0, 49 | }; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Logic/NN/NetworkUpdate.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace Lizard.Logic.NN 5 | { 6 | public unsafe struct PerspectiveUpdate 7 | { 8 | public fixed int Adds[2]; 9 | public fixed int Subs[2]; 10 | public int AddCnt = 0; 11 | public int SubCnt = 0; 12 | 13 | public PerspectiveUpdate() { } 14 | 15 | [MethodImpl(Inline)] 16 | public void Clear() 17 | { 18 | AddCnt = SubCnt = 0; 19 | } 20 | 21 | [MethodImpl(Inline)] 22 | public void PushSub(int sub1) 23 | { 24 | Subs[SubCnt++] = sub1; 25 | } 26 | 27 | [MethodImpl(Inline)] 28 | public void PushSubAdd(int sub1, int add1) 29 | { 30 | Subs[SubCnt++] = sub1; 31 | Adds[AddCnt++] = add1; 32 | } 33 | 34 | [MethodImpl(Inline)] 35 | public void PushSubSubAdd(int sub1, int sub2, int add1) 36 | { 37 | Subs[SubCnt++] = sub1; 38 | Subs[SubCnt++] = sub2; 39 | Adds[AddCnt++] = add1; 40 | } 41 | 42 | [MethodImpl(Inline)] 43 | public void PushSubSubAddAdd(int sub1, int sub2, int add1, int add2) 44 | { 45 | Subs[SubCnt++] = sub1; 46 | Subs[SubCnt++] = sub2; 47 | Adds[AddCnt++] = add1; 48 | Adds[AddCnt++] = add2; 49 | } 50 | } 51 | 52 | [InlineArray(2)] 53 | public unsafe struct NetworkUpdate 54 | { 55 | public PerspectiveUpdate _Update; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Bindings/arch.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "defs.h" 4 | #include "simd.h" 5 | 6 | constexpr auto INPUT_BUCKETS = 14; 7 | constexpr auto INPUT_SIZE = 768; 8 | constexpr auto L1_SIZE = 2048; 9 | constexpr auto L2_SIZE = 16; 10 | constexpr auto L3_SIZE = 32; 11 | constexpr auto OUTPUT_BUCKETS = 8; 12 | 13 | constexpr auto FT_QUANT = 255; 14 | constexpr auto FT_SHIFT = 10; 15 | constexpr auto L1_QUANT = 132; 16 | constexpr auto OutputScale = 400; 17 | 18 | constexpr auto U8_CHUNK_SIZE = sizeof(vec_i8) / sizeof(u8); 19 | constexpr auto I16_CHUNK_SIZE = sizeof(vec_i16) / sizeof(i16); 20 | constexpr auto I32_CHUNK_SIZE = sizeof(vec_i32) / sizeof(i32); 21 | constexpr auto F32_CHUNK_SIZE = sizeof(vec_ps) / sizeof(float); 22 | 23 | constexpr auto NNZ_INPUT_SIMD_WIDTH = sizeof(vec_i32) / sizeof(i32); 24 | constexpr auto NNZ_CHUNK_SIZE = (NNZ_INPUT_SIMD_WIDTH > 8) ? NNZ_INPUT_SIMD_WIDTH : 8; 25 | constexpr auto NNZ_OUTPUTS_PER_CHUNK = NNZ_CHUNK_SIZE / 8; 26 | 27 | constexpr auto L1_CHUNK_PER_32 = sizeof(i32) / sizeof(i8); 28 | constexpr auto L1_PAIR_COUNT = L1_SIZE / 2; 29 | 30 | constexpr auto SIMD_CHUNKS = L1_SIZE / (sizeof(vec_i16) / sizeof(i16)); 31 | 32 | constexpr float L1_MUL = (1 << FT_SHIFT) / static_cast(FT_QUANT * FT_QUANT * L1_QUANT); 33 | 34 | constexpr auto N_FTW = INPUT_SIZE * L1_SIZE * INPUT_BUCKETS; 35 | constexpr auto N_FTB = L1_SIZE; 36 | constexpr auto N_L1W = OUTPUT_BUCKETS * L1_SIZE * L2_SIZE; 37 | constexpr auto N_L1B = OUTPUT_BUCKETS * L2_SIZE; 38 | constexpr auto N_L2W = OUTPUT_BUCKETS * L2_SIZE * L3_SIZE; 39 | constexpr auto N_L2B = OUTPUT_BUCKETS * L3_SIZE; 40 | constexpr auto N_L3W = OUTPUT_BUCKETS * L3_SIZE; 41 | constexpr auto N_L3B = OUTPUT_BUCKETS; -------------------------------------------------------------------------------- /Logic/Search/History/PlyHistoryTable.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Lizard.Logic.Search.History 4 | { 5 | public readonly unsafe struct PlyHistoryTable 6 | { 7 | private readonly PlyHistEntry* _History; 8 | private const int HistoryElements = MaxPlies * SquareNB * SquareNB; 9 | public const int MaxPlies = 4; 10 | 11 | public PlyHistoryTable() 12 | { 13 | _History = AlignedAllocZeroed(HistoryElements); 14 | } 15 | 16 | public PlyHistEntry this[int idx] 17 | { 18 | get => _History[idx]; 19 | set => _History[idx] = value; 20 | } 21 | 22 | public PlyHistEntry this[int ply, Move m] 23 | { 24 | get => _History[HistoryIndex(ply, m)]; 25 | set => _History[HistoryIndex(ply, m)] = value; 26 | } 27 | 28 | public static int HistoryIndex(int ply, Move m) => (ply * 4096) + m.MoveMask; 29 | 30 | public void Dispose() => NativeMemory.AlignedFree(_History); 31 | public void Clear() => NativeMemory.Clear(_History, (nuint)sizeof(PlyHistEntry) * HistoryElements); 32 | 33 | public readonly struct PlyHistEntry(short v) 34 | { 35 | private const int Clamp = 8192; 36 | 37 | public readonly short Value = v; 38 | public static implicit operator short(PlyHistEntry entry) => entry.Value; 39 | public static implicit operator PlyHistEntry(short s) => new(s); 40 | public static PlyHistEntry operator <<(PlyHistEntry entry, int bonus) => (PlyHistEntry)(entry + (bonus - (entry * Math.Abs(bonus) / Clamp))); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Logic/Search/History/ContinuationHistory.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Lizard.Logic.Search.History 4 | { 5 | /// 6 | /// Records the history for a pair of moves. 7 | ///

8 | /// This is an array of [12][64], with a size of . 9 | ///
10 | public unsafe struct ContinuationHistory 11 | { 12 | private PieceToHistory* _History; 13 | 14 | private const int DimX = PieceNB * 2; 15 | private const int DimY = SquareNB; 16 | 17 | /// 18 | /// 12 * 64 == 768 elements 19 | /// 20 | public const int Length = DimX * DimY; 21 | 22 | 23 | public ContinuationHistory() 24 | { 25 | _History = (PieceToHistory*)AlignedAllocZeroed((nuint)(sizeof(ulong) * Length), AllocAlignment); 26 | 27 | for (nuint i = 0; i < Length; i++) 28 | { 29 | (_History + i)->Alloc(); 30 | } 31 | } 32 | 33 | public PieceToHistory* this[int pc, int pt, int sq] => &_History[PieceToHistory.GetIndex(pc, pt, sq)]; 34 | public PieceToHistory* this[int idx] => &_History[idx]; 35 | 36 | public void Dispose() 37 | { 38 | for (nuint i = 0; i < Length; i++) 39 | { 40 | (_History + i)->Dispose(); 41 | } 42 | 43 | NativeMemory.AlignedFree(_History); 44 | } 45 | 46 | public void Clear() 47 | { 48 | for (nuint i = 0; i < Length; i++) 49 | { 50 | _History[i].Clear(); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/debug.yml: -------------------------------------------------------------------------------- 1 | name: Debug 2 | 3 | on: [workflow_dispatch] 4 | jobs: 5 | build: 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, windows-latest, macos-13, macos-14] 10 | include: 11 | - os: ubuntu-latest 12 | target: linux-x64 13 | - os: windows-latest 14 | target: win-x64 15 | - os: macos-13 16 | target: osx-x64 17 | - os: macos-14 18 | target: osx-arm64 19 | fail-fast: false 20 | env: 21 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 22 | 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Install .NET Core 28 | uses: actions/setup-dotnet@v3 29 | with: 30 | dotnet-version: 9.0 31 | 32 | - name: Build ${{ matrix.target }} 33 | run: dotnet publish Lizard.csproj --self-contained -p:DebugType=embedded -c debug -p:PublishAOT=true -p:PublishSingleFile=false -p:DefineConstants="PUBLISH_AOT" -r ${{ matrix.target }} -o "${{ github.workspace }}/builds/${{ matrix.target }}" 34 | 35 | - name: Check Compiler 36 | run: ${{ github.workspace }}/builds/${{ matrix.target }}/Lizard compiler 37 | 38 | - name: Run Bench 8 39 | run: ${{ github.workspace }}/builds/${{ matrix.target }}/Lizard bench 8 40 | 41 | - name: Check crashlog.txt 42 | if: ${{ failure() }} 43 | run: cat ${{ github.workspace }}/builds/${{ matrix.target }}/crashlog.txt 44 | 45 | - name: Upload ${{ matrix.target }} Build 46 | uses: actions/upload-artifact@v3 47 | with: 48 | name: Lizard-${{ matrix.target }} 49 | path: ${{ github.workspace }}/builds/${{ matrix.target }} 50 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [workflow_dispatch] 4 | jobs: 5 | build: 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, windows-latest, macos-13, macos-14] 10 | include: 11 | - os: ubuntu-latest 12 | target: linux-x64 13 | - os: windows-latest 14 | target: win-x64 15 | - os: macos-13 16 | target: osx-x64 17 | - os: macos-14 18 | target: osx-arm64 19 | fail-fast: false 20 | env: 21 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 22 | 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Install .NET Core 28 | uses: actions/setup-dotnet@v3 29 | with: 30 | dotnet-version: 9.0 31 | 32 | - name: Build ${{ matrix.target }} 33 | run: dotnet publish Lizard.csproj --self-contained -p:DebugType=embedded -c release -p:PublishAOT=true -p:PublishSingleFile=false -p:DefineConstants="PUBLISH_AOT" -r ${{ matrix.target }} -o "${{ github.workspace }}/builds/${{ matrix.target }}" 34 | 35 | - name: Check Compiler 36 | run: ${{ github.workspace }}/builds/${{ matrix.target }}/Lizard compiler 37 | 38 | - name: Run Bench 39 | run: ${{ github.workspace }}/builds/${{ matrix.target }}/Lizard bench 40 | 41 | - name: Check crashlog.txt 42 | if: ${{ failure() }} 43 | run: cat ${{ github.workspace }}/builds/${{ matrix.target }}/crashlog.txt 44 | 45 | - name: Upload ${{ matrix.target }} Build 46 | uses: actions/upload-artifact@v3 47 | with: 48 | name: Lizard-${{ matrix.target }} 49 | path: ${{ github.workspace }}/builds/${{ matrix.target }} 50 | -------------------------------------------------------------------------------- /Logic/Transposition/TTCluster.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Lizard.Logic.Transposition 4 | { 5 | /// 6 | /// Contains 3 TTEntry's, all of which map to a particular index within the Transposition Table. 7 | ///

8 | /// A pointer to this (TTCluster*) can be casted to a TTEntry* and indexed from 0 to 2 to access 9 | /// the individual TTEntry's since the offsets of the entries do not change. 10 | ///
11 | [StructLayout(LayoutKind.Explicit, Size = 32)] 12 | public unsafe struct TTCluster 13 | { 14 | [FieldOffset( 0)] private TTEntry _elem0; 15 | [FieldOffset(10)] private TTEntry _elem1; 16 | [FieldOffset(20)] private TTEntry _elem2; 17 | [FieldOffset(30)] private fixed byte _pad0[2]; 18 | 19 | /// 20 | /// Initializes the memory for this TTCluster instance. 21 | /// 22 | /// This constructor should ONLY be called on TTCluster's created with ! 23 | /// 24 | public TTCluster() 25 | { 26 | _elem0 = new TTEntry(); 27 | _elem1 = new TTEntry(); 28 | _elem2 = new TTEntry(); 29 | 30 | _pad0[0] = (byte)':'; 31 | _pad0[1] = (byte)')'; 32 | } 33 | 34 | /// 35 | /// Zeroes the memory for each of the three TTEntry's in this cluster. 36 | /// 37 | public void Clear() 38 | { 39 | fixed (void* ptr = &_elem0) 40 | { 41 | // Clear all 3 here. 42 | NativeMemory.Clear((void*)ptr, (nuint)sizeof(TTEntry) * 3); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Logic/Core/StateInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | using Lizard.Logic.NN; 4 | 5 | namespace Lizard.Logic.Core 6 | { 7 | /// 8 | /// Contains information for a single moment of a . 9 | ///

10 | /// When a is made, the will update one of these 11 | /// with information such as the move's captured piece and changes to either players , 12 | /// and keep track of the squares that checks can occur on. 13 | ///
14 | [StructLayout(LayoutKind.Explicit)] 15 | public unsafe struct StateInfo 16 | { 17 | public static readonly nuint StateCopySize = (nuint)Marshal.OffsetOf(nameof(_pad0)); 18 | 19 | [FieldOffset( 0)] public fixed ulong CheckSquares[PieceNB]; 20 | [FieldOffset( 48)] public fixed ulong BlockingPieces[2]; 21 | [FieldOffset( 64)] public fixed ulong Pinners[2]; 22 | [FieldOffset( 80)] public fixed int KingSquares[2]; 23 | [FieldOffset( 88)] public ulong Checkers = 0; 24 | [FieldOffset( 96)] public ulong Hash = 0; 25 | [FieldOffset(104)] public ulong PawnHash = 0; 26 | [FieldOffset(112)] public fixed ulong NonPawnHash[2]; 27 | [FieldOffset(128)] public int HalfmoveClock = 0; 28 | [FieldOffset(132)] public int EPSquare = EPNone; 29 | [FieldOffset(136)] public int CapturedPiece = None; 30 | [FieldOffset(140)] public int PliesFromNull = 0; 31 | [FieldOffset(144)] public CastlingStatus CastleStatus = CastlingStatus.None; 32 | [FieldOffset(148)] private fixed byte _pad0[4]; 33 | [FieldOffset(152)] public Accumulator* Accumulator; 34 | 35 | 36 | 37 | public StateInfo() 38 | { 39 | 40 | } 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Logic/Data/RunOptions.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | #define USE_AGGRESSIVE_INLINING 4 | 5 | //#define USE_SKIP_INIT 6 | 7 | //#define SKIP_INIT_IN_DEBUG 8 | 9 | 10 | #if USE_SKIP_INIT 11 | 12 | // Using SkipInit will cause methods to be generated without a ".locals init" flag, 13 | // meaning that local variables aren't automatically initialized to 0 when they are created. 14 | 15 | // This does seem to make things run 2-3% faster, but it can be dangerous 16 | // since stackalloc'd arrays will have junk data in them in the indices that haven't been written to. 17 | 18 | // So long as the code is written properly (which is the kicker...), there won't be any runtime differences 19 | // besides the slight performance improvement. 20 | 21 | 22 | // I prefer to have SkipInit off while debugging since the values that you mouse over can have confusing values 23 | #if RELEASE || DEV || SKIP_INIT_IN_DEBUG 24 | [module: System.Runtime.CompilerServices.SkipLocalsInit] 25 | #endif 26 | 27 | #endif 28 | 29 | 30 | namespace Lizard.Logic.Data 31 | { 32 | public static class RunOptions 33 | { 34 | 35 | // PreserveSig shouldn't have any meaningful impact on performance... I hope. 36 | 37 | #if USE_AGGRESSIVE_INLINING 38 | public const MethodImplOptions Inline = MethodImplOptions.AggressiveInlining; 39 | #else 40 | public const MethodImplOptions Inline = MethodImplOptions.PreserveSig; 41 | #endif 42 | 43 | public const MethodImplOptions NoInline = MethodImplOptions.NoInlining; 44 | 45 | #if PEXT 46 | public const bool HasPext = true; 47 | #else 48 | public const bool HasPext = false; 49 | #endif 50 | 51 | #if USE_SKIP_INIT && (RELEASE || DEV || SKIP_INIT_IN_DEBUG) 52 | public const bool HasSkipInit = true; 53 | #else 54 | public const bool HasSkipInit = false; 55 | #endif 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Logic/Threads/ConditionVariable.cs: -------------------------------------------------------------------------------- 1 | namespace Lizard.Logic.Threads 2 | { 3 | /// 4 | /// 5 | /// 6 | /// https://stackoverflow.com/questions/15657637/condition-variables-c-net/37163788#37163788 7 | /// 8 | /// 9 | /// 10 | public class ConditionVariable 11 | { 12 | private int waiters = 0; 13 | private object waitersLock = "cond_t"; 14 | private SemaphoreSlim sema = new SemaphoreSlim(0, Int32.MaxValue); 15 | 16 | public ConditionVariable() 17 | { 18 | } 19 | 20 | /// 21 | /// Releases the semaphore once if any threads are waiting on it. 22 | /// 23 | /// This is similar to pthread_cond_signal for the Linux inclined 24 | /// 25 | public void Pulse() 26 | { 27 | bool release; 28 | 29 | lock (waitersLock) 30 | { 31 | release = waiters > 0; 32 | } 33 | 34 | if (release) 35 | { 36 | sema.Release(); 37 | } 38 | } 39 | 40 | /// 41 | /// Releases the lock on , blocks on the condition, 42 | /// and finally reacquires the lock on before returning. 43 | /// 44 | /// must be locked before it is waited on or an exception will be thrown. 45 | /// 46 | /// This is similar to pthread_cond_wait for the Linux inclined 47 | /// 48 | public void Wait(object mutex) 49 | { 50 | lock (waitersLock) 51 | { 52 | ++waiters; 53 | } 54 | 55 | Monitor.Exit(mutex); 56 | 57 | sema.Wait(); 58 | 59 | lock (waitersLock) 60 | { 61 | --waiters; 62 | } 63 | 64 | Monitor.Enter(mutex); 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Logic/Datagen/BulletDataFormat.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Lizard.Logic.Datagen 5 | { 6 | [StructLayout(LayoutKind.Explicit)] 7 | public unsafe struct BulletDataFormat : TOutputFormat 8 | { 9 | // STM here is used to fix the game result, which is dependent on the STM: 10 | // If black is to move, the result is flipped around WhiteWin <-> BlackWin. 11 | // WE don't know the result when creating the entries, and STM isn't stored within them anywhere, 12 | // So manually place the STM in the last byte of padding of the entries. 13 | [FieldOffset( 0)] BulletFormatEntry BFE; 14 | [FieldOffset(29)] Move BestMove; 15 | [FieldOffset(31)] byte STM; 16 | 17 | public int Score 18 | { 19 | get => BFE.score; 20 | set => BFE.score = (short)value; 21 | } 22 | 23 | public GameResult Result 24 | { 25 | get => (GameResult)BFE.result; 26 | set => BFE.result = (byte)value; 27 | } 28 | 29 | public void SetSTM(int stm) { STM = (byte)stm; } 30 | public void SetResult(GameResult gr) 31 | { 32 | if (STM == Black) 33 | { 34 | gr = (GameResult)(2 - gr); 35 | } 36 | 37 | Result = gr; 38 | } 39 | 40 | public string GetWritableTextData() 41 | { 42 | return ""; 43 | } 44 | 45 | public byte[] GetWritableData() 46 | { 47 | int len = Marshal.SizeOf(); 48 | IntPtr ptr = Marshal.AllocHGlobal(len); 49 | byte[] myBuffer = new byte[len]; 50 | 51 | Marshal.StructureToPtr(BFE, ptr, false); 52 | Marshal.Copy(ptr, myBuffer, 0, len); 53 | Marshal.FreeHGlobal(ptr); 54 | 55 | return myBuffer; 56 | } 57 | 58 | public void Fill(Position pos, Move bestMove, int score) 59 | { 60 | BFE = BulletFormatEntry.FromBitboard(ref pos.bb, pos.ToMove, (short)score, GameResult.Draw); 61 | STM = (byte)pos.ToMove; 62 | BestMove = bestMove; 63 | } 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Logic/NN/Accumulator.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | using System.Runtime.Intrinsics; 4 | 5 | namespace Lizard.Logic.NN 6 | { 7 | /// 8 | /// Keeps track of the weights of the active features for both sides. 9 | /// 10 | public unsafe struct Accumulator 11 | { 12 | public const int ByteSize = Bucketed768.L1_SIZE * sizeof(short); 13 | 14 | public readonly short* White; 15 | public readonly short* Black; 16 | 17 | public fixed bool NeedsRefresh[2]; 18 | public fixed bool Computed[2]; 19 | public NetworkUpdate Update; 20 | 21 | public Accumulator() 22 | { 23 | White = AlignedAllocZeroed(Bucketed768.L1_SIZE); 24 | Black = AlignedAllocZeroed(Bucketed768.L1_SIZE); 25 | 26 | NeedsRefresh[Color.White] = NeedsRefresh[Color.Black] = true; 27 | Computed[Color.White] = Computed[Color.Black] = false; 28 | } 29 | 30 | public Vector256* this[int perspective] => (perspective == Color.White) ? (Vector256*)White : (Vector256*)Black; 31 | 32 | [MethodImpl(Inline)] 33 | public void CopyTo(Accumulator* target) 34 | { 35 | Unsafe.CopyBlock(target->White, White, ByteSize); 36 | Unsafe.CopyBlock(target->Black, Black, ByteSize); 37 | 38 | target->NeedsRefresh[0] = NeedsRefresh[0]; 39 | target->NeedsRefresh[1] = NeedsRefresh[1]; 40 | 41 | } 42 | 43 | [MethodImpl(Inline)] 44 | public void CopyTo(ref Accumulator target, int perspective) 45 | { 46 | Unsafe.CopyBlock(target[perspective], this[perspective], ByteSize); 47 | target.NeedsRefresh[perspective] = NeedsRefresh[perspective]; 48 | } 49 | 50 | public void ResetWithBiases(short* biases, uint byteCount) 51 | { 52 | Unsafe.CopyBlock(White, biases, byteCount); 53 | Unsafe.CopyBlock(Black, biases, byteCount); 54 | } 55 | 56 | public void Dispose() 57 | { 58 | NativeMemory.AlignedFree(White); 59 | NativeMemory.AlignedFree(Black); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /Logic/Util/ExceptionHandling.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Lizard.Logic.Util 4 | { 5 | public static class ExceptionHandling 6 | { 7 | 8 | [System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.NoInlining)] 9 | [Conditional("ENABLE_ASSERTIONS")] 10 | public static void Assert(bool condition, string message) 11 | { 12 | #if DEBUG 13 | Debug.Assert(condition, message); 14 | #else 15 | if (!condition) 16 | { 17 | throw new AssertionException("Assertion failed: " + message + Environment.NewLine); 18 | } 19 | #endif 20 | } 21 | 22 | public static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs args) 23 | { 24 | Exception e = (Exception)args.ExceptionObject; 25 | 26 | if (e.GetType() == typeof(AssertionException)) 27 | { 28 | // This is "handled" 29 | return; 30 | } 31 | 32 | Log("An UnhandledException occurred!\r\n" + e.ToString()); 33 | using (FileStream fs = new FileStream(@".\crashlog.txt", FileMode.Create, FileAccess.Write, FileShare.Read)) 34 | { 35 | using StreamWriter sw = new StreamWriter(fs); 36 | 37 | sw.WriteLine("An UnhandledException occurred!\r\n" + e.ToString()); 38 | 39 | sw.Flush(); 40 | } 41 | 42 | if (UCIClient.Active) 43 | { 44 | // Try to tell the UCI what happened before this process terminates 45 | Console.WriteLine("info string I'm going to crash! Exception: "); 46 | 47 | // Send each exception line separately, in case the UCI doesn't like 48 | // newlines in the strings that it reads. 49 | foreach (string s in e.ToString().Split(Environment.NewLine)) 50 | { 51 | Console.WriteLine("info string " + s); 52 | Thread.Sleep(10); 53 | } 54 | 55 | } 56 | } 57 | 58 | } 59 | 60 | public class AssertionException : Exception 61 | { 62 | public AssertionException() { } 63 | public AssertionException(string message) : base(message) { } 64 | public AssertionException(string message, Exception inner) : base(message, inner) { } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Logic/Search/SearchStackEntry.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Text; 3 | 4 | using Lizard.Logic.Search.History; 5 | 6 | namespace Lizard.Logic.Search 7 | { 8 | /// 9 | /// Used during a search to keep track of information from earlier plies/depths 10 | /// 11 | [StructLayout(LayoutKind.Explicit, Size = 32)] 12 | public unsafe struct SearchStackEntry 13 | { 14 | public static SearchStackEntry NullEntry = new SearchStackEntry(); 15 | 16 | [FieldOffset( 0)] public Move* PV; 17 | [FieldOffset( 8)] public PieceToHistory* ContinuationHistory; 18 | [FieldOffset(16)] public short DoubleExtensions; 19 | [FieldOffset(18)] public short Ply; 20 | [FieldOffset(20)] public short StaticEval; 21 | [FieldOffset(22)] public Move KillerMove; 22 | [FieldOffset(24)] public Move CurrentMove; 23 | [FieldOffset(26)] public Move Skip; 24 | [FieldOffset(28)] public bool InCheck; 25 | [FieldOffset(29)] public bool TTPV; 26 | [FieldOffset(30)] public bool TTHit; 27 | [FieldOffset(31)] private fixed byte _pad0[1]; 28 | 29 | 30 | public SearchStackEntry() 31 | { 32 | Clear(); 33 | } 34 | 35 | /// 36 | /// Zeroes the fields within this Entry. 37 | /// 38 | public void Clear() 39 | { 40 | CurrentMove = Move.Null; 41 | Skip = Move.Null; 42 | ContinuationHistory = null; 43 | 44 | Ply = 0; 45 | DoubleExtensions = 0; 46 | StaticEval = ScoreNone; 47 | 48 | InCheck = false; 49 | TTPV = false; 50 | TTHit = false; 51 | 52 | if (PV != null) 53 | { 54 | NativeMemory.AlignedFree(PV); 55 | PV = null; 56 | } 57 | 58 | KillerMove = Move.Null; 59 | } 60 | 61 | public static string GetMovesPlayed(SearchStackEntry* curr) 62 | { 63 | StringBuilder sb = new StringBuilder(); 64 | 65 | // Not using a while loop here to prevent infinite loops or some other nonsense. 66 | for (int i = curr->Ply; i >= 0; i--) 67 | { 68 | sb.Insert(0, curr->CurrentMove.ToString() + " "); 69 | curr--; 70 | } 71 | 72 | return sb.ToString(); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Logic/Search/WDL.cs: -------------------------------------------------------------------------------- 1 | namespace Lizard.Logic.Search 2 | { 3 | public static unsafe class WDL 4 | { 5 | private static ReadOnlySpan Mat_As => [-140.14947546, 473.31880899, -605.86369938, 524.99731145]; 6 | private static ReadOnlySpan Mat_Bs => [-28.43262875, 101.83936924, -80.27661412, 94.27337419]; 7 | public static (int win, int loss) MaterialModel(int score, int mat) 8 | { 9 | double m = Math.Clamp(mat, 17, 78) / 48.0; 10 | double x = Math.Clamp(score, -4000, 4000); 11 | 12 | double a = (((Mat_As[0] * m + Mat_As[1]) * m + Mat_As[2]) * m) + Mat_As[3]; 13 | double b = (((Mat_Bs[0] * m + Mat_Bs[1]) * m + Mat_Bs[2]) * m) + Mat_Bs[3]; 14 | 15 | int win = (int)double.Round(1000.0 / (1 + double.Exp((a - x) / b))); 16 | int loss = (int)double.Round(1000.0 / (1 + double.Exp((a + x) / b))); 17 | 18 | return (win, loss); 19 | } 20 | 21 | public static (int win, int loss) MaterialModel(int score, Position pos) 22 | { 23 | ref Bitboard bb = ref pos.bb; 24 | var mat = popcount(bb.Pieces[Queen]) * 9 + 25 | popcount(bb.Pieces[Rook]) * 5 + 26 | popcount(bb.Pieces[Bishop]) * 3 + 27 | popcount(bb.Pieces[Knight]) * 3 + 28 | popcount(bb.Pieces[Pawn]) * 1; 29 | return MaterialModel(score, (int)mat); 30 | } 31 | 32 | 33 | private static ReadOnlySpan Ply_As => [10.96887666, -62.17283318, 251.25333664, 172.65209925]; 34 | private static ReadOnlySpan Ply_Bs => [-23.34690728, 98.46140259, -111.32704842, 117.15171803]; 35 | 36 | public static (int win, int loss) PlyModel(int score, int ply) 37 | { 38 | double m = Math.Clamp(ply, 8, 160) / 64.0; 39 | double x = Math.Clamp(score, -4000, 4000); 40 | 41 | double a = (((Ply_As[0] * m + Ply_As[1]) * m + Ply_As[2]) * m) + Ply_As[3]; 42 | double b = (((Ply_Bs[0] * m + Ply_Bs[1]) * m + Ply_Bs[2]) * m) + Ply_Bs[3]; 43 | 44 | int win = (int)double.Round(1000.0 / (1 + double.Exp((a - x) / b))); 45 | int loss = (int)double.Round(1000.0 / (1 + double.Exp((a + x) / b))); 46 | 47 | return (win, loss); 48 | } 49 | 50 | public static (int win, int loss) PlyModel(int score, Position pos) 51 | { 52 | int ply = pos.FullMoves * 2 - Not(pos.ToMove) - 1; 53 | return PlyModel(score, ply); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Logic/Search/History/PieceToHistory.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Lizard.Logic.Search.History 5 | { 6 | /// 7 | /// Records how successful different moves have been in the past by recording that move's 8 | /// piece type and color, and the square that it is moving to. 9 | ///

10 | /// This is a short array with dimensions [12][64], with a size of . 11 | ///
12 | public unsafe struct PieceToHistory 13 | { 14 | private StatEntry* _History; 15 | 16 | private const short FillValue = -50; 17 | 18 | private const int DimX = PieceNB * 2; 19 | private const int DimY = SquareNB; 20 | 21 | /// 22 | /// 12 * 64 == 768 elements 23 | /// 24 | public const int Length = DimX * DimY; 25 | 26 | public PieceToHistory() { } 27 | 28 | public void Dispose() 29 | { 30 | NativeMemory.AlignedFree(_History); 31 | } 32 | 33 | public StatEntry this[int pc, int pt, int sq] 34 | { 35 | get => _History[GetIndex(pc, pt, sq)]; 36 | set => _History[GetIndex(pc, pt, sq)] = value; 37 | } 38 | 39 | public StatEntry this[int idx] 40 | { 41 | get => _History[idx]; 42 | set => _History[idx] = value; 43 | } 44 | 45 | /// 46 | /// Returns the index of the score in the History array for a piece of color 47 | /// and type moving to the square . 48 | /// 49 | public static int GetIndex(int pc, int pt, int sq) 50 | { 51 | Assert((((pt + (PieceNB * pc)) * DimY) + sq) is >= 0 and < (int)Length, $"GetIndex({pc}, {pt}, {sq}) should be < {Length}"); 52 | 53 | return ((pt + (PieceNB * pc)) * DimY) + sq; 54 | } 55 | 56 | /// 57 | /// Allocates memory for this instance's array. 58 | /// 59 | public void Alloc() 60 | { 61 | _History = AlignedAllocZeroed(Length); 62 | } 63 | 64 | /// 65 | /// Fills this instance's array with the value of . 66 | /// 67 | public void Clear() 68 | { 69 | NativeMemory.Clear(_History, (nuint)(sizeof(StatEntry) * Length)); 70 | Span span = new Span(_History, (int)Length); 71 | span.Fill(FillValue); 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /Logic/Datagen/ProgressBroker.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace Lizard.Logic.Datagen 4 | { 5 | public static class ProgressBroker 6 | { 7 | private static readonly ConcurrentDictionary ThreadGameTotals = new ConcurrentDictionary(); 8 | private static readonly ConcurrentDictionary ThreadPositionTotals = new ConcurrentDictionary(); 9 | private static readonly ConcurrentDictionary ThreadNPS = new ConcurrentDictionary(); 10 | private static readonly CancellationTokenSource TokenSource = new CancellationTokenSource(); 11 | 12 | public static void StartMonitoring() 13 | { 14 | Task.Run(() => MonitorProgress(TokenSource.Token)); 15 | } 16 | 17 | public static void StopMonitoring() 18 | { 19 | TokenSource.Cancel(); 20 | } 21 | 22 | private static void MonitorProgress(CancellationToken token) 23 | { 24 | Console.WriteLine("\n"); 25 | Console.WriteLine(" games positions pos/sec"); 26 | (int _, int top) = Console.GetCursorPosition(); 27 | 28 | while (!token.IsCancellationRequested) 29 | { 30 | Console.SetCursorPosition(0, top); 31 | Console.CursorVisible = false; 32 | for (int y = 0; y < Console.WindowHeight - top; y++) 33 | Console.Write(new string(' ', Console.WindowWidth)); 34 | Console.SetCursorPosition(0, top); 35 | //Console.CursorVisible = true; 36 | 37 | ulong totalGames = 0; 38 | double totalNPS = 0; 39 | ulong totalPositions = 0; 40 | 41 | foreach (var kvp in ThreadGameTotals) 42 | { 43 | int id = kvp.Key; 44 | var games = kvp.Value; 45 | var positions = ThreadPositionTotals[id]; 46 | var nps = ThreadNPS[id]; 47 | 48 | Console.WriteLine($"Thread {id,3}: {games,12} {positions,15:N0} {nps,12:N2}"); 49 | 50 | totalGames += games; 51 | totalPositions += positions; 52 | totalNPS += nps; 53 | } 54 | 55 | Console.WriteLine($" ------------------------------------------"); 56 | Console.WriteLine($" {totalGames,12} {totalPositions,15:N0} {totalNPS,12:N2}"); 57 | 58 | Thread.Sleep(250); 59 | } 60 | } 61 | 62 | public static void ReportProgress(int threadId, ulong gameNum, ulong totalPositions, double nps) 63 | { 64 | ThreadGameTotals[threadId] = gameNum; 65 | ThreadPositionTotals[threadId] = totalPositions; 66 | ThreadNPS[threadId] = nps; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Lizard.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | public class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | public static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Lizard.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | public static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Logic/NN/NetContainer.cs: -------------------------------------------------------------------------------- 1 | 2 | #pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type 3 | #pragma warning disable CA1814 // Prefer jagged arrays over multidimensional 4 | 5 | using static Lizard.Logic.NN.Bucketed768; 6 | 7 | namespace Lizard.Logic.NN 8 | { 9 | 10 | public unsafe struct NetContainer 11 | { 12 | public readonly T* FTWeights; 13 | public readonly T* FTBiases; 14 | public readonly W** L1Weights; 15 | public readonly U** L1Biases; 16 | public readonly U** L2Weights; 17 | public readonly U** L2Biases; 18 | public readonly U** L3Weights; 19 | public readonly U* L3Biases; 20 | 21 | public NetContainer() 22 | { 23 | FTWeights = (T*)AlignedAllocZeroed((nuint)sizeof(T) * INPUT_SIZE * L1_SIZE * INPUT_BUCKETS); 24 | FTBiases = (T*)AlignedAllocZeroed((nuint)sizeof(T) * L1_SIZE); 25 | 26 | 27 | L1Weights = (W**)AlignedAllocZeroed((nuint)sizeof(W*) * OUTPUT_BUCKETS); 28 | L1Biases = (U**)AlignedAllocZeroed((nuint)sizeof(U*) * OUTPUT_BUCKETS); 29 | L2Weights = (U**)AlignedAllocZeroed((nuint)sizeof(U*) * OUTPUT_BUCKETS); 30 | L2Biases = (U**)AlignedAllocZeroed((nuint)sizeof(U*) * OUTPUT_BUCKETS); 31 | L3Weights = (U**)AlignedAllocZeroed((nuint)sizeof(U*) * OUTPUT_BUCKETS); 32 | L3Biases = (U*) AlignedAllocZeroed((nuint)sizeof(U) * OUTPUT_BUCKETS); 33 | 34 | for (int i = 0; i < OUTPUT_BUCKETS; i++) 35 | { 36 | L1Weights[i] = (W*)AlignedAllocZeroed((nuint)sizeof(W) * (L1_SIZE * L2_SIZE)); 37 | L1Biases[i] = (U*)AlignedAllocZeroed((nuint)sizeof(U) * (L2_SIZE)); 38 | L2Weights[i] = (U*)AlignedAllocZeroed((nuint)sizeof(U) * (L2_SIZE * L3_SIZE )); 39 | L2Biases[i] = (U*)AlignedAllocZeroed((nuint)sizeof(U) * (L3_SIZE)); 40 | L3Weights[i] = (U*)AlignedAllocZeroed((nuint)sizeof(U) * (L3_SIZE)); 41 | } 42 | } 43 | } 44 | 45 | public unsafe struct UQNetContainer 46 | { 47 | public readonly short[] FTWeights; 48 | public readonly short[] FTBiases; 49 | public readonly sbyte[,,] L1Weights; 50 | public readonly float[,] L1Biases; 51 | public readonly float[,,] L2Weights; 52 | public readonly float[,] L2Biases; 53 | public readonly float[,] L3Weights; 54 | public readonly float[] L3Biases; 55 | 56 | public UQNetContainer() 57 | { 58 | FTWeights = new short[INPUT_SIZE * L1_SIZE * INPUT_BUCKETS]; 59 | FTBiases = new short[L1_SIZE]; 60 | 61 | L1Weights = new sbyte[L1_SIZE, OUTPUT_BUCKETS, L2_SIZE]; 62 | L1Biases = new float[OUTPUT_BUCKETS, L2_SIZE]; 63 | 64 | L2Weights = new float[L2_SIZE, OUTPUT_BUCKETS, L3_SIZE]; 65 | L2Biases = new float[OUTPUT_BUCKETS, L3_SIZE]; 66 | 67 | L3Weights = new float[L3_SIZE, OUTPUT_BUCKETS]; 68 | L3Biases = new float[OUTPUT_BUCKETS]; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Logic/Search/SearchInformation.cs: -------------------------------------------------------------------------------- 1 | 2 | #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. 3 | 4 | namespace Lizard.Logic.Search 5 | { 6 | public struct SearchInformation 7 | { 8 | /// 9 | /// The normal delegate doesn't allow passing by reference 10 | /// 11 | public delegate void ActionRef(ref T info); 12 | 13 | /// 14 | /// The method to call (which must accept a parameter) 15 | /// during a search just before the depth increases. 16 | ///

17 | /// By default, this will print out the "info depth # ..." string. 18 | ///
19 | public ActionRef? OnDepthFinish; 20 | 21 | /// 22 | /// The method to call (which must accept a parameter) 23 | /// when a search is finished. 24 | ///

25 | /// By default, this will print "bestmove (move)". 26 | ///
27 | public ActionRef? OnSearchFinish; 28 | 29 | public Position Position; 30 | 31 | public int DepthLimit = Utilities.MaxDepth; 32 | public ulong NodeLimit = MaxSearchNodes; 33 | public ulong SoftNodeLimit = MaxSearchNodes; 34 | 35 | 36 | /// 37 | /// Set to true the first time that OnSearchFinish is invoked. 38 | /// 39 | public bool SearchFinishedCalled = false; 40 | 41 | /// 42 | /// Set to true while a search is ongoing, and false otherwise. 43 | /// 44 | public bool SearchActive = false; 45 | 46 | public TimeManager TimeManager; 47 | 48 | public bool HasDepthLimit => (DepthLimit != Utilities.MaxDepth); 49 | public bool HasNodeLimit => (NodeLimit != MaxSearchNodes); 50 | public bool HasTimeLimit => (this.TimeManager.MaxSearchTime != SearchConstants.MaxSearchTime); 51 | 52 | public bool IsInfinite => !HasDepthLimit && !HasTimeLimit; 53 | 54 | public SearchInformation(Position p, int depth = Utilities.MaxDepth, int searchTime = SearchConstants.MaxSearchTime) 55 | { 56 | this.Position = p; 57 | this.DepthLimit = depth; 58 | 59 | this.TimeManager = new TimeManager(); 60 | this.TimeManager.MaxSearchTime = searchTime; 61 | 62 | this.OnDepthFinish = Utilities.PrintSearchInfo; 63 | this.OnSearchFinish = (ref SearchInformation info) => Log($"bestmove {info.Position.Owner.AssocPool.GetBestThread().RootMoves[0].Move.ToString()}"); 64 | } 65 | 66 | public void SetMoveTime(int moveTime) 67 | { 68 | TimeManager.MaxSearchTime = moveTime; 69 | TimeManager.HasMoveTime = true; 70 | } 71 | 72 | public override string ToString() 73 | { 74 | return $"DepthLimit: {DepthLimit}, NodeLimit: {NodeLimit}, MaxSearchTime: {MaxSearchTime}, SearchTime: " 75 | + (TimeManager == null ? "0 (NULL!)" : TimeManager.GetSearchTime()); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Properties/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | [assembly: SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "These are only Lists because arrays are more annoying to use", Scope = "namespaceanddescendants", Target = "~N:Lizard.Logic.Threads")] 5 | [assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "These aren't handled in an exception-specific way", Scope = "namespaceanddescendants", Target = "~N:Lizard.Logic")] 6 | [assembly: SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "This is a fair point but I don't care", Scope = "namespaceanddescendants", Target = "~N:Lizard.Logic")] 7 | [assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Validate them yourself, you lazy calling method", Scope = "namespaceanddescendants", Target = "~N:Lizard.Logic")] 8 | [assembly: SuppressMessage("Design", "CA1040:Avoid empty interfaces", Scope = "namespaceanddescendants", Target = "~N:Lizard")] 9 | 10 | 11 | 12 | [assembly: SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "These are for informational purposes only", Scope = "namespaceanddescendants", Target = "~N:Lizard")] 13 | [assembly: SuppressMessage("Globalization", "CA1304:Specify CultureInfo", Justification = "Most strings (FEN, UCI, etc.) are in English", Scope = "namespaceanddescendants", Target = "~N:Lizard")] 14 | [assembly: SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Most strings (FEN, UCI, etc.) are in English", Scope = "namespaceanddescendants", Target = "~N:Lizard")] 15 | [assembly: SuppressMessage("Globalization", "CA1307:Specify StringComparison for clarity", Justification = "Most strings (FEN, UCI, etc.) are in English", Scope = "namespaceanddescendants", Target = "~N:Lizard")] 16 | [assembly: SuppressMessage("Globalization", "CA1311:Specify a culture or use an invariant version", Justification = "Piece names are always in English", Scope = "namespaceanddescendants", Target = "~N:Lizard")] 17 | [assembly: SuppressMessage("Globalization", "CA1310:Specify StringComparison for correctness", Scope = "namespaceanddescendants", Target = "~N:Lizard")] 18 | 19 | 20 | 21 | [assembly: SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline", Justification = "This doesn't seem to be an issue", Scope = "namespaceanddescendants", Target = "~N:Lizard")] 22 | [assembly: SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Scope = "namespaceanddescendants", Target = "~N:Lizard.Logic")] 23 | [assembly: SuppressMessage("Performance", "CA1805:Do not initialize unnecessarily", Scope = "namespaceanddescendants", Target = "~N:Lizard.Logic")] 24 | 25 | 26 | 27 | [assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Constants are more readable", Scope = "namespaceanddescendants", Target = "~N:Lizard")] 28 | 29 | 30 | 31 | [assembly: SuppressMessage("Usage", "CA2211:Non-constant fields should not be visible", Scope = "namespaceanddescendants", Target = "~N:Lizard")] 32 | [assembly: SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Scope = "namespaceanddescendants", Target = "~N:Lizard")] 33 | 34 | 35 | 36 | [assembly: SuppressMessage("Maintainability", "CA1508:Avoid dead conditional code", Scope = "namespaceanddescendants", Target = "~N:Lizard")] 37 | 38 | -------------------------------------------------------------------------------- /Logic/Datagen/BulletFormatEntry.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Buffers.Binary; 3 | using System.Numerics; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace Lizard.Logic.Datagen 7 | { 8 | [StructLayout(LayoutKind.Explicit, Size = 32)] 9 | public unsafe struct BulletFormatEntry 10 | { 11 | public const int Size = 32; 12 | 13 | [FieldOffset(0)] public ulong occ; 14 | [FieldOffset(8)] public fixed byte pcs[16]; 15 | [FieldOffset(24)] public short score; 16 | [FieldOffset(26)] public byte result; 17 | [FieldOffset(27)] public byte ksq; 18 | [FieldOffset(28)] public byte opp_ksq; 19 | [FieldOffset(29)] public fixed byte _pad[3]; 20 | 21 | 22 | 23 | public void FillBitboard(ref Bitboard bb) 24 | { 25 | bb.Reset(); 26 | 27 | bb.Occupancy = occ; 28 | 29 | ulong temp = occ; 30 | int idx = 0; 31 | while (temp != 0) 32 | { 33 | int sq = poplsb(&temp); 34 | int piece = (pcs[idx / 2] >> (4 * (idx & 1))) & 0b1111; 35 | 36 | bb.AddPiece(sq, piece / 8, piece % 8); 37 | 38 | idx++; 39 | } 40 | } 41 | 42 | 43 | 44 | public static BulletFormatEntry FromBitboard(ref Bitboard bb, int stm, short score, GameResult result) 45 | { 46 | Span bbs = [ 47 | bb.Colors[White], bb.Colors[Black], 48 | bb.Pieces[0], bb.Pieces[1], bb.Pieces[2], 49 | bb.Pieces[3], bb.Pieces[4], bb.Pieces[5], 50 | ]; 51 | 52 | if (stm == Black) 53 | { 54 | for (int i = 0; i < bbs.Length; i++) 55 | bbs[i] = BinaryPrimitives.ReverseEndianness(bbs[i]); 56 | 57 | (bbs[White], bbs[Black]) = (bbs[Black], bbs[White]); 58 | 59 | score = (short)-score; 60 | result = 1 - result; 61 | } 62 | 63 | ulong occ = bbs[0] | bbs[1]; 64 | 65 | BulletFormatEntry bfe = new BulletFormatEntry 66 | { 67 | score = score, 68 | occ = occ, 69 | result = (byte)(2 * (int)result), 70 | ksq = (byte) BitOperations.TrailingZeroCount(bbs[0] & bbs[7]), 71 | opp_ksq = (byte)(BitOperations.TrailingZeroCount(bbs[1] & bbs[7]) ^ 56) 72 | }; 73 | 74 | Span pieces = stackalloc byte[16]; 75 | 76 | int idx = 0; 77 | ulong occ2 = occ; 78 | int piece = 0; 79 | while (occ2 > 0) 80 | { 81 | int sq = BitOperations.TrailingZeroCount(occ2); 82 | ulong bit = 1UL << sq; 83 | occ2 &= occ2 - 1; 84 | 85 | byte colour = (byte)(((bit & bbs[1]) > 0 ? 1 : 0) << 3); 86 | for (int i = 2; i < 8; i++) 87 | if ((bit & bbs[i]) > 0) 88 | { 89 | piece = i - 2; 90 | break; 91 | } 92 | 93 | byte pc = (byte)(colour | (byte)piece); 94 | 95 | bfe.pcs[idx / 2] |= (byte)(pc << (4 * (idx & 1))); 96 | 97 | idx += 1; 98 | } 99 | 100 | return bfe; 101 | } 102 | 103 | 104 | 105 | public void WriteToBuffer(Span buff) 106 | { 107 | fixed (void* buffPtr = &buff[0], thisPtr = &this) 108 | { 109 | Unsafe.CopyBlock(buffPtr, thisPtr, BulletFormatEntry.Size); 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Lizard - A C# chess engine 3 |

4 | 5 |

6 | 7 |

8 | 9 | Creating this in my spare time, mainly using it to learn more about optimization and computer games. 10 | I'm uploading it here so I can keep backups of it and not lose it when my laptop finally dies. 11 | 12 | ## Ratings 13 |
14 | 15 | | Version | Released | [CCRL 40/15](https://www.computerchess.org.uk/ccrl/4040/) | [CCRL Blitz](https://www.computerchess.org.uk/ccrl/404/) | [CCRL FRC](https://www.computerchess.org.uk/ccrl/404FRC/) | Notes | 16 | | ---- | ------------ | ---- | ---- | ---- | --- | 17 | | 10.0 | Jan. 4 2024 | 3368 | 3409 | - | First non-Stockfish NNUE | 18 | | 10.1 | Jan. 13 2024 | 3430 | - | - | Various improvements to search | 19 | | 10.2 | Feb. 9 2024 | 3499 | 3587 | - | Larger network, more tunes | 20 | | 10.3 | Mar. 8 2024 | 3513 | - | 3600 | Significant speedups, FRC support | 21 | | 10.4 | Jun. 2 2024 | 3548 | 3635 | 3612 | Larger network, better time management | 22 | | 10.5 | Jul. 13 2024 | 3556 | 3665 | 3685 | Significant speedups, DFRC data | 23 | | 11.0 | Sep. 26 2024 | 3555 | 3664 | 3810 | More heuristics, Selfplay data | 24 | | 11.1 | Nov. 10 2024 | 3563 | 3682 | 3914 | QOL/bugfixes, more DFRC data | 25 | | 11.2 | Jan. 8 2025 | TBD | TBD | TBD | Larger network, speedups | 26 | 27 |
28 | 29 | ## Features 30 | ### NNUE Evaluation: 31 | Version 11.2 uses a (768x14 -> 2048)x2 -> (16 -> 32 -> 1)x8 network with ~6.6 billion positions. 32 | 33 | Version 11.1 used a (768x16 -> 2048)x2 -> 8 network with ~4.5 billion positions. 34 | 35 | Version 10.5 used a (768x5 -> 1536)x2 -> 8 network, and used ~8 billion positions from a collection of Lc0 datasets. 36 | 37 | All networks are trained using [Bullet](https://github.com/jw1912/bullet). Networks after 10.5 use incremental self-play data starting with the network from version 10.5. 38 | 39 | ## Building 40 | Using `make` is the easiest way, as this calls `dotnet publish` with the proper parameters. 41 | If your processor supports Avx512, you can also use `make 512` to compile a binary that fully uses those instructions during NNUE inference. 42 | 43 | > [!NOTE] 44 | > Building Lizard requires at least .NET 8.0 or higher. 45 | 46 | > [!NOTE] 47 | > Building the optional bindings requires a g++ version supporting C++20. 48 | 49 | > [!IMPORTANT] 50 | > NNUE networks are served in [a separate repository](https://github.com/liamt19/lizard-nets/) to keep the size of this main repository small (thanks to [Stormphrax](https://github.com/Ciekce/Stormphrax) author Ciekce for the process). MSBuild will automatically take care of this by retrieving the network specified in the [network.txt file in the repo root](/network.txt). If this fails for whatever reason, you can also [manually download the corresponding network file](https://github.com/liamt19/lizard-nets/releases/latest) and place it in the directory. 51 | 52 | ## Some spotty history: 53 | #### Version 9.3: 54 | Uses its own NNUE evaluation, and began proper parameter testing with [SPRT](https://en.wikipedia.org/wiki/Sequential_probability_ratio_test). 55 | 9.3.1 was the last version to be named "LTChess". 56 | 57 | #### Version 9.1: 58 | Some major speed improvements to both searches and move generation. 59 | It was rated a bit above 2500 bullet/blitz on Lichess. 60 | 61 | #### Version 8.4: 62 | A decent rating increase, and a lot fewer "dumb" moves. 63 | Many of the commits between 8.0 and 8.4 improved some of the early architectural decisions, and it is now far easier to debug and improve the code. 64 | It was rated a bit above 2400 bullet/blitz on Lichess. 65 | 66 | #### Version 7.0: 67 | A large rating increase (around 250) and was far more polished. 68 | It was rated a bit above 2000 bullet on Lichess. 69 | 70 | 71 | 72 | ## Contributing 73 | If you have any ideas or comments, feel free to create an issue or pull request! 74 | -------------------------------------------------------------------------------- /Logic/Search/Ordering/HistoryTable.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Lizard.Logic.Search.History 4 | { 5 | /// 6 | /// Holds instances of the MainHistory, CaptureHistory, and 4 ContinuationHistory's for a single SearchThread. 7 | /// 8 | public unsafe struct HistoryTable 9 | { 10 | public const int NormalClamp = 16384; 11 | 12 | /// 13 | /// Stores history for a color moving from one square to another 14 | /// 15 | public readonly MainHistoryTable MainHistory; 16 | 17 | /// 18 | /// Stores history for a piece + color capturing a piece on a square 19 | /// 20 | public readonly CaptureHistoryTable CaptureHistory; 21 | 22 | public readonly PlyHistoryTable PlyHistory; 23 | 24 | /// 25 | /// Stores history for how far off the static evaluation was from the result of a search for either color, 26 | /// indexed by a position's pawn PSQT hash. 27 | /// 28 | public PawnCorrectionTable PawnCorrection; 29 | 30 | /// 31 | /// Stores history for how far off the static evaluation was from the result of a search for either color, 32 | /// indexed by a position's non-pawn PSQT hash. 33 | /// 34 | public NonPawnCorrectionTable NonPawnCorrection; 35 | 36 | /// 37 | /// Index with [inCheck] [Capture] 38 | /// 39 | /// Continuations[0][0] is the PieceToHistory[][] for a non-capture while we aren't in check, 40 | /// and that PieceToHistory[0, 1, 2] is the correct PieceToHistory for a white (0) knight (1) moving to C1 (2). 41 | /// This is then used by .AssignScores 42 | /// 43 | public readonly ContinuationHistory** Continuations; 44 | 45 | 46 | 47 | public HistoryTable() 48 | { 49 | MainHistory = new MainHistoryTable(); 50 | CaptureHistory = new CaptureHistoryTable(); 51 | PlyHistory = new PlyHistoryTable(); 52 | PawnCorrection = new PawnCorrectionTable(); 53 | NonPawnCorrection = new NonPawnCorrectionTable(); 54 | 55 | // 5D arrays aren't real, they can't hurt you. 56 | // 5D arrays: 57 | Continuations = (ContinuationHistory**)AlignedAllocZeroed((nuint)(sizeof(ContinuationHistory*) * 2)); 58 | ContinuationHistory* cont0 = (ContinuationHistory*)AlignedAllocZeroed((nuint)(sizeof(ContinuationHistory*) * 2)); 59 | ContinuationHistory* cont1 = (ContinuationHistory*)AlignedAllocZeroed((nuint)(sizeof(ContinuationHistory*) * 2)); 60 | 61 | cont0[0] = new ContinuationHistory(); 62 | cont0[1] = new ContinuationHistory(); 63 | 64 | cont1[0] = new ContinuationHistory(); 65 | cont1[1] = new ContinuationHistory(); 66 | 67 | Continuations[0] = cont0; 68 | Continuations[1] = cont1; 69 | } 70 | 71 | public void Dispose() 72 | { 73 | MainHistory.Dispose(); 74 | CaptureHistory.Dispose(); 75 | PlyHistory.Dispose(); 76 | PawnCorrection.Dispose(); 77 | NonPawnCorrection.Dispose(); 78 | 79 | for (int i = 0; i < 2; i++) 80 | { 81 | Continuations[i][0].Dispose(); 82 | Continuations[i][1].Dispose(); 83 | } 84 | 85 | NativeMemory.AlignedFree(Continuations); 86 | } 87 | 88 | public void Clear() 89 | { 90 | MainHistory.Clear(); 91 | CaptureHistory.Clear(); 92 | PlyHistory.Clear(); 93 | PawnCorrection.Clear(); 94 | NonPawnCorrection.Clear(); 95 | 96 | for (int i = 0; i < 2; i++) 97 | { 98 | Continuations[i][0].Clear(); 99 | Continuations[i][1].Clear(); 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Logic/Util/HorsieBindings.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Reflection; 3 | using System.Runtime.InteropServices; 4 | using static System.Runtime.InteropServices.RuntimeInformation; 5 | 6 | namespace Lizard.Logic.Util 7 | { 8 | public static unsafe partial class HorsieBindings 9 | { 10 | public static readonly bool HasBindings; 11 | private static readonly nint Handle; 12 | 13 | private const string DEST_NAME = "HorsieBindings"; 14 | private static readonly bool IsWin = IsOSPlatform(OSPlatform.Windows); 15 | 16 | static HorsieBindings() 17 | { 18 | HasBindings = false; 19 | if (!IsOSPlatform(OSPlatform.Windows) && !IsOSPlatform(OSPlatform.Linux)) 20 | return; 21 | 22 | string fExt = IsWin ? "dll" : "so"; 23 | string fileName = $"{DEST_NAME}.{fExt}"; 24 | string resName = $"Lizard.{fileName}"; 25 | 26 | string absPath = Path.Combine(Path.GetDirectoryName(AppContext.BaseDirectory), fileName); 27 | 28 | try 29 | { 30 | if (!File.Exists(absPath) && !ExtractEmbeddedLibrary(resName, fileName)) 31 | { 32 | return; 33 | } 34 | 35 | Handle = NativeLibrary.Load(absPath); 36 | } 37 | catch (Exception e) 38 | { 39 | Log("Failed loading Horsie bindings! :("); 40 | Log(e.Message); 41 | return; 42 | } 43 | 44 | HasBindings = true; 45 | Log("Loaded Horsie bindings!"); 46 | } 47 | 48 | private static bool ExtractEmbeddedLibrary(string resName, string fileName) 49 | { 50 | var asm = Assembly.GetExecutingAssembly(); 51 | Debug.WriteLine($"looking for {resName} in [{string.Join(", ", asm.GetManifestResourceNames())}]"); 52 | using Stream stream = asm.GetManifestResourceStream(resName); 53 | 54 | if (stream == null) 55 | { 56 | Log("Running without Horsie bindings"); 57 | return false; 58 | } 59 | 60 | string exePath = Path.GetDirectoryName(AppContext.BaseDirectory); 61 | string dllPath = Path.Combine(exePath, fileName); 62 | 63 | using FileStream fs = new FileStream(dllPath, FileMode.Create, FileAccess.Write); 64 | stream.CopyTo(fs); 65 | 66 | return true; 67 | } 68 | 69 | 70 | 71 | public static void DoSetupNNZ() 72 | { 73 | if (IsWin) 74 | HorsieSetupNNZWin(); 75 | else 76 | HorsieSetupNNZUnix(); 77 | } 78 | 79 | public static void DoGetEvaluation(short* us, short* them, sbyte* L1Weights, float* L1Biases, 80 | float* L2Weights, float* L2Biases, float* L3weights, float L3bias, ref int L3Output) 81 | { 82 | if (IsWin) 83 | HorsieGetEvaluationWin(us, them, L1Weights, L1Biases, L2Weights, L2Biases, L3weights, L3bias, ref L3Output); 84 | else 85 | HorsieGetEvaluationUnix(us, them, L1Weights, L1Biases, L2Weights, L2Biases, L3weights, L3bias, ref L3Output); 86 | } 87 | 88 | 89 | 90 | [LibraryImport("HorsieBindings.so", EntryPoint = "SetupNNZ")] 91 | public static partial void HorsieSetupNNZUnix(); 92 | 93 | [LibraryImport("HorsieBindings.so", EntryPoint = "EvaluateBound")] 94 | public static partial void HorsieGetEvaluationUnix(short* us, short* them, sbyte* L1Weights, float* L1Biases, 95 | float* L2Weights, float* L2Biases, float* L3weights, float L3bias, ref int L3Output); 96 | 97 | 98 | [LibraryImport("HorsieBindings.dll", EntryPoint = "SetupNNZ")] 99 | public static partial void HorsieSetupNNZWin(); 100 | 101 | [LibraryImport("HorsieBindings.dll", EntryPoint = "EvaluateBound")] 102 | public static partial void HorsieGetEvaluationWin(short* us, short* them, sbyte* L1Weights, float* L1Biases, 103 | float* L2Weights, float* L2Biases, float* L3weights, float L3bias, ref int L3Output); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Logic/Transposition/Cuckoo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Lizard.Logic.Transposition 4 | { 5 | public static unsafe class Cuckoo 6 | { 7 | private static readonly Move* Moves; 8 | private static readonly ulong* Keys; 9 | 10 | private const int TableSize = 8192; 11 | 12 | private static int Hash1(ulong key) => (int)(key & 0x1FFF); 13 | private static int Hash2(ulong key) => (int)((key >> 16) & 0x1FFF); 14 | 15 | static Cuckoo() 16 | { 17 | Moves = AlignedAllocZeroed(TableSize); 18 | Keys = AlignedAllocZeroed(TableSize); 19 | new Span(Moves, TableSize).Fill(Move.Null); 20 | 21 | for (int pc = White; pc <= Black; pc++) 22 | { 23 | for (int pt = Knight; pt <= King; pt++) 24 | { 25 | for (int s1 = A1; s1 <= H8; s1++) 26 | { 27 | for (int s2 = s1 + 1; s2 <= H8; s2++) 28 | { 29 | if ((AttackMask(s1, pc, pt) & SquareBB[s2]) != 0) 30 | { 31 | Move m = new Move(s1, s2); 32 | ulong key = Zobrist.HashForPiece(pc, pt, s1) ^ Zobrist.HashForPiece(pc, pt, s2) ^ Zobrist.ColorHash; 33 | 34 | int iter = Hash1(key); 35 | while (true) 36 | { 37 | (Keys[iter], key) = (key, Keys[iter]); 38 | (Moves[iter], m) = (m, Moves[iter]); 39 | 40 | if (m == Move.Null) 41 | break; 42 | 43 | iter = iter == Hash1(key) ? Hash2(key) : Hash1(key); 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | ulong AttackMask(int idx, int pc, int pt) 52 | { 53 | return pt switch 54 | { 55 | Knight => KnightMasks[idx], 56 | Bishop => GetBishopMoves(0, idx), 57 | Rook => GetRookMoves(0, idx), 58 | Queen => GetBishopMoves(0, idx) | GetRookMoves(0, idx), 59 | _ => NeighborsMask[idx], 60 | }; 61 | } 62 | } 63 | 64 | public static bool HasCycle(Position pos, int ply) 65 | { 66 | ref Bitboard bb = ref pos.bb; 67 | var occ = bb.Occupancy; 68 | StateInfo* st = pos.State; 69 | 70 | int dist = Math.Min(st->HalfmoveClock, st->PliesFromNull); 71 | if (dist < 3) 72 | return false; 73 | 74 | ulong HashFromStack(int i) => pos.Hashes[^i]; 75 | 76 | int slot; 77 | var other = st->Hash ^ HashFromStack(1) ^ Zobrist.ColorHash; 78 | for (int i = 3; i <= dist; i += 2) 79 | { 80 | var currKey = HashFromStack(i); 81 | other ^= currKey ^ HashFromStack(i - 1) ^ Zobrist.ColorHash; 82 | 83 | if (other != 0) 84 | continue; 85 | 86 | var diff = st->Hash ^ currKey; 87 | 88 | if (diff != Keys[(slot = Hash1(diff))] && 89 | diff != Keys[(slot = Hash2(diff))]) 90 | continue; 91 | 92 | Move m = Moves[slot]; 93 | int moveFrom = m.From; 94 | int moveTo = m.To; 95 | 96 | if ((occ & BetweenBB[moveFrom][moveTo]) != 0) 97 | continue; 98 | 99 | if (ply >= i) 100 | return true; 101 | 102 | for (int j = i + 4; j <= dist; j += 2) 103 | { 104 | if (HashFromStack(j) == HashFromStack(i)) 105 | return true; 106 | } 107 | } 108 | 109 | return false; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | ifndef EXE 3 | EXE = Lizard 4 | endif 5 | 6 | ifndef OUT_PATH 7 | OUT_PATH = .\\ 8 | endif 9 | 10 | ifeq ($(OS),Windows_NT) 11 | BINARY_SUFFIX = .exe 12 | PDB_SUFF = pdb 13 | DLL_SUFF = dll 14 | 15 | RENAME_CMD = -ren 16 | RM_FILE_CMD = del 17 | RM_FOLDER_CMD = rmdir /s /q 18 | else 19 | PDB_SUFF = dbg 20 | BINARY_SUFFIX = 21 | DLL_SUFF = so 22 | 23 | RENAME_CMD = mv 24 | RM_FILE_CMD = rm 25 | RM_FOLDER_CMD = rm -rf 26 | endif 27 | 28 | FULL_EXE_PATH = $(EXE)$(BINARY_SUFFIX) 29 | BINDINGS_FILE = HorsieBindings.$(DLL_SUFF) 30 | RM_PDB = -$(RM_FILE_CMD) $(EXE).$(PDB_SUFF) 31 | RM_BLD_FOLDER = -cd bin && $(RM_FOLDER_CMD) Release && cd .. 32 | RM_OBJ_FOLDER = -$(RM_FOLDER_CMD) obj 33 | 34 | INST_SET = native 35 | 36 | 37 | # Macos doesn't seem to like this parameter and the GenerateBundle task fails during building. 38 | OUT_DIR = -o ./ 39 | FIX_OUTPUT = 40 | ifneq ($(OS),Windows_NT) 41 | UNAME_S := $(shell uname -s) 42 | ifeq ($(UNAME_S),Darwin) 43 | OUT_DIR = 44 | FIX_OUTPUT = mv ./bin/Release/osx-arm64/publish/Lizard ./Lizard 45 | endif 46 | UNAME_P := $(shell uname -p) 47 | ifneq ($(filter arm%,$(UNAME_P)),) 48 | OUT_DIR = 49 | endif 50 | endif 51 | 52 | 53 | ifdef EVALFILE 54 | EVALFILE_STR = -p:EVALFILE=$(EVALFILE) 55 | endif 56 | 57 | 58 | # self-contained .NET Core won't need to be installed to run the binary 59 | # -v quiet Silences CS#### warnings during building (e.g. "CS0162: Unreachable code detected") 60 | # -p:WarningLevel=0 Silences CS#### warnings during building 61 | # $(OUT_DIR) Should be "-o ./", which outputs the binary in the current directory 62 | # -c Release Builds using the Release configuration in Lizard.csproj 63 | # -p:AssemblyName=$(EXE) Renames the binary to whatever $(EXE) is. 64 | # -p:EVALFILE=$(EVALFILE) Path to a network to be bundled. 65 | BUILD_OPTS := --self-contained -v quiet -p:WarningLevel=0 $(OUT_DIR) -c Release -p:AssemblyName=$(EXE) $(EVALFILE_STR) -p:BINDINGS=$(BINDINGS_FILE) 66 | 67 | 68 | # -p:PublishAOT=true Actually enables AOT 69 | # -p:PublishSingleFile=false AOT is incompatible with single file publishing 70 | # -p:IS_AOT=true Sets a variable during runtime signalling AOT is enabled, same to how EVALFILE works. 71 | # -p:IlcInstructionSet=$(INST_SET) Instruction set to use, should be "native" if you are only running the binary on your cpu. 72 | AOT_OPTS = -p:PublishAOT=true -p:PublishSingleFile=false -p:IS_AOT=true -p:IlcInstructionSet=$(INST_SET) 73 | 74 | 75 | .PHONY: release 76 | .DEFAULT_GOAL := release 77 | 78 | 79 | $(BINDINGS_FILE): 80 | -g++ -std=c++20 -O3 -funroll-loops -march=x86-64-v3 -fPIC -shared -o $(BINDINGS_FILE) ./Bindings/simd.cpp 81 | bindings: $(BINDINGS_FILE) 82 | 83 | 84 | # Try building the non-AOT version first, and then try to build the AOT version if possible. 85 | # This recipe should always work, but AOT requires some additional setup so that recipe may fail. 86 | release: $(BINDINGS_FILE) 87 | dotnet publish . $(BUILD_OPTS) 88 | $(FIX_OUTPUT) 89 | 90 | # This will/might only succeed if you have the right toolchain 91 | aot: 92 | -dotnet publish . $(BUILD_OPTS) $(AOT_OPTS) 93 | 94 | 512: $(BINDINGS_FILE) 95 | dotnet publish . $(BUILD_OPTS) -p:DefineConstants="AVX512" 96 | $(FIX_OUTPUT) 97 | 98 | aot_512: 99 | -dotnet publish . $(BUILD_OPTS) $(AOT_OPTS) -p:DefineConstants="AVX512" 100 | 101 | all: 102 | $(MAKE) aot INST_SET=x86-x64 103 | $(RENAME_CMD) $(FULL_EXE_PATH) $(EXE)-aot-v1$(BINARY_SUFFIX) 104 | $(MAKE) aot INST_SET=x86-x64-v2 105 | $(RENAME_CMD) $(FULL_EXE_PATH) $(EXE)-aot-v2$(BINARY_SUFFIX) 106 | $(MAKE) aot INST_SET=x86-x64-v3 107 | $(RENAME_CMD) $(FULL_EXE_PATH) $(EXE)-aot-v3$(BINARY_SUFFIX) 108 | $(MAKE) aot_512 INST_SET=x86-x64-v4 109 | $(RENAME_CMD) $(FULL_EXE_PATH) $(EXE)-aot-v4$(BINARY_SUFFIX) 110 | $(MAKE) 512 111 | $(RENAME_CMD) $(FULL_EXE_PATH) $(EXE)-512$(BINARY_SUFFIX) 112 | $(MAKE) release 113 | $(RENAME_CMD) $(FULL_EXE_PATH) $(EXE)$(BINARY_SUFFIX) 114 | 115 | clean: 116 | $(RM_OBJ_FOLDER) 117 | $(RM_BLD_FOLDER) 118 | $(RM_PDB) -------------------------------------------------------------------------------- /Logic/UCI/UCIOption.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Lizard.Logic.UCI 4 | { 5 | public class UCIOption 6 | { 7 | /// 8 | /// +/- 45% 9 | /// 10 | private const double AutoMinMaxMultiplier = 0.45; 11 | 12 | 13 | /// 14 | /// The name of the option. 15 | /// 16 | public string Name; 17 | 18 | /// 19 | /// Either "spin" for numerical values, or "check" for booleans. 20 | /// 21 | public string Type; 22 | 23 | /// 24 | /// The default value of the field, which should always be kept in sync with the FieldHandle's value. 25 | /// 26 | /// TODO: this can be a setter. 27 | /// 28 | public string DefaultValue; 29 | 30 | /// 31 | /// The minimum value that this option can be set to. 32 | /// Requests to set it lower than this will be ignored. 33 | /// 34 | public int MinValue; 35 | 36 | /// 37 | /// The maximum value that this option can be set to. 38 | /// Requests to set it higher than this will be ignored. 39 | /// 40 | public int MaxValue; 41 | 42 | /// 43 | /// The actual field that this option represents, which is in the class. 44 | /// 45 | public FieldInfo FieldHandle; 46 | 47 | public UCIOption(string name, string type, string defaultValue, FieldInfo fieldHandle) 48 | { 49 | Name = name; 50 | Type = type; 51 | DefaultValue = defaultValue; 52 | FieldHandle = fieldHandle; 53 | } 54 | 55 | public void SetMinMax(int min, int max) 56 | { 57 | MinValue = min; 58 | MaxValue = max; 59 | } 60 | 61 | /// 62 | /// Sets the and to be a percentage of the , 63 | /// which is based on ( Currently ). 64 | /// 65 | public void AutoMinMax() 66 | { 67 | if (FieldHandle.FieldType != typeof(int)) 68 | { 69 | Log($"AutoMinMax was called on {FieldHandle.Name}, which is a {FieldHandle.FieldType}, not an int!"); 70 | return; 71 | } 72 | 73 | int v = int.Parse(DefaultValue); 74 | MinValue = (int)double.Round(v * (1 - AutoMinMaxMultiplier)); 75 | MaxValue = (int)double.Round(v * (1 + AutoMinMaxMultiplier)); 76 | } 77 | 78 | 79 | /// 80 | /// Sets the value of the that this option represents to 81 | /// 82 | public void RefreshBackingField() 83 | { 84 | if (FieldHandle == null) 85 | { 86 | return; 87 | } 88 | 89 | if (FieldHandle.FieldType == typeof(int)) 90 | { 91 | FieldHandle.SetValue(null, int.Parse(DefaultValue)); 92 | } 93 | else if (FieldHandle.FieldType == typeof(bool)) 94 | { 95 | FieldHandle.SetValue(null, bool.Parse(DefaultValue)); 96 | } 97 | } 98 | 99 | 100 | /// 101 | /// Returns a string in the formatting expected by OpenBench's SPSA tuner. 102 | /// 103 | /// This looks like "name, int, default, min, max, step-size end, learning rate" 104 | /// 105 | public string GetSPSAFormat() 106 | { 107 | const double minStepSize = 0.01; 108 | double stepSize = Math.Max(minStepSize, (MaxValue - MinValue) / 20.0); 109 | 110 | const double normalLearningRate = 0.002; 111 | double learningRate = double.Round(Math.Max(normalLearningRate, normalLearningRate * (0.50 / stepSize)), 4); 112 | 113 | // name, int, default, min, max, step-size end, learning rate 114 | return $"{FieldHandle.Name}, int, {DefaultValue}, {MinValue}, {MaxValue}, {stepSize}, {learningRate}"; 115 | } 116 | 117 | public override string ToString() 118 | { 119 | return "option name " + Name + " type " + Type + " default " + DefaultValue + (FieldHandle.FieldType == typeof(int) ? (" min " + MinValue + " max " + MaxValue) : string.Empty); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Logic/Search/SearchOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Lizard.Logic.Search 2 | { 3 | public static class SearchOptions 4 | { 5 | /// 6 | /// This number of threads will be used during searches. 7 | /// 8 | /// For values above 1, the engine will create extra threads to increase the amount of nodes that can be looked at. 9 | /// Do note that a decent amount of the nodes that are looked at by secondary threads won't influence anything, 10 | /// but higher numbers of threads tends to correlate with better playing strength. 11 | /// 12 | public static int Threads = 1; 13 | 14 | 15 | /// 16 | /// Calls to search will display this amount of principal variation lines. 17 | /// 18 | /// Ordinarily engines only search for the 1 "best" move, but with MultiPV values 19 | /// above 1 this will also display the 2nd best move, the 3rd best, etc. 20 | /// 21 | public static int MultiPV = 1; 22 | 23 | 24 | /// 25 | /// The size in megabytes of the transposition table. 26 | /// 27 | public static int Hash = 32; 28 | 29 | 30 | public static bool UCI_Chess960 = false; 31 | public static bool UCI_ShowWDL = false; 32 | public static bool UCI_PrettyPrint = true; 33 | 34 | 35 | public const int CorrectionScale = 1024; 36 | public const int CorrectionGrain = 256; 37 | public const short CorrectionMax = CorrectionGrain * 64; 38 | 39 | 40 | public const bool ShallowPruning = true; 41 | public const bool UseSingularExtensions = true; 42 | public const bool UseNMP = true; 43 | public const bool UseRazoring = true; 44 | public const bool UseRFP = true; 45 | public const bool UseProbCut = true; 46 | 47 | 48 | public static int QuietOrderMin = 100; 49 | public static int QuietOrderMax = 200; 50 | public static int QuietOrderMult = 10; 51 | 52 | public static int SEMinDepth = 5; 53 | public static int SENumerator = 11; 54 | public static int SEDoubleMargin = 24; 55 | public static int SETripleMargin = 97; 56 | public static int SETripleCapSub = 73; 57 | public static int SEDepthAdj = -1; 58 | 59 | public static int NMPMinDepth = 6; 60 | public static int NMPBaseRed = 4; 61 | public static int NMPDepthDiv = 4; 62 | public static int NMPEvalDiv = 165; 63 | public static int NMPEvalMin = 2; 64 | 65 | public static int RazoringMaxDepth = 4; 66 | public static int RazoringMult = 280; 67 | 68 | public static int RFPMaxDepth = 6; 69 | public static int RFPMargin = 46; 70 | 71 | public static int ProbcutBeta = 257; 72 | public static int ProbcutBetaImp = 93; 73 | 74 | public static int NMFutileBase = 473; 75 | public static int NMFutilePVCoeff = 1062; 76 | public static int NMFutileImpCoeff = 1017; 77 | public static int NMFutileHistCoeff = 1051; 78 | public static int NMFutMarginB = 180; 79 | public static int NMFutMarginM = 81; 80 | public static int NMFutMarginDiv = 139; 81 | public static int ShallowSEEMargin = 80; 82 | public static int ShallowMaxDepth = 9; 83 | 84 | public static int LMRQuietDiv = 12948; 85 | public static int LMRCaptureDiv = 9424; 86 | 87 | public static int DeeperMargin = 50; 88 | 89 | public static int QSFutileMargin = 187; 90 | public static int QSSeeMargin = 78; 91 | 92 | public static int OrderingCheckBonus = 9315; 93 | public static int OrderingVictimMult = 11; 94 | 95 | public static int IIRMinDepth = 3; 96 | public static int AspWindow = 12; 97 | 98 | public static int StatBonusMult = 182; 99 | public static int StatBonusSub = 82; 100 | public static int StatBonusMax = 1713; 101 | 102 | public static int StatMalusMult = 654; 103 | public static int StatMalusSub = 102; 104 | public static int StatMalusMax = 1441; 105 | 106 | public static int SEEValuePawn = 105; 107 | public static int SEEValueKnight = 300; 108 | public static int SEEValueBishop = 315; 109 | public static int SEEValueRook = 535; 110 | public static int SEEValueQueen = 970; 111 | 112 | public static int ValuePawn = 171; 113 | public static int ValueKnight = 794; 114 | public static int ValueBishop = 943; 115 | public static int ValueRook = 1620; 116 | public static int ValueQueen = 2994; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Logic/Search/TimeManager.cs: -------------------------------------------------------------------------------- 1 | namespace Lizard.Logic.Search 2 | { 3 | public class TimeManager 4 | { 5 | 6 | /// 7 | /// Add this amount of milliseconds to the total search time when checking if the 8 | /// search should stop, in case the move overhead is very low and the UCI expects 9 | /// the search to stop very quickly after our time expires. 10 | /// 11 | public const int TimerBuffer = 50; 12 | 13 | /// 14 | /// If we got a "movetime" command, we use a smaller buffer to bring the time we actually search 15 | /// much closer to the requested time. 16 | /// 17 | private const int MoveTimeBuffer = 5; 18 | 19 | /// 20 | /// The minimum amount of time to search, regardless of the other limitations of the search. 21 | /// This only applies to the amount of time that we were told to search for (i.e. "movetime 100"). 22 | /// If we receive a "stop" command from the UCI, this does no apply and we stop as soon as possible. 23 | /// 24 | public const int MinSearchTime = 200; 25 | 26 | /// 27 | /// Set to true if the go command has the "movetime" parameter. 28 | /// 29 | public bool HasMoveTime = false; 30 | 31 | /// 32 | /// The time in milliseconds that we were explicitly told to search for. 33 | /// 34 | public int MoveTime = 0; 35 | 36 | /// 37 | /// The time in milliseconds that the search should stop at. 38 | /// 39 | public int MaxSearchTime; 40 | 41 | 42 | public double SoftTimeLimit = -1; 43 | public bool HasSoftTime => SoftTimeLimit > 0; 44 | 45 | 46 | public int MovesToGo = DefaultMovesToGo; 47 | 48 | /// 49 | /// Set to the value of winc/binc if one was provided during a UCI "go" command. 50 | /// Only used 51 | /// 52 | public int PlayerIncrement; 53 | 54 | /// 55 | /// Set to the value of wtime/btime if one was provided during a UCI "go" command. 56 | /// If the search time gets too close to this, it will stop prematurely so we don't lose on time. 57 | /// 58 | public int PlayerTime; 59 | 60 | // Side note: Having this be a Stopwatch rather than keeping track of time via DateTime.Now is annoying, 61 | // but DateTime.Now can have unacceptably poor resolution (on some machines, like windows 7/8) of 62 | // +- 15ms, which can have a big impact for short time controls and especially "movetime" uci commands. 63 | // https://learn.microsoft.com/en-us/dotnet/api/system.datetime?view=net-8.0&redirectedfrom=MSDN#datetime-resolution 64 | private static Stopwatch TotalSearchTime = new Stopwatch(); 65 | 66 | 67 | public TimeManager() 68 | { 69 | PlayerIncrement = 0; 70 | PlayerTime = SearchConstants.MaxSearchTime; 71 | } 72 | 73 | public void StartTimer() => TotalSearchTime.Start(); 74 | 75 | public void StopTimer() => TotalSearchTime.Stop(); 76 | 77 | public void ResetTimer() => TotalSearchTime.Reset(); 78 | 79 | public void RestartTimer() => TotalSearchTime.Restart(); 80 | 81 | /// 82 | /// Returns the current search time in milliseconds. 83 | /// 84 | public double GetSearchTime() => TotalSearchTime.Elapsed.TotalMilliseconds; 85 | 86 | /// 87 | /// Returns true if we have searched for our maximum allotted time 88 | /// 89 | public bool CheckUp() 90 | { 91 | double currentTime = GetSearchTime(); 92 | 93 | if (currentTime > (MaxSearchTime - (HasMoveTime ? MoveTimeBuffer : TimerBuffer))) 94 | { 95 | // Stop if we are close to going over the max time 96 | return true; 97 | } 98 | 99 | return false; 100 | } 101 | 102 | 103 | /// 104 | /// Sets the maximum search time for the player , given the number of moves made so far. 105 | /// 106 | /// This currently prioritizes early game moves since each search is given a percentage of the player's remaining time, 107 | /// which works well since there are more pieces and therefore more moves that need to be considered in the early/midgame. 108 | /// 109 | public void MakeMoveTime() 110 | { 111 | int newSearchTime = PlayerIncrement + Math.Max(PlayerTime / 2, PlayerTime / MovesToGo); 112 | 113 | newSearchTime = Math.Min(newSearchTime, PlayerTime); 114 | 115 | // Values from Clarity (who took them from Stormphrax), then slightly adjusted 116 | SoftTimeLimit = 0.65 * ((PlayerTime / MovesToGo) + (PlayerIncrement * 3 / 4)); 117 | 118 | MaxSearchTime = newSearchTime; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Logic/Transposition/TTEntry.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | 4 | using static Lizard.Logic.Transposition.TranspositionTable; 5 | 6 | 7 | namespace Lizard.Logic.Transposition 8 | { 9 | /// 10 | /// Represents an entry within a transposition table.

11 | /// Entries are added when a beta cutoff occurs or new best move is found during a search. 12 | /// 13 | /// Setting Pack=1/2 causes the struct to NOT pad itself with an extra 2 bytes, so its size would increase from 10 -> 12. 14 | /// Each TTCluster contains 3 TTEntry, and TTClusters are meant to align on 32 byte boundaries, so we need this to be 10 bytes max. 15 | /// 16 | /// The replacement strategy and depth logic are inspired by Stockfish and Berserk. 17 | ///
18 | [StructLayout(LayoutKind.Explicit, Pack = 2, Size = 10)] 19 | public struct TTEntry 20 | { 21 | public const int DepthOffset = -7; 22 | public const int DepthNone = -6; 23 | 24 | [FieldOffset(0)] private short _Score; // 2 = 16 bits 25 | [FieldOffset(2)] private short _StatEval; // 2 = 16 bits 26 | [FieldOffset(4)] private Move _Move; // 2 = 16 bits 27 | [FieldOffset(6)] private ushort _Key; // 2 = 16 bits 28 | [FieldOffset(8)] private byte _AgePVType; // 1 = 8 bits (5 bits for age, 1 for isPV, 2 for type) 29 | [FieldOffset(9)] private byte _Depth; // 1 = 8 bits 30 | 31 | 32 | public readonly short Score => _Score; 33 | public readonly short StatEval => _StatEval; 34 | public readonly Move BestMove => _Move; 35 | public readonly ushort Key => _Key; 36 | public readonly int Depth => (_Depth + DepthOffset); 37 | public readonly int RawDepth => (_Depth); 38 | 39 | public readonly int Age => _AgePVType & TT_AGE_MASK; 40 | public readonly bool PV => (_AgePVType & TT_PV_MASK) != 0; 41 | public readonly TTNodeType NodeType => (TTNodeType)(_AgePVType & TT_BOUND_MASK); 42 | public readonly int Bound => _AgePVType & TT_BOUND_MASK; 43 | 44 | [MethodImpl(Inline)] 45 | public readonly sbyte RelAge(byte age) => (sbyte)((TT_AGE_CYCLE + age - _AgePVType) & TT_AGE_MASK); 46 | public readonly bool IsEmpty => _Depth == 0; 47 | 48 | public void Update(ulong key, short score, TTNodeType nodeType, int depth, Move move, short statEval, byte age, bool isPV = false) 49 | { 50 | var k = (ushort)key; 51 | if (move != Move.Null || k != Key) 52 | { 53 | _Move = move; 54 | } 55 | 56 | if (nodeType == TTNodeType.Exact 57 | || k != Key 58 | || age != Age 59 | || depth + (isPV ? 2 : 0) > Depth - 4) 60 | { 61 | _Key = k; 62 | _Score = score; 63 | _StatEval = statEval; 64 | _Depth = (byte)(depth - DepthOffset); 65 | _AgePVType = (byte)(age | ((isPV ? 1u : 0u) << 2) | (uint)nodeType); 66 | 67 | Assert(score == ScoreNone || (score <= ScoreMate && score >= -ScoreMate), 68 | $"WARN the score {score} is outside of bounds for normal TT entries!"); 69 | } 70 | } 71 | 72 | 73 | /// 74 | /// Converts the retrieved from a TT hit to a usable score from the root position. 75 | /// 76 | public static short MakeNormalScore(short ttScore, int ply) 77 | { 78 | return ttScore switch 79 | { 80 | ScoreNone => ttScore, 81 | >= ScoreTTWin => (short)(ttScore - ply), 82 | <= ScoreTTLoss => (short)(ttScore + ply), 83 | _ => ttScore 84 | }; 85 | } 86 | 87 | /// 88 | /// Converts the to one suitable for the TT. 89 | /// 90 | /// If is a mate score, it would ordinarily be saved as a "mate in X" in relation to the current search ply of X. 91 | /// This is not correct since a mate in 1 could be delayed by a few moves to make it a mate in mate in 2/3/... instead, so what we really 92 | /// care about is the number of plies from the current position and not the number of plies when the score was calculated. 93 | /// 94 | public static short MakeTTScore(short score, int ply) 95 | { 96 | return score switch 97 | { 98 | ScoreNone => score, 99 | >= ScoreTTWin => (short)(score + ply), 100 | <= ScoreTTLoss => (short)(score - ply), 101 | _ => score 102 | }; 103 | } 104 | 105 | public override string ToString() 106 | { 107 | return NodeType.ToString() + ", Depth " + Depth + ", Age: " + Age + ", BestMove " + BestMove.ToString() + ", Score " + Score + ", StatEval: " + StatEval + ", Key " + Key; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Logic/Util/FishBench.cs: -------------------------------------------------------------------------------- 1 | namespace Lizard.Logic.Util 2 | { 3 | public static class FishBench 4 | { 5 | private static Dictionary FENDict = new Dictionary() 6 | { 7 | { "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", new ulong[] {197281, 4865609} }, 8 | { "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 10", new ulong[] {4085603, 193690690} }, 9 | { "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 11", new ulong[] {43238, 674624} }, 10 | { "4rrk1/pp1n3p/3q2pQ/2p1pb2/2PP4/2P3N1/P2B2PP/4RRK1 b - - 7 19", new ulong[] {2038080, 72228466} }, 11 | { "rq3rk1/ppp2ppp/1bnpN3/3N2B1/4P3/7P/PPPQ1PP1/2KR3R b - - 0 14", new ulong[] {1651350, 45187461} }, 12 | { "r1bq1r1k/1pp1n1pp/1p1p4/4p2Q/4PpP1/1BNP4/PPP2P1P/3R1RK1 b - g3 0 14", new ulong[] {1267861, 40266111} }, 13 | { "r3r1k1/2p2ppp/p1p1bn2/8/1q2P3/2NPQN2/PPP3PP/R4RK1 b - - 2 15", new ulong[] {3977950, 172436031} }, 14 | { "r1bbk1nr/pp3p1p/2n5/1N4p1/2Np1B2/8/PPP2PPP/2KR1B1R w kq - 0 13", new ulong[] {1313494, 50688363} }, 15 | { "r1bq1rk1/ppp1nppp/4n3/3p3Q/3P4/1BP1B3/PP1N2PP/R4RK1 w - - 1 16", new ulong[] {1577355, 72185472} }, 16 | { "4r1k1/r1q2ppp/ppp2n2/4P3/5Rb1/1N1BQ3/PPP3PP/R5K1 w - - 1 17", new ulong[] {4189865, 210468700} }, 17 | { "2rqkb1r/ppp2p2/2npb1p1/1N1Nn2p/2P1PP2/8/PP2B1PP/R1BQK2R b KQ - 0 11", new ulong[] {2001581, 68841702} }, 18 | { "r1bq1r1k/b1p1npp1/p2p3p/1p6/3PP3/1B2NN2/PP3PPP/R2Q1RK1 w - - 1 16", new ulong[] {1342987, 51439641} }, 19 | { "3r1rk1/p5pp/bpp1pp2/8/q1PP1P2/b3P3/P2NQRPP/1R2B1K1 b - - 6 22", new ulong[] {1307975, 49893295} }, 20 | { "r1q2rk1/2p1bppp/2Pp4/p6b/Q1PNp3/4B3/PP1R1PPP/2K4R w - - 2 18", new ulong[] {1415967, 55479889} }, 21 | { "4k2r/1pb2ppp/1p2p3/1R1p4/3P4/2r1PN2/P4PPP/1R4K1 b - - 3 22", new ulong[] {629156, 18452493} }, 22 | { "3q2k1/pb3p1p/4pbp1/2r5/PpN2N2/1P2P2P/5PP1/Q2R2K1 b - - 4 26", new ulong[] {3982247, 177035052} }, 23 | { "6k1/6p1/6Pp/ppp5/3pn2P/1P3K2/1PP2P2/3N4 b - - 0 1", new ulong[] {22537, 289580} }, 24 | { "3b4/5kp1/1p1p1p1p/pP1PpP1P/P1P1P3/3KN3/8/8 w - - 0 1", new ulong[] {8212, 102373} }, 25 | { "2K5/p7/7P/5pR1/8/5k2/r7/8 w - - 4 3", new ulong[] {79278, 1348405} }, 26 | { "8/6pk/1p6/8/PP3p1p/5P2/4KP1q/3Q4 w - - 0 1", new ulong[] {70042, 1345987} }, 27 | { "7k/3p2pp/4q3/8/4Q3/5Kp1/P6b/8 w - - 0 1", new ulong[] {364175, 6947406} }, 28 | { "8/2p5/8/2kPKp1p/2p4P/2P5/3P4/8 w - - 0 1", new ulong[] {2021, 18397} }, 29 | { "8/1p3pp1/7p/5P1P/2k3P1/8/2K2P2/8 w - - 0 1", new ulong[] {10807, 100735} }, 30 | { "8/pp2r1k1/2p1p3/3pP2p/1P1P1P1P/P5KR/8/8 w - - 0 1", new ulong[] {30953, 487838} }, 31 | { "8/3p4/p1bk3p/Pp6/1Kp1PpPp/2P2P1P/2P5/5B2 b - - 0 1", new ulong[] {6650, 84037} }, 32 | { "5k2/7R/4P2p/5K2/p1r2P1p/8/8/8 b - - 0 1", new ulong[] {41391, 589437} }, 33 | { "6k1/6p1/P6p/r1N5/5p2/7P/1b3PP1/4R1K1 w - - 0 1", new ulong[] {340174, 8065991} }, 34 | { "1r3k2/4q3/2Pp3b/3Bp3/2Q2p2/1p1P2P1/1P2KP2/3N4 w - - 0 1", new ulong[] {654142, 19229707} }, 35 | { "6k1/4pp1p/3p2p1/P1pPb3/R7/1r2P1PP/3B1P2/6K1 w - - 0 1", new ulong[] {425923, 8984804} }, 36 | { "8/3p3B/5p2/5P2/p7/PP5b/k7/6K1 w - - 0 1", new ulong[] {7812, 73669} }, 37 | { "5rk1/q6p/2p3bR/1pPp1rP1/1P1Pp3/P3B1Q1/1K3P2/R7 w - - 93 90", new ulong[] {1469918, 52898139} }, 38 | { "4rrk1/1p1nq3/p7/2p1P1pp/3P2bp/3Q1Bn1/PPPB4/1K2R1NR w - - 40 21", new ulong[] {3311502, 150850085} }, 39 | { "r3k2r/3nnpbp/q2pp1p1/p7/Pp1PPPP1/4BNN1/1P5P/R2Q1RK1 w kq - 0 16", new ulong[] {2428006, 90133805} }, 40 | { "3Qb1k1/1r2ppb1/pN1n2q1/Pp1Pp1Pr/4P2p/4BP2/4B1R1/1R5K b - - 11 40", new ulong[] {1923471, 61309107} }, 41 | { "4k3/3q1r2/1N2r1b1/3ppN2/2nPP3/1B1R2n1/2R1Q3/3K4 w - - 5 1", new ulong[] {2529287, 101660587} }, 42 | }; 43 | 44 | /// 45 | /// Runs a perft command on each of the positions in BenchFENs, and verifies that the number of nodes 46 | /// returned by our perft command is the same as the number that Stockfish 14 reported. 47 | /// 48 | public static bool Go(int depth = 4) 49 | { 50 | if (depth != 4 && depth != 5) 51 | { 52 | Log("FishBench 'go' commands must have a depth of 4 or 5!"); 53 | return false; 54 | } 55 | 56 | Position pos = new Position(InitialFEN, false); 57 | 58 | bool nodesCorrect = true; 59 | ulong total = 0; 60 | 61 | Stopwatch sw = Stopwatch.StartNew(); 62 | foreach (var item in FENDict) 63 | { 64 | string fen = item.Key; 65 | ulong correctNodes = item.Value[depth - 4]; 66 | 67 | pos.LoadFromFEN(fen); 68 | ulong ourNodes = pos.Perft(depth); 69 | if (ourNodes != correctNodes) 70 | { 71 | Log($"[{fen}]: Expected {correctNodes} nodes but got {ourNodes} nodes instead!"); 72 | nodesCorrect = false; 73 | } 74 | 75 | total += ourNodes; 76 | } 77 | 78 | Log($"\r\nNodes searched: {total} in {sw.Elapsed.TotalSeconds} s ({(int)(total / sw.Elapsed.TotalSeconds):N0} nps)" + "\r\n"); 79 | return nodesCorrect; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Logic/Data/Enums.cs: -------------------------------------------------------------------------------- 1 | namespace Lizard.Logic.Data 2 | { 3 | public static class Color 4 | { 5 | public const int White = 0; 6 | public const int Black = 1; 7 | 8 | public const int ColorNB = 2; 9 | } 10 | 11 | public static class Piece 12 | { 13 | public const int Pawn = 0; 14 | public const int Knight = 1; 15 | public const int Bishop = 2; 16 | public const int Rook = 3; 17 | public const int Queen = 4; 18 | public const int King = 5; 19 | 20 | public const int None = 6; 21 | public const int PieceNB = 6; 22 | } 23 | 24 | [Flags] 25 | public enum CastlingStatus 26 | { 27 | None = 0, 28 | WK = 1, 29 | WQ = 2, 30 | BK = 4, 31 | BQ = 8, 32 | 33 | White = WK | WQ, 34 | Black = BK | BQ, 35 | 36 | Kingside = WK | BK, 37 | Queenside = WQ | BQ, 38 | 39 | All = WK | WQ | BK | BQ, 40 | } 41 | 42 | public enum TTNodeType 43 | { 44 | Invalid, 45 | /// 46 | /// Upper Bound 47 | /// 48 | Beta, 49 | /// 50 | /// Lower bound 51 | /// 52 | Alpha, 53 | Exact = Beta | Alpha 54 | }; 55 | 56 | public static class Bound 57 | { 58 | public const int BoundNone = (int)TTNodeType.Invalid; 59 | public const int BoundUpper = (int)TTNodeType.Beta; 60 | public const int BoundLower = (int)TTNodeType.Alpha; 61 | public const int BoundExact = (int)TTNodeType.Exact; 62 | } 63 | 64 | public interface SearchNodeType { } 65 | public struct PVNode : SearchNodeType { } 66 | public struct NonPVNode : SearchNodeType { } 67 | public struct RootNode : SearchNodeType { } 68 | 69 | 70 | public interface MoveGenerationType { } 71 | public struct GenNoisy : MoveGenerationType { } 72 | public struct GenEvasions : MoveGenerationType { } 73 | public struct GenNonEvasions : MoveGenerationType { } 74 | 75 | 76 | public enum NetworkArchitecture 77 | { 78 | Bucketed768 79 | } 80 | 81 | 82 | public static class Files 83 | { 84 | public const int A = 0; 85 | public const int B = 1; 86 | public const int C = 2; 87 | public const int D = 3; 88 | public const int E = 4; 89 | public const int F = 5; 90 | public const int G = 6; 91 | public const int H = 7; 92 | } 93 | 94 | public static class Direction 95 | { 96 | public const int NORTH = 8; 97 | public const int EAST = 1; 98 | public const int SOUTH = -NORTH; 99 | public const int WEST = -EAST; 100 | 101 | public const int NORTH_EAST = NORTH + EAST; 102 | public const int SOUTH_EAST = SOUTH + EAST; 103 | public const int SOUTH_WEST = SOUTH + WEST; 104 | public const int NORTH_WEST = NORTH + WEST; 105 | } 106 | 107 | public static class Squares 108 | { 109 | public const int A1 = 0; 110 | public const int B1 = 1; 111 | public const int C1 = 2; 112 | public const int D1 = 3; 113 | public const int E1 = 4; 114 | public const int F1 = 5; 115 | public const int G1 = 6; 116 | public const int H1 = 7; 117 | public const int A2 = 8; 118 | public const int B2 = 9; 119 | public const int C2 = 10; 120 | public const int D2 = 11; 121 | public const int E2 = 12; 122 | public const int F2 = 13; 123 | public const int G2 = 14; 124 | public const int H2 = 15; 125 | public const int A3 = 16; 126 | public const int B3 = 17; 127 | public const int C3 = 18; 128 | public const int D3 = 19; 129 | public const int E3 = 20; 130 | public const int F3 = 21; 131 | public const int G3 = 22; 132 | public const int H3 = 23; 133 | public const int A4 = 24; 134 | public const int B4 = 25; 135 | public const int C4 = 26; 136 | public const int D4 = 27; 137 | public const int E4 = 28; 138 | public const int F4 = 29; 139 | public const int G4 = 30; 140 | public const int H4 = 31; 141 | public const int A5 = 32; 142 | public const int B5 = 33; 143 | public const int C5 = 34; 144 | public const int D5 = 35; 145 | public const int E5 = 36; 146 | public const int F5 = 37; 147 | public const int G5 = 38; 148 | public const int H5 = 39; 149 | public const int A6 = 40; 150 | public const int B6 = 41; 151 | public const int C6 = 42; 152 | public const int D6 = 43; 153 | public const int E6 = 44; 154 | public const int F6 = 45; 155 | public const int G6 = 46; 156 | public const int H6 = 47; 157 | public const int A7 = 48; 158 | public const int B7 = 49; 159 | public const int C7 = 50; 160 | public const int D7 = 51; 161 | public const int E7 = 52; 162 | public const int F7 = 53; 163 | public const int G7 = 54; 164 | public const int H7 = 55; 165 | public const int A8 = 56; 166 | public const int B8 = 57; 167 | public const int C8 = 58; 168 | public const int D8 = 59; 169 | public const int E8 = 60; 170 | public const int F8 = 61; 171 | public const int G8 = 62; 172 | public const int H8 = 63; 173 | 174 | public const int SquareNB = 64; 175 | public const int EPNone = 0; 176 | } 177 | } -------------------------------------------------------------------------------- /Logic/Util/SearchBench.cs: -------------------------------------------------------------------------------- 1 | namespace Lizard.Logic.Util 2 | { 3 | public static class SearchBench 4 | { 5 | 6 | public static void Go(int depth = 12, bool openBench = false) 7 | { 8 | Position pos = new Position(InitialFEN, owner: GlobalSearchPool.MainThread); 9 | 10 | Stopwatch sw = Stopwatch.StartNew(); 11 | 12 | ulong totalNodes = 0; 13 | SearchInformation info = new SearchInformation(pos, depth); 14 | info.OnDepthFinish = null; 15 | info.OnSearchFinish = null; 16 | 17 | GlobalSearchPool.MainThread.WaitForThreadFinished(); 18 | GlobalSearchPool.TTable.Clear(); 19 | GlobalSearchPool.Clear(); 20 | 21 | foreach (string fen in BenchFENs) 22 | { 23 | pos.LoadFromFEN(fen); 24 | GlobalSearchPool.StartSearch(pos, ref info); 25 | 26 | GlobalSearchPool.MainThread.WaitForThreadFinished(); 27 | 28 | ulong thisNodeCount = GlobalSearchPool.GetNodeCount(); 29 | totalNodes += thisNodeCount; 30 | if (!openBench) 31 | { 32 | Log($"{fen,-76}\t{thisNodeCount}"); 33 | } 34 | 35 | GlobalSearchPool.TTable.Clear(); 36 | GlobalSearchPool.Clear(); 37 | } 38 | sw.Stop(); 39 | 40 | if (openBench) 41 | { 42 | Console.WriteLine($"info string {sw.Elapsed.TotalSeconds} seconds"); 43 | 44 | var time = (int)(totalNodes / sw.Elapsed.TotalSeconds); 45 | Console.WriteLine($"{totalNodes} nodes {string.Join("", time.ToString("N0").Where(char.IsDigit))} nps"); 46 | } 47 | else if (UCIClient.Active) 48 | { 49 | Console.WriteLine($"info string nodes {totalNodes} time {Math.Round(sw.Elapsed.TotalSeconds)} nps {(int)(totalNodes / sw.Elapsed.TotalSeconds):N0}"); 50 | } 51 | else 52 | { 53 | Log($"\r\nNodes searched: {totalNodes} in {sw.Elapsed.TotalSeconds} s ({(int)(totalNodes / sw.Elapsed.TotalSeconds):N0} nps)" + "\r\n"); 54 | } 55 | } 56 | 57 | private static string[] BenchFENs = new string[] 58 | { 59 | "r3k2r/2pb1ppp/2pp1q2/p7/1nP1B3/1P2P3/P2N1PPP/R2QK2R w KQkq a6 0 14", 60 | "4rrk1/2p1b1p1/p1p3q1/4p3/2P2n1p/1P1NR2P/PB3PP1/3R1QK1 b - - 2 24", 61 | "r3qbrk/6p1/2b2pPp/p3pP1Q/PpPpP2P/3P1B2/2PB3K/R5R1 w - - 16 42", 62 | "6k1/1R3p2/6p1/2Bp3p/3P2q1/P7/1P2rQ1K/5R2 b - - 4 44", 63 | "8/8/1p2k1p1/3p3p/1p1P1P1P/1P2PK2/8/8 w - - 3 54", 64 | "7r/2p3k1/1p1p1qp1/1P1Bp3/p1P2r1P/P7/4R3/Q4RK1 w - - 0 36", 65 | "r1bq1rk1/pp2b1pp/n1pp1n2/3P1p2/2P1p3/2N1P2N/PP2BPPP/R1BQ1RK1 b - - 2 10", 66 | "3r3k/2r4p/1p1b3q/p4P2/P2Pp3/1B2P3/3BQ1RP/6K1 w - - 3 87", 67 | "2r4r/1p4k1/1Pnp4/3Qb1pq/8/4BpPp/5P2/2RR1BK1 w - - 0 42", 68 | "4q1bk/6b1/7p/p1p4p/PNPpP2P/KN4P1/3Q4/4R3 b - - 0 37", 69 | "2q3r1/1r2pk2/pp3pp1/2pP3p/P1Pb1BbP/1P4Q1/R3NPP1/4R1K1 w - - 2 34", 70 | "1r2r2k/1b4q1/pp5p/2pPp1p1/P3Pn2/1P1B1Q1P/2R3P1/4BR1K b - - 1 37", 71 | "r3kbbr/pp1n1p1P/3ppnp1/q5N1/1P1pP3/P1N1B3/2P1QP2/R3KB1R b KQkq b3 0 17", 72 | "8/6pk/2b1Rp2/3r4/1R1B2PP/P5K1/8/2r5 b - - 16 42", 73 | "1r4k1/4ppb1/2n1b1qp/pB4p1/1n1BP1P1/7P/2PNQPK1/3RN3 w - - 8 29", 74 | "8/p2B4/PkP5/4p1pK/4Pb1p/5P2/8/8 w - - 29 68", 75 | "3r4/ppq1ppkp/4bnp1/2pN4/2P1P3/1P4P1/PQ3PBP/R4K2 b - - 2 20", 76 | "5rr1/4n2k/4q2P/P1P2n2/3B1p2/4pP2/2N1P3/1RR1K2Q w - - 1 49", 77 | "1r5k/2pq2p1/3p3p/p1pP4/4QP2/PP1R3P/6PK/8 w - - 1 51", 78 | "q5k1/5ppp/1r3bn1/1B6/P1N2P2/BQ2P1P1/5K1P/8 b - - 2 34", 79 | "r1b2k1r/5n2/p4q2/1ppn1Pp1/3pp1p1/NP2P3/P1PPBK2/1RQN2R1 w - - 0 22", 80 | "r1bqk2r/pppp1ppp/5n2/4b3/4P3/P1N5/1PP2PPP/R1BQKB1R w KQkq - 0 5", 81 | "r1bqr1k1/pp1p1ppp/2p5/8/3N1Q2/P2BB3/1PP2PPP/R3K2n b Q - 1 12", 82 | "r1bq2k1/p4r1p/1pp2pp1/3p4/1P1B3Q/P2B1N2/2P3PP/4R1K1 b - - 2 19", 83 | "r4qk1/6r1/1p4p1/2ppBbN1/1p5Q/P7/2P3PP/5RK1 w - - 2 25", 84 | "r7/6k1/1p6/2pp1p2/7Q/8/p1P2K1P/8 w - - 0 32", 85 | "r3k2r/ppp1pp1p/2nqb1pn/3p4/4P3/2PP4/PP1NBPPP/R2QK1NR w KQkq - 1 5", 86 | "3r1rk1/1pp1pn1p/p1n1q1p1/3p4/Q3P3/2P5/PP1NBPPP/4RRK1 w - - 0 12", 87 | "5rk1/1pp1pn1p/p3Brp1/8/1n6/5N2/PP3PPP/2R2RK1 w - - 2 20", 88 | "8/1p2pk1p/p1p1r1p1/3n4/8/5R2/PP3PPP/4R1K1 b - - 3 27", 89 | "8/4pk2/1p1r2p1/p1p4p/Pn5P/3R4/1P3PP1/4RK2 w - - 1 33", 90 | "8/5k2/1pnrp1p1/p1p4p/P6P/4R1PK/1P3P2/4R3 b - - 1 38", 91 | "8/8/1p1kp1p1/p1pr1n1p/P6P/1R4P1/1P3PK1/1R6 b - - 15 45", 92 | "8/8/1p1k2p1/p1prp2p/P2n3P/6P1/1P1R1PK1/4R3 b - - 5 49", 93 | "8/8/1p4p1/p1p2k1p/P2npP1P/4K1P1/1P6/3R4 w - - 6 54", 94 | "8/8/1p4p1/p1p2k1p/P2n1P1P/4K1P1/1P6/6R1 b - - 6 59", 95 | "8/5k2/1p4p1/p1pK3p/P2n1P1P/6P1/1P6/4R3 b - - 14 63", 96 | "8/1R6/1p1K1kp1/p6p/P1p2P1P/6P1/1Pn5/8 w - - 0 67", 97 | "1rb1rn1k/p3q1bp/2p3p1/2p1p3/2P1P2N/PP1RQNP1/1B3P2/4R1K1 b - - 4 23", 98 | "4rrk1/pp1n1pp1/q5p1/P1pP4/2n3P1/7P/1P3PB1/R1BQ1RK1 w - - 3 22", 99 | "r2qr1k1/pb1nbppp/1pn1p3/2ppP3/3P4/2PB1NN1/PP3PPP/R1BQR1K1 w - - 4 12", 100 | "2r2k2/8/4P1R1/1p6/8/P4K1N/7b/2B5 b - - 0 55", 101 | "6k1/5pp1/8/2bKP2P/2P5/p4PNb/B7/8 b - - 1 44", 102 | "2rqr1k1/1p3p1p/p2p2p1/P1nPb3/2B1P3/5P2/1PQ2NPP/R1R4K w - - 3 25", 103 | "r1b2rk1/p1q1ppbp/6p1/2Q5/8/4BP2/PPP3PP/2KR1B1R b - - 2 14", 104 | "6r1/5k2/p1b1r2p/1pB1p1p1/1Pp3PP/2P1R1K1/2P2P2/3R4 w - - 1 36", 105 | "rnbqkb1r/pppppppp/5n2/8/2PP4/8/PP2PPPP/RNBQKBNR b KQkq c3 0 2", 106 | "2rr2k1/1p4bp/p1q1p1p1/4Pp1n/2PB4/1PN3P1/P3Q2P/2RR2K1 w - f6 0 20", 107 | "3br1k1/p1pn3p/1p3n2/5pNq/2P1p3/1PN3PP/P2Q1PB1/4R1K1 w - - 0 23", 108 | "2r2b2/5p2/5k2/p1r1pP2/P2pB3/1P3P2/K1P3R1/7R w - - 23 93" 109 | }; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Bindings/simd.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "defs.h" 5 | 6 | #if defined(AVX512) && defined(TODO_SOME_DAY) 7 | 8 | using vec_i8 = __m512i; 9 | using vec_i16 = __m512i; 10 | using vec_i32 = __m512i; 11 | using vec_ps = __m512; 12 | using vec_128i = __m128i; 13 | 14 | inline vec_i8 vec_packus_epi16(const vec_i16 a, const vec_i16 b) { return _mm512_packus_epi16(a, b); } 15 | inline void vec_storeu_epi8(vec_i8* a, const vec_i8 b) { _mm512_storeu_si512(a, b); } 16 | 17 | inline vec_ps vec_set1_ps(const float a) { return _mm512_set1_ps(a); } 18 | inline vec_ps vec_fmadd_ps(const vec_ps a, const vec_ps b, const vec_ps c) { return _mm512_fmadd_ps(a, b, c); } 19 | inline vec_ps vec_min_ps(const vec_ps a, const vec_ps b) { return _mm512_min_ps(a, b); } 20 | inline vec_ps vec_max_ps(const vec_ps a, const vec_ps b) { return _mm512_max_ps(a, b); } 21 | inline vec_ps vec_mul_ps(const vec_ps a, const vec_ps b) { return _mm512_mul_ps(a, b); } 22 | inline vec_ps vec_cvtepi32_ps(const vec_i32 a) { return _mm512_cvtepi32_ps(a); } 23 | inline vec_ps vec_loadu_ps(const float* a) { return _mm512_loadu_ps(a); } 24 | inline void vec_storeu_ps(float* a, const vec_ps b) { _mm512_storeu_ps(a, b); } 25 | 26 | inline vec_i16 vec_set1_epi16(const i16 a) { return _mm512_set1_epi16(a); } 27 | inline vec_i16 vec_setzero_epi16() { return _mm512_setzero_si512(); } 28 | inline vec_i16 vec_add_epi16(const vec_i16 a, const vec_i16 b) { return _mm512_add_epi16(a, b); } 29 | inline vec_i16 vec_sub_epi16(const vec_i16 a, const vec_i16 b) { return _mm512_sub_epi16(a, b); } 30 | inline vec_i16 vec_maddubs_epi16(const vec_i8 a, const vec_i8 b) { return _mm512_maddubs_epi16(a, b); } 31 | inline vec_i16 vec_mulhi_epi16(const vec_i16 a, const vec_i16 b) { return _mm512_mulhi_epi16(a, b); } 32 | inline vec_i16 vec_slli_epi16(const vec_i16 a, const i16 i) { return _mm512_slli_epi16(a, i); } 33 | inline vec_i16 vec_min_epi16(const vec_i16 a, const vec_i16 b) { return _mm512_min_epi16(a, b); } 34 | inline vec_i16 vec_max_epi16(const vec_i16 a, const vec_i16 b) { return _mm512_max_epi16(a, b); } 35 | inline vec_i16 vec_load_epi16(const vec_i16* a) { return _mm512_load_si512(a); } 36 | inline void vec_storeu_i16(vec_i16* a, const vec_i16 b) { _mm512_storeu_si512(a, b); } 37 | 38 | inline vec_i32 vec_set1_epi32(const i32 a) { return _mm512_set1_epi32(a); } 39 | inline vec_i32 vec_add_epi32(const vec_i32 a, const vec_i32 b) { return _mm512_add_epi32(a, b); } 40 | inline vec_i32 vec_madd_epi16(const vec_i16 a, const vec_i16 b) { return _mm512_madd_epi16(a, b); } 41 | 42 | inline uint16_t vec_nnz_mask(const vec_i32 vec) { return _mm512_cmpgt_epi32_mask(vec, _mm512_setzero_si512()); } 43 | 44 | inline float vec_hsum_ps(const vec_ps v) { 45 | return _mm512_reduce_add_ps(v); 46 | } 47 | 48 | inline vec_i32 vec_dpbusd_epi32(const vec_i32 sum, const vec_i8 vec0, const vec_i8 vec1) { 49 | const vec_i16 product16 = vec_maddubs_epi16(vec0, vec1); 50 | const vec_i32 product32 = vec_madd_epi16(product16, vec_set1_epi16(1)); 51 | return vec_add_epi32(sum, product32); 52 | } 53 | 54 | 55 | #else 56 | 57 | using vec_i8 = __m256i; 58 | using vec_i16 = __m256i; 59 | using vec_i32 = __m256i; 60 | using vec_ps = __m256; 61 | using vec_128i = __m128i; 62 | 63 | inline vec_i8 vec_packus_epi16(const vec_i16 a, const vec_i16 b) { return _mm256_packus_epi16(a, b); } 64 | inline void vec_storeu_epi8(vec_i8* a, const vec_i8 b) { _mm256_storeu_si256(a, b); } 65 | 66 | inline vec_ps vec_set1_ps(const float a) { return _mm256_set1_ps(a); } 67 | inline vec_ps vec_fmadd_ps(const vec_ps a, const vec_ps b, const vec_ps c) { return _mm256_fmadd_ps(a, b, c); } 68 | inline vec_ps vec_min_ps(const vec_ps a, const vec_ps b) { return _mm256_min_ps(a, b); } 69 | inline vec_ps vec_max_ps(const vec_ps a, const vec_ps b) { return _mm256_max_ps(a, b); } 70 | inline vec_ps vec_mul_ps(const vec_ps a, const vec_ps b) { return _mm256_mul_ps(a, b); } 71 | inline vec_ps vec_cvtepi32_ps(const vec_i32 a) { return _mm256_cvtepi32_ps(a); } 72 | inline vec_ps vec_loadu_ps(const float* a) { return _mm256_loadu_ps(a); } 73 | inline void vec_storeu_ps(float* a, const vec_ps b) { _mm256_storeu_ps(a, b); } 74 | 75 | inline vec_i16 vec_set1_epi16(const i16 a) { return _mm256_set1_epi16(a); } 76 | inline vec_i16 vec_setzero_epi16() { return _mm256_setzero_si256(); } 77 | inline vec_i16 vec_maddubs_epi16(const vec_i8 a, const vec_i8 b) { return _mm256_maddubs_epi16(a, b); } 78 | inline vec_i16 vec_add_epi16(const vec_i16 a, const vec_i16 b) { return _mm256_add_epi16(a, b); } 79 | inline vec_i16 vec_sub_epi16(const vec_i16 a, const vec_i16 b) { return _mm256_sub_epi16(a, b); } 80 | inline vec_i16 vec_mulhi_epi16(const vec_i16 a, const vec_i16 b) { return _mm256_mulhi_epi16(a, b); } 81 | inline vec_i16 vec_slli_epi16(const vec_i16 a, const i16 i) { return _mm256_slli_epi16(a, i); } 82 | inline vec_i16 vec_min_epi16(const vec_i16 a, const vec_i16 b) { return _mm256_min_epi16(a, b); } 83 | inline vec_i16 vec_max_epi16(const vec_i16 a, const vec_i16 b) { return _mm256_max_epi16(a, b); } 84 | inline vec_i16 vec_load_epi16(const vec_i16* a) { return _mm256_load_si256(a); } 85 | inline void vec_storeu_i16(vec_i16* a, const vec_i16 b) { _mm256_storeu_si256(a, b); } 86 | 87 | inline vec_i32 vec_set1_epi32(const i32 a) { return _mm256_set1_epi32(a); } 88 | inline vec_i32 vec_add_epi32(const vec_i32 a, const vec_i32 b) { return _mm256_add_epi32(a, b); } 89 | inline vec_i32 vec_madd_epi16(const vec_i16 a, const vec_i16 b) { return _mm256_madd_epi16(a, b); } 90 | 91 | inline uint16_t vec_nnz_mask(const vec_i32 vec) { return _mm256_movemask_ps(_mm256_castsi256_ps(_mm256_cmpgt_epi32(vec, _mm256_setzero_si256()))); } 92 | 93 | inline float vec_hsum_ps(const vec_ps v) { 94 | __m128 sum128 = _mm_add_ps(_mm256_castps256_ps128(v), _mm256_extractf128_ps(v, 1)); 95 | sum128 = _mm_hadd_ps(sum128, sum128); 96 | sum128 = _mm_hadd_ps(sum128, sum128); 97 | return _mm_cvtss_f32(sum128); 98 | } 99 | 100 | inline vec_i32 vec_dpbusd_epi32(const vec_i32 sum, const vec_i8 vec0, const vec_i8 vec1) { 101 | const vec_i16 product16 = vec_maddubs_epi16(vec0, vec1); 102 | const vec_i32 product32 = vec_madd_epi16(product16, vec_set1_epi16(1)); 103 | return vec_add_epi32(sum, product32); 104 | } 105 | 106 | #endif -------------------------------------------------------------------------------- /Properties/Resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | -------------------------------------------------------------------------------- /Logic/Datagen/Rescorer.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Collections.Concurrent; 3 | using System.Runtime.InteropServices; 4 | 5 | using static Lizard.Logic.Datagen.DatagenParameters; 6 | 7 | using Lizard.Logic.Datagen; 8 | using Lizard.Logic.Threads; 9 | 10 | namespace Lizard.Logic.Util 11 | { 12 | public unsafe static class Rescorer 13 | { 14 | private const int MaxEntriesInQueue = 64; 15 | private const int ChunkSize = 1024; 16 | private const int BytesToRead = BulletFormatEntry.Size * ChunkSize; 17 | private const double BlendPercentage = 0.0; 18 | 19 | static readonly ConcurrentQueue InputQueue = new ConcurrentQueue(); 20 | static readonly SemaphoreSlim QueueSignal = new SemaphoreSlim(MaxEntriesInQueue); 21 | static bool readingCompleted = false; 22 | 23 | 24 | public static void Start(string inputFile, int workerCount = 1) 25 | { 26 | SearchOptions.Hash = HashSize; 27 | 28 | Task readerThread = Task.Run(() => ReaderTask(inputFile)); 29 | 30 | Parallel.For(0, workerCount, i => 31 | { 32 | WorkerTask(inputFile, i); 33 | }); 34 | 35 | readerThread.Wait(); 36 | } 37 | 38 | static void ReaderTask(string inputDataFile) 39 | { 40 | long fileSize = new FileInfo(inputDataFile).Length; 41 | 42 | using FileStream fs = new FileStream(inputDataFile, FileMode.Open, FileAccess.Read); 43 | byte[] buffer = new byte[BytesToRead]; 44 | int bytesRead; 45 | while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0) 46 | { 47 | // If the last chunk is smaller than the buffer size, resize it 48 | if (bytesRead < buffer.Length) 49 | { 50 | Array.Resize(ref buffer, bytesRead); 51 | } 52 | 53 | Span entries = MemoryMarshal.Cast(buffer); 54 | for (int i = 0; i < entries.Length; i++) 55 | { 56 | fixed (byte* b = entries[i]._pad) 57 | { 58 | (*(byte*)&b[0]) = 58; 59 | (*(short*)&b[1]) = 25395; 60 | } 61 | } 62 | 63 | Console.Title = $"Progress {((double)fs.Position / fs.Length) * 100:N4}%"; 64 | 65 | QueueSignal.Wait(); // Only allow MaxEntriesInQueue items to be in memory at any given time 66 | 67 | InputQueue.Enqueue(buffer); 68 | buffer = new byte[BytesToRead]; // Allocate a new buffer for the next chunk 69 | } 70 | 71 | readingCompleted = true; 72 | } 73 | 74 | static void WorkerTask(string inputDataFile, int workerId) 75 | { 76 | var finfo = new FileInfo(inputDataFile); 77 | string outputFile = Path.Join(finfo.Directory.FullName, $"{finfo.Name[..finfo.Name.IndexOf(".bin")]}_rescored{workerId}.bin"); 78 | 79 | if (outputFile == inputDataFile) { throw new Exception($"Failed creating outputFile for thread {workerId}!"); } 80 | Log($"{Environment.CurrentManagedThreadId,2}:{workerId,2} writing to {outputFile}"); 81 | 82 | using var file = File.Open(outputFile, FileMode.Create); 83 | using BinaryWriter outputWriter = new BinaryWriter(file, System.Text.Encoding.UTF8); 84 | 85 | SearchThreadPool pool = new SearchThreadPool(1); 86 | Position pos = new Position(owner: pool.MainThread); 87 | ref Bitboard bb = ref pos.bb; 88 | 89 | SearchInformation info = new SearchInformation(pos) 90 | { 91 | SoftNodeLimit = SoftNodeLimit, 92 | NodeLimit = SoftNodeLimit * 20, 93 | DepthLimit = DepthLimit, 94 | OnDepthFinish = null, 95 | OnSearchFinish = null, 96 | }; 97 | 98 | long numRescored = 0; 99 | Stopwatch sw = Stopwatch.StartNew(); 100 | 101 | while (!readingCompleted || !InputQueue.IsEmpty) 102 | { 103 | if (InputQueue.TryDequeue(out byte[] data)) 104 | { 105 | QueueSignal.Release(); 106 | Span entries = MemoryMarshal.Cast(data); 107 | 108 | pos.LoadFromFEN(InitialFEN); 109 | 110 | int entryCount = data.Length / BulletFormatEntry.Size; 111 | //Log($"WorkerThread{workerId} got {entryCount} entries"); 112 | for (int i = 0; i < entryCount; i++) 113 | { 114 | ref BulletFormatEntry e = ref entries[i]; 115 | 116 | e.FillBitboard(ref bb); 117 | Selfplay.ResetPosition(pos); 118 | 119 | pool.TTable.Clear(); 120 | pool.Clear(); 121 | 122 | pool.StartSearch(pos, ref info); 123 | pool.BlockCallerUntilFinished(); 124 | 125 | int score = pool.GetBestThread().RootMoves[0].Score; 126 | 127 | if (Math.Abs(score) > MaxFilteringScore) 128 | { 129 | // If the score is outside the acceptable bounds, leave the entry as it was 130 | continue; 131 | } 132 | 133 | //Log($"{pos.GetFEN(),-72}\t{e.score}\t->\t{score}"); 134 | 135 | // Adjust scores based on BlendPercentage, where: 136 | // 0% will save the score as returned by search 137 | // 100% will save the existing, unchanged score 138 | int blendedScore = (int)((score * (1 - BlendPercentage)) + (e.score * BlendPercentage)); 139 | e.score = (short)int.Clamp(blendedScore, short.MinValue, short.MaxValue); 140 | } 141 | numRescored += entryCount; 142 | 143 | var posPerSec = (double)numRescored / sw.Elapsed.TotalSeconds; 144 | Log($"{Environment.CurrentManagedThreadId,2}:{workerId,2}\t" + 145 | $"{numRescored,9}" + 146 | $"{posPerSec,10:N1}/sec"); 147 | 148 | outputWriter.Write(data, 0, data.Length); 149 | } 150 | } 151 | } 152 | 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /Bindings/SIMD.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "simd.h" 11 | #include "arch.h" 12 | #include "defs.h" 13 | 14 | #if defined(_MSC_VER) 15 | #define DLL_EXPORT extern "C" __declspec(dllexport) 16 | #else 17 | #define DLL_EXPORT extern "C" 18 | #endif 19 | 20 | DLL_EXPORT void SetupNNZ(); 21 | 22 | DLL_EXPORT void EvaluateBound(const i16* us, const i16* them, 23 | const i8* L1Weights, const float* L1Biases, 24 | const float* L2Weights, const float* L2Biases, 25 | const float* L3weights, const float L3bias, 26 | int& L3Output); 27 | 28 | struct NNZTable { 29 | __m128i Entries[256]; 30 | }; 31 | NNZTable nnzTable; 32 | 33 | DLL_EXPORT void SetupNNZ() { 34 | for (u32 i = 0; i < 256; i++) { 35 | u16* ptr = reinterpret_cast(&nnzTable.Entries[i]); 36 | 37 | u32 j = i; 38 | u32 k = 0; 39 | while (j != 0) { 40 | u32 lsbIndex = std::countr_zero(j); 41 | j &= j - 1; 42 | ptr[k++] = (u16)lsbIndex; 43 | } 44 | } 45 | } 46 | 47 | DLL_EXPORT void EvaluateBound(const i16* us, const i16* them, 48 | const i8* L1Weights, const float* L1Biases, 49 | const float* L2Weights, const float* L2Biases, 50 | const float* L3weights, const float L3bias, 51 | i32& L3Output) { 52 | 53 | i32 nnzCount = 0; 54 | u16 nnzIndices[L1_SIZE / L1_CHUNK_PER_32] alignas(32); 55 | i8 FTOutputs[L1_SIZE] alignas(32); 56 | 57 | vec_i32 L1Temp[L2_SIZE / I32_CHUNK_SIZE] alignas(32) = {}; 58 | float L1Outputs[L2_SIZE] alignas(32); 59 | 60 | vec_ps L2Outputs[L3_SIZE / F32_CHUNK_SIZE] alignas(32); 61 | 62 | // FT 63 | { 64 | const auto ft_zero = vec_setzero_epi16(); 65 | const auto ft_one = vec_set1_epi16(FT_QUANT); 66 | const vec_128i baseInc = _mm_set1_epi16(u16(8)); 67 | vec_128i baseVec = _mm_setzero_si128(); 68 | i32 offset = 0; 69 | 70 | for (const auto acc : { us, them }) { 71 | for (i32 i = 0; i < L1_PAIR_COUNT; i += (I16_CHUNK_SIZE * 2)) { 72 | const auto input0a = vec_load_epi16(reinterpret_cast(&acc[i + 0 * I16_CHUNK_SIZE + 0])); 73 | const auto input0b = vec_load_epi16(reinterpret_cast(&acc[i + 1 * I16_CHUNK_SIZE + 0])); 74 | 75 | const auto input1a = vec_load_epi16(reinterpret_cast(&acc[i + 0 * I16_CHUNK_SIZE + L1_PAIR_COUNT])); 76 | const auto input1b = vec_load_epi16(reinterpret_cast(&acc[i + 1 * I16_CHUNK_SIZE + L1_PAIR_COUNT])); 77 | 78 | const auto clipped0a = vec_min_epi16(vec_max_epi16(input0a, ft_zero), ft_one); 79 | const auto clipped0b = vec_min_epi16(vec_max_epi16(input0b, ft_zero), ft_one); 80 | 81 | const auto clipped1a = vec_min_epi16(input1a, ft_one); 82 | const auto clipped1b = vec_min_epi16(input1b, ft_one); 83 | 84 | const auto producta = vec_mulhi_epi16(vec_slli_epi16(clipped0a, 16 - FT_SHIFT), clipped1a); 85 | const auto productb = vec_mulhi_epi16(vec_slli_epi16(clipped0b, 16 - FT_SHIFT), clipped1b); 86 | 87 | const auto product_one = vec_packus_epi16(producta, productb); 88 | vec_storeu_epi8(reinterpret_cast(&FTOutputs[offset + i]), product_one); 89 | 90 | const auto nnz_mask = vec_nnz_mask(product_one); 91 | 92 | for (i32 j = 0; j < NNZ_OUTPUTS_PER_CHUNK; j++) { 93 | i32 lookup = (nnz_mask >> (j * 8)) & 0xFF; 94 | auto offsets = nnzTable.Entries[lookup]; 95 | _mm_storeu_si128(reinterpret_cast(&nnzIndices[nnzCount]), _mm_add_epi16(baseVec, offsets)); 96 | 97 | nnzCount += std::popcount(static_cast(lookup)); 98 | baseVec = _mm_add_epi16(baseVec, baseInc); 99 | } 100 | 101 | } 102 | 103 | offset += L1_PAIR_COUNT; 104 | } 105 | } 106 | 107 | 108 | // L1 109 | { 110 | i8* L1Inputs = FTOutputs; 111 | const auto inputs32 = (i32*)(FTOutputs); 112 | for (i32 i = 0; i < nnzCount; i++) { 113 | const auto index = nnzIndices[i]; 114 | const auto input32 = vec_set1_epi32(inputs32[index]); 115 | const auto weight = reinterpret_cast(&L1Weights[index * L1_CHUNK_PER_32 * L2_SIZE]); 116 | for (i32 k = 0; k < L2_SIZE / F32_CHUNK_SIZE; k++) 117 | L1Temp[k] = vec_dpbusd_epi32(L1Temp[k], input32, weight[k]); 118 | } 119 | 120 | const auto zero = vec_set1_ps(0.0f); 121 | const auto one = vec_set1_ps(1.0f); 122 | const auto sumMul = vec_set1_ps(L1_MUL); 123 | for (i32 i = 0; i < L2_SIZE / F32_CHUNK_SIZE; ++i) { 124 | const auto biasVec = vec_loadu_ps(&L1Biases[i * F32_CHUNK_SIZE]); 125 | const auto sumPs = vec_fmadd_ps(vec_cvtepi32_ps(L1Temp[i]), sumMul, biasVec); 126 | const auto clipped = vec_min_ps(vec_max_ps(sumPs, zero), one); 127 | const auto squared = vec_mul_ps(clipped, clipped); 128 | vec_storeu_ps(&L1Outputs[i * F32_CHUNK_SIZE], squared); 129 | } 130 | } 131 | 132 | 133 | // L2 134 | { 135 | float* L2Inputs = L1Outputs; 136 | for (i32 i = 0; i < L3_SIZE / F32_CHUNK_SIZE; ++i) 137 | L2Outputs[i] = vec_loadu_ps(&L2Biases[i * F32_CHUNK_SIZE]); 138 | 139 | for (i32 i = 0; i < L2_SIZE; ++i) { 140 | const auto inputVec = vec_set1_ps(L2Inputs[i]); 141 | const auto weight = reinterpret_cast(&L2Weights[i * L3_SIZE]); 142 | for (i32 j = 0; j < L3_SIZE / F32_CHUNK_SIZE; ++j) 143 | L2Outputs[j] = vec_fmadd_ps(inputVec, weight[j], L2Outputs[j]); 144 | } 145 | } 146 | 147 | 148 | // L3 149 | { 150 | auto l3Sum = vec_set1_ps(0.0f); 151 | const auto zero = vec_set1_ps(0.0f); 152 | const auto one = vec_set1_ps(1.0f); 153 | for (i32 i = 0; i < L3_SIZE / F32_CHUNK_SIZE; ++i) { 154 | const auto clipped = vec_min_ps(vec_max_ps(L2Outputs[i], zero), one); 155 | const auto squared = vec_mul_ps(clipped, clipped); 156 | 157 | const auto weightVec = vec_loadu_ps(&L3weights[i * F32_CHUNK_SIZE]); 158 | l3Sum = vec_fmadd_ps(squared, weightVec, l3Sum); 159 | } 160 | 161 | L3Output = static_cast((L3bias + vec_hsum_ps(l3Sum)) * OutputScale); 162 | } 163 | } 164 | 165 | 166 | -------------------------------------------------------------------------------- /Logic/Util/Interop.cs: -------------------------------------------------------------------------------- 1 | using Lizard.Properties; 2 | using System.Numerics; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | using System.Runtime.Intrinsics.X86; 6 | 7 | #pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type 8 | 9 | namespace Lizard.Logic.Util 10 | { 11 | public static class Interop 12 | { 13 | /// 14 | /// Hints the CPU that we are going to be using the data located at soon, 15 | /// so it should fetch a cache line from that address and place it in a high locality cache. 16 | /// 17 | /// This isn't a guarantee, and the time it takes for to compute the address does hurt, 18 | /// but regardless this seems to help. 19 | /// 20 | [MethodImpl(Inline)] 21 | public static unsafe void prefetch(void* address) 22 | { 23 | if (Sse.IsSupported) 24 | { 25 | Sse.Prefetch0(address); 26 | } 27 | } 28 | 29 | 30 | /// 31 | /// Returns the number of bits set in using _mm_popcnt_u64 32 | /// 33 | [MethodImpl(Inline)] 34 | public static ulong popcount(ulong value) 35 | { 36 | return ulong.PopCount(value); 37 | } 38 | 39 | /// 40 | /// Returns true if has more than one bit set. 41 | /// 42 | [MethodImpl(Inline)] 43 | public static bool MoreThanOne(ulong value) 44 | { 45 | return poplsb(value) != 0; 46 | } 47 | 48 | /// 49 | /// Returns the number of trailing least significant zero bits in using Bmi1.X64.TrailingZeroCount. 50 | /// So lsb(100_2) returns 2. 51 | /// 52 | [MethodImpl(Inline)] 53 | public static int lsb(ulong value) 54 | { 55 | return (int)ulong.TrailingZeroCount(value); 56 | } 57 | 58 | /// 59 | /// Sets the least significant bit to 0 using Bmi1.X64.ResetLowestSetBit. 60 | /// So PopLsb(10110_2) returns 10100_2. 61 | /// 62 | [MethodImpl(Inline)] 63 | public static ulong poplsb(ulong value) 64 | { 65 | return value & (value - 1); 66 | } 67 | 68 | /// 69 | /// Returns the number of trailing least significant zero bits in using _mm_tzcnt_64, 70 | /// and clears the lowest set bit with _blsr_u64. 71 | /// 72 | [MethodImpl(Inline)] 73 | public static unsafe int poplsb(ulong* value) 74 | { 75 | int sq = (int)ulong.TrailingZeroCount(*value); 76 | *value = *value & (*value - 1); 77 | return sq; 78 | } 79 | 80 | /// 81 | /// Returns the index of the most significant bit (highest, toward the square H8) 82 | /// set in the mask using Lzcnt.X64.LeadingZeroCount 83 | /// 84 | [MethodImpl(Inline)] 85 | public static int msb(ulong value) 86 | { 87 | if (Lzcnt.X64.IsSupported) 88 | { 89 | return (int)(63 - Lzcnt.X64.LeadingZeroCount(value)); 90 | } 91 | else 92 | { 93 | return BitOperations.Log2(value - 1) + 1; 94 | } 95 | } 96 | 97 | /// 98 | /// Returns with the most significant bit set to 0. 99 | /// 100 | [MethodImpl(Inline)] 101 | public static unsafe int popmsb(ulong* value) 102 | { 103 | int sq = (int)(63 - ulong.LeadingZeroCount(*value)); 104 | *value = *value & ~(1UL << sq); 105 | return sq; 106 | } 107 | 108 | 109 | /// 110 | /// Extracts the bits from that are set in , 111 | /// and places them in the least significant bits of the result. 112 | ///

113 | /// The output will be somewhat similar to a bitwise AND operation, just shifted and condensed to the right. 114 | /// 115 | /// So pext("ABCD EFGH", 1011 0001) would return "0000 ACDH", 116 | /// where ACDH could each be 0 or 1 depending on if they were set in 117 | ///
118 | [MethodImpl(Inline)] 119 | public static ulong pext(ulong value, ulong mask) 120 | { 121 | if (Bmi2.X64.IsSupported) 122 | { 123 | return Bmi2.X64.ParallelBitExtract(value, mask); 124 | } 125 | else 126 | { 127 | ulong res = 0; 128 | for (ulong bb = 1; mask != 0; bb += bb) 129 | { 130 | if ((value & mask & (0UL - mask)) != 0) 131 | { 132 | res |= bb; 133 | } 134 | mask &= mask - 1; 135 | } 136 | return res; 137 | } 138 | } 139 | 140 | 141 | /// 142 | /// Allocates a block of memory of size , aligned on the boundary , 143 | /// and clears the block before returning its address. 144 | /// 145 | /// The class provides to make sure that the block is aligned, 146 | /// and , to ensure that the memory in that block is set to 0 before it is used, 147 | /// but doesn't have a method to do these both. 148 | /// 149 | public static unsafe void* AlignedAllocZeroed(nuint byteCount, nuint alignment = AllocAlignment) 150 | { 151 | void* block = NativeMemory.AlignedAlloc(byteCount, alignment); 152 | NativeMemory.Clear(block, byteCount); 153 | 154 | return block; 155 | } 156 | 157 | /// 158 | /// 159 | /// 160 | public static unsafe T* AlignedAllocZeroed(nuint items, nuint alignment = AllocAlignment) 161 | { 162 | nuint bytes = ((nuint)sizeof(T) * (nuint)items); 163 | void* block = NativeMemory.AlignedAlloc(bytes, alignment); 164 | NativeMemory.Clear(block, bytes); 165 | 166 | return (T*)block; 167 | } 168 | 169 | 170 | [DllImport("libc", SetLastError = true)] 171 | private static extern int madvise(IntPtr addr, UIntPtr length, int advice); 172 | private const int MADV_HUGEPAGE = 14; 173 | public static unsafe void AdviseHugePage(void* addr, nuint length) 174 | { 175 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 176 | { 177 | return; 178 | } 179 | 180 | try 181 | { 182 | int result = madvise(new IntPtr(addr), length, MADV_HUGEPAGE); 183 | if (result != 0) 184 | { 185 | Console.WriteLine($"info string madvise failed with result {result} and error {Marshal.GetLastSystemError()}"); 186 | } 187 | } 188 | catch (Exception exc) 189 | { 190 | Console.WriteLine($"info string madvise threw {exc.GetType()}"); 191 | } 192 | } 193 | 194 | 195 | public static bool HasAnsi = true; 196 | 197 | public static void CheckAnsi() 198 | { 199 | if (Console.IsOutputRedirected) 200 | { 201 | HasAnsi = false; 202 | return; 203 | } 204 | 205 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 206 | { 207 | HasAnsi = true; 208 | return; 209 | } 210 | 211 | // Windows 11 212 | HasAnsi = Environment.OSVersion.Version.Build >= 22000; 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /Logic/Transposition/Zobrist.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Lizard.Logic.Transposition 5 | { 6 | public static unsafe class Zobrist 7 | { 8 | private const int DefaultSeed = 0xBEEF; 9 | 10 | private static readonly ulong[] ColorPieceSquareHashes = new ulong[ColorNB * 6 * 64]; 11 | private static readonly ulong[] CastlingRightsHashes = new ulong[ColorNB * 2]; 12 | private static readonly ulong[] EnPassantFileHashes = new ulong[8]; 13 | private static ulong BlackHash; 14 | private static readonly Random rand = new Random(DefaultSeed); 15 | 16 | public static ulong HashForPiece(int pc, int pt, int sq) => ColorPieceSquareHashes[ColorPieceSquareHashesIndex(pc, pt, sq)]; 17 | public static ulong ColorHash => BlackHash; 18 | 19 | [ModuleInitializer] 20 | public static void Initialize() 21 | { 22 | for (int pt = Piece.Pawn; pt <= Piece.King; pt++) 23 | { 24 | for (int i = 0; i < 64; i++) 25 | { 26 | ColorPieceSquareHashes[ColorPieceSquareHashesIndex(Color.White, pt, i)] = rand.NextUlong(); 27 | ColorPieceSquareHashes[ColorPieceSquareHashesIndex(Color.Black, pt, i)] = rand.NextUlong(); 28 | } 29 | } 30 | 31 | for (int i = 0; i < 4; i++) 32 | { 33 | CastlingRightsHashes[i] = rand.NextUlong(); 34 | } 35 | 36 | for (int i = 0; i < 8; i++) 37 | { 38 | EnPassantFileHashes[i] = rand.NextUlong(); 39 | } 40 | 41 | BlackHash = rand.NextUlong(); 42 | } 43 | 44 | public static ulong GetHash(Position position, ulong* pawnHash, ulong* nonPawnHash) 45 | { 46 | ref Bitboard bb = ref position.bb; 47 | 48 | ulong hash = 0; 49 | 50 | ulong white = bb.Colors[Color.White]; 51 | ulong black = bb.Colors[Color.Black]; 52 | 53 | while (white != 0) 54 | { 55 | int idx = poplsb(&white); 56 | int pt = bb.GetPieceAtIndex(idx); 57 | hash ^= ColorPieceSquareHashes[ColorPieceSquareHashesIndex(Color.White, pt, idx)]; 58 | 59 | if (pt == Pawn) 60 | { 61 | *pawnHash ^= ColorPieceSquareHashes[ColorPieceSquareHashesIndex(Color.White, pt, idx)]; 62 | } 63 | else 64 | { 65 | *nonPawnHash ^= ColorPieceSquareHashes[ColorPieceSquareHashesIndex(Color.White, pt, idx)]; 66 | } 67 | } 68 | 69 | while (black != 0) 70 | { 71 | int idx = poplsb(&black); 72 | int pt = bb.GetPieceAtIndex(idx); 73 | hash ^= ColorPieceSquareHashes[ColorPieceSquareHashesIndex(Color.Black, pt, idx)]; 74 | 75 | if (pt == Pawn) 76 | { 77 | *pawnHash ^= ColorPieceSquareHashes[ColorPieceSquareHashesIndex(Color.Black, pt, idx)]; 78 | } 79 | else 80 | { 81 | *nonPawnHash ^= ColorPieceSquareHashes[ColorPieceSquareHashesIndex(Color.Black, pt, idx)]; 82 | } 83 | } 84 | 85 | if ((position.State->CastleStatus & CastlingStatus.WK) != 0) 86 | { 87 | hash ^= CastlingRightsHashes[0]; 88 | } 89 | if ((position.State->CastleStatus & CastlingStatus.WQ) != 0) 90 | { 91 | hash ^= CastlingRightsHashes[1]; 92 | } 93 | if ((position.State->CastleStatus & CastlingStatus.BK) != 0) 94 | { 95 | hash ^= CastlingRightsHashes[2]; 96 | } 97 | if ((position.State->CastleStatus & CastlingStatus.BQ) != 0) 98 | { 99 | hash ^= CastlingRightsHashes[3]; 100 | } 101 | 102 | if (position.State->EPSquare != EPNone) 103 | { 104 | hash ^= EnPassantFileHashes[GetIndexFile(position.State->EPSquare)]; 105 | } 106 | 107 | if (position.ToMove == Color.Black) 108 | { 109 | hash ^= BlackHash; 110 | } 111 | 112 | return hash; 113 | } 114 | 115 | /// 116 | /// Updates the hash by moving the piece of type and color from to . 117 | /// If the move is a capture, ZobristToggleSquare needs to be done as well. 118 | /// 119 | public static void ZobristMove(this ref ulong hash, int from, int to, int color, int pt) 120 | { 121 | Assert(from is >= A1 and <= H8, $"ZobristMove({from}, {to}, {color}, {pt}) wasn't given a valid From square! (should be 0 <= idx <= 63)"); 122 | Assert(to is >= A1 and <= H8, $"ZobristMove({from}, {to}, {color}, {pt}) wasn't given a valid To square! (should be 0 <= idx <= 63)"); 123 | Assert(color is White or Black, $"ZobristMove({from}, {to}, {color}, {pt}) wasn't given a valid piece color! (should be 0 or 1)"); 124 | Assert(pt is >= Pawn and <= King, $"ZobristMove({from}, {to}, {color}, {pt}) wasn't given a valid piece type! (should be 0 <= pt <= 5)"); 125 | 126 | var fromIndex = ColorPieceSquareHashesIndex(color, pt, from); 127 | var toIndex = ColorPieceSquareHashesIndex(color, pt, to); 128 | ref var start = ref MemoryMarshal.GetArrayDataReference(ColorPieceSquareHashes); 129 | 130 | hash ^= Unsafe.Add(ref start, fromIndex) ^ Unsafe.Add(ref start, toIndex); 131 | } 132 | 133 | /// 134 | /// Adds or removes the piece of type and color at index 135 | /// 136 | public static void ZobristToggleSquare(this ref ulong hash, int color, int pt, int idx) 137 | { 138 | Assert(color is White or Black, $"ZobristToggleSquare({color}, {pt}, {idx}) wasn't given a valid piece color! (should be 0 or 1)"); 139 | Assert(pt is >= Pawn and <= King, $"ZobristToggleSquare({color}, {pt}, {idx}) wasn't given a valid piece type! (should be 0 <= pt <= 5)"); 140 | Assert(idx is >= A1 and <= H8, $"ZobristToggleSquare({color}, {pt}, {idx}) wasn't given a valid square! (should be 0 <= idx <= 63)"); 141 | 142 | var index = ColorPieceSquareHashesIndex(color, pt, idx); 143 | 144 | hash ^= Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(ColorPieceSquareHashes), index); 145 | } 146 | 147 | /// 148 | /// Updates the castling status of the hash, and doesn't change anything if the castling status hasn't changed 149 | /// 150 | public static void ZobristCastle(this ref ulong hash, CastlingStatus prev, CastlingStatus toRemove) 151 | { 152 | ulong change = (ulong)(prev & toRemove); 153 | while (change != 0) 154 | { 155 | hash ^= Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(CastlingRightsHashes), poplsb(&change)); 156 | } 157 | } 158 | 159 | /// 160 | /// Sets the En Passant status of the hash, which is set to the of the pawn that moved two squares previously 161 | /// 162 | public static void ZobristEnPassant(this ref ulong hash, int file) 163 | { 164 | hash ^= Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(EnPassantFileHashes), file); 165 | } 166 | 167 | /// 168 | /// Called each time White makes a move, which updates the hash to show that it's black to move now 169 | /// 170 | public static void ZobristChangeToMove(this ref ulong hash) 171 | { 172 | hash ^= BlackHash; 173 | } 174 | 175 | /// 176 | /// x 6 x 64 177 | /// 178 | private static int ColorPieceSquareHashesIndex(int color, int piece, int square) 179 | { 180 | const int colorOffset = 6 * 64; 181 | const int pieceOffset = 64; 182 | 183 | return (color * colorOffset) 184 | + (piece * pieceOffset) 185 | + square; 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Logic/Search/Ordering/MoveOrdering.cs: -------------------------------------------------------------------------------- 1 | 2 | using Lizard.Logic.Search.History; 3 | 4 | namespace Lizard.Logic.Search.Ordering 5 | { 6 | public static unsafe class MoveOrdering 7 | { 8 | 9 | /// 10 | /// Gives each of the pseudo-legal moves in the scores. 11 | /// 12 | /// The entry containing Killer moves to prioritize 13 | /// A reference to a with MainHistory/CaptureHistory scores. 14 | /// The retrieved from the TT probe, or Move.Null if the probe missed (ss->ttHit == false). 15 | public static void AssignScores(Position pos, SearchStackEntry* ss, in HistoryTable history, 16 | ScoredMove* list, int size, Move ttMove) 17 | { 18 | ref Bitboard bb = ref pos.bb; 19 | int pc = pos.ToMove; 20 | 21 | var pawnThreats = pos.ThreatsBy(Not(pc), Pawn); 22 | var minorThreats = pos.ThreatsBy(Not(pc), Knight) | pos.ThreatsBy(Not(pc), Bishop) | pawnThreats; 23 | var rookThreats = pos.ThreatsBy(Not(pc), Rook) | minorThreats; 24 | 25 | for (int i = 0; i < size; i++) 26 | { 27 | ref ScoredMove sm = ref list[i]; 28 | Move m = sm.Move; 29 | int moveTo = m.To; 30 | int moveFrom = m.From; 31 | int pt = bb.GetPieceAtIndex(moveFrom); 32 | 33 | if (m.Equals(ttMove)) 34 | { 35 | sm.Score = int.MaxValue - 1_000_000; 36 | } 37 | else if (m == ss->KillerMove) 38 | { 39 | sm.Score = int.MaxValue - 10_000_000; 40 | } 41 | else if (bb.GetPieceAtIndex(moveTo) != None && !m.IsCastle) 42 | { 43 | int capturedPiece = bb.GetPieceAtIndex(moveTo); 44 | sm.Score = (OrderingVictimMult * GetPieceValue(capturedPiece)) + 45 | (history.CaptureHistory[pc, pt, moveTo, capturedPiece]); 46 | } 47 | else 48 | { 49 | int contIdx = PieceToHistory.GetIndex(pc, pt, moveTo); 50 | 51 | sm.Score = 2 * history.MainHistory[pc, m]; 52 | sm.Score += 2 * (*(ss - 1)->ContinuationHistory)[contIdx]; 53 | sm.Score += (*(ss - 2)->ContinuationHistory)[contIdx]; 54 | sm.Score += (*(ss - 4)->ContinuationHistory)[contIdx]; 55 | sm.Score += (*(ss - 6)->ContinuationHistory)[contIdx]; 56 | 57 | if (ss->Ply < PlyHistoryTable.MaxPlies) 58 | { 59 | sm.Score += ((2 * PlyHistoryTable.MaxPlies + 1) * history.PlyHistory[ss->Ply, m]) / (2 * ss->Ply + 1); 60 | } 61 | 62 | if (pos.GivesCheck(pt, moveTo)) 63 | { 64 | sm.Score += OrderingCheckBonus; 65 | } 66 | 67 | int threat = 0; 68 | var fromBB = SquareBB[moveFrom]; 69 | var toBB = SquareBB[moveTo]; 70 | if (pt == Queen) 71 | { 72 | threat += ((fromBB & rookThreats) != 0) ? 12288 : 0; 73 | threat -= ((toBB & rookThreats) != 0) ? 11264 : 0; 74 | } 75 | else if (pt == Rook) 76 | { 77 | threat += ((fromBB & minorThreats) != 0) ? 10240 : 0; 78 | threat -= ((toBB & minorThreats) != 0) ? 9216 : 0; 79 | } 80 | else if (pt == Bishop || pt == Knight) 81 | { 82 | threat += ((fromBB & pawnThreats)!= 0) ? 8192 : 0; 83 | threat -= ((toBB & pawnThreats)!= 0) ? 7168 : 0; 84 | } 85 | 86 | list[i].Score += threat; 87 | } 88 | 89 | if (pt == Knight) 90 | { 91 | sm.Score += 200; 92 | } 93 | } 94 | } 95 | 96 | /// 97 | /// Gives each of the pseudo-legal moves in the scores, 98 | /// ignoring any killer moves placed in the entry. 99 | /// 100 | /// A reference to a with MainHistory/CaptureHistory scores. 101 | /// The retrieved from the TT probe, or Move.Null if the probe missed (ss->ttHit == false). 102 | public static void AssignQuiescenceScores(Position pos, SearchStackEntry* ss, in HistoryTable history, 103 | ScoredMove* list, int size, Move ttMove) 104 | { 105 | ref Bitboard bb = ref pos.bb; 106 | int pc = pos.ToMove; 107 | 108 | for (int i = 0; i < size; i++) 109 | { 110 | ref ScoredMove sm = ref list[i]; 111 | Move m = sm.Move; 112 | int moveTo = m.To; 113 | int moveFrom = m.From; 114 | int pt = bb.GetPieceAtIndex(moveFrom); 115 | 116 | if (m.Equals(ttMove)) 117 | { 118 | sm.Score = int.MaxValue - 1_000_000; 119 | } 120 | else if (bb.GetPieceAtIndex(moveTo) != None && !m.IsCastle) 121 | { 122 | int capturedPiece = bb.GetPieceAtIndex(moveTo); 123 | sm.Score = (OrderingVictimMult * GetPieceValue(capturedPiece)) + 124 | (history.CaptureHistory[pc, pt, moveTo, capturedPiece]); 125 | } 126 | else 127 | { 128 | int contIdx = PieceToHistory.GetIndex(pc, pt, moveTo); 129 | 130 | sm.Score = 2 * history.MainHistory[pc, m]; 131 | sm.Score += 2 * (*(ss - 1)->ContinuationHistory)[contIdx]; 132 | sm.Score += (*(ss - 2)->ContinuationHistory)[contIdx]; 133 | sm.Score += (*(ss - 4)->ContinuationHistory)[contIdx]; 134 | sm.Score += (*(ss - 6)->ContinuationHistory)[contIdx]; 135 | 136 | if (pos.GivesCheck(pt, moveTo)) 137 | { 138 | sm.Score += OrderingCheckBonus; 139 | } 140 | } 141 | 142 | if (pt == Knight) 143 | { 144 | sm.Score += 200; 145 | } 146 | } 147 | } 148 | 149 | /// 150 | /// Assigns scores to each of the moves in the . 151 | ///

152 | /// This is only called for ProbCut, so the only moves in should be generated 153 | /// using , which only generates captures and promotions. 154 | ///
155 | public static void AssignProbCutScores(ref Bitboard bb, ScoredMove* list, int size) 156 | { 157 | for (int i = 0; i < size; i++) 158 | { 159 | ref ScoredMove sm = ref list[i]; 160 | Move m = sm.Move; 161 | 162 | sm.Score = m.IsEnPassant ? Pawn : bb.GetPieceAtIndex(m.To); 163 | if (m.IsPromotion) 164 | { 165 | // Gives promotions a higher score than captures. 166 | // We can assume a queen promotion is better than most captures. 167 | sm.Score += 10; 168 | } 169 | } 170 | } 171 | 172 | 173 | /// 174 | /// Passes over the list of , bringing the move with the highest 175 | /// within the range of and to the front and returning it. 176 | /// 177 | public static Move OrderNextMove(ScoredMove* moves, int size, int listIndex) 178 | { 179 | int max = int.MinValue; 180 | int maxIndex = listIndex; 181 | 182 | for (int i = listIndex; i < size; i++) 183 | { 184 | if (moves[i].Score > max) 185 | { 186 | max = moves[i].Score; 187 | maxIndex = i; 188 | } 189 | } 190 | 191 | (moves[maxIndex], moves[listIndex]) = (moves[listIndex], moves[maxIndex]); 192 | 193 | return moves[listIndex].Move; 194 | } 195 | 196 | } 197 | 198 | 199 | } 200 | -------------------------------------------------------------------------------- /Logic/Data/Move.cs: -------------------------------------------------------------------------------- 1 | 2 | #pragma warning disable CS0660 // Type defines operator == or operator != but does not override Object.Equals(object o) 3 | #pragma warning disable CS0661 // Type defines operator == or operator != but does not override Object.GetHashCode() 4 | 5 | using System.Text; 6 | using System.Runtime.CompilerServices; 7 | 8 | namespace Lizard.Logic.Data 9 | { 10 | public unsafe readonly struct Move(int from, int to, int flags = 0) 11 | { 12 | public static readonly Move Null = new Move(); 13 | 14 | // 6 bits for From and To 15 | // 16 | // 2 bits for the EnPassant/Castle/Promotion flags 17 | // 2 bits for PromotionTo, which defaults to a knight (1), so the "Promotion" flag MUST be looked at before "PromotionTo" is. 18 | // (Otherwise every move would show up as a promotion to a knight, woohoo for horses!). 19 | private readonly ushort _data = (ushort)(to | (from << 6) | flags); 20 | 21 | 22 | [MethodImpl(Inline)] 23 | public ushort GetData() => _data; 24 | 25 | 26 | 27 | public const int FlagEnPassant = 0b0001 << 12; 28 | public const int FlagCastle = 0b0010 << 12; 29 | public const int FlagPromotion = 0b0011 << 12; 30 | 31 | private const int SpecialFlagsMask = 0b0011 << 12; 32 | 33 | public const int FlagPromoKnight = 0b00 << 14 | FlagPromotion; 34 | public const int FlagPromoBishop = 0b01 << 14 | FlagPromotion; 35 | public const int FlagPromoRook = 0b10 << 14 | FlagPromotion; 36 | public const int FlagPromoQueen = 0b11 << 14 | FlagPromotion; 37 | 38 | /// 39 | /// A mask of and 40 | /// 41 | private const int Mask_ToFrom = 0xFFF; 42 | 43 | 44 | 45 | public readonly int To => (_data >> 0) & 0x3F; 46 | public readonly int From => (_data >> 6) & 0x3F; 47 | 48 | public readonly int MoveMask => (_data & Mask_ToFrom); 49 | 50 | /// 51 | /// Gets the piece type that this pawn is promoting to. This is stored as (piece type - 1) to save space, 52 | /// so a PromotionTo == 0 (Piece.Pawn) is treated as 1 (Piece.Knight). 53 | /// 54 | public readonly int PromotionTo => ((_data >> 14) & 0x3) + 1; 55 | 56 | public readonly bool IsEnPassant => (_data & SpecialFlagsMask) == FlagEnPassant; 57 | public readonly bool IsCastle => (_data & SpecialFlagsMask) == FlagCastle; 58 | public readonly bool IsPromotion => (_data & SpecialFlagsMask) == FlagPromotion; 59 | 60 | [MethodImpl(Inline)] 61 | public readonly bool IsNull() => (_data & Mask_ToFrom) == 0; 62 | 63 | 64 | [MethodImpl(Inline)] 65 | public readonly int CastlingKingSquare() 66 | { 67 | if (From < A2) 68 | { 69 | return (To > From) ? G1 : C1; 70 | } 71 | 72 | return (To > From) ? G8 : C8; 73 | } 74 | 75 | [MethodImpl(Inline)] 76 | public readonly int CastlingRookSquare() 77 | { 78 | if (From < A2) 79 | { 80 | return (To > From) ? F1 : D1; 81 | } 82 | 83 | return (To > From) ? F8 : D8; 84 | } 85 | 86 | [MethodImpl(Inline)] 87 | public readonly CastlingStatus RelevantCastlingRight() 88 | { 89 | if (From < A2) 90 | { 91 | return (To > From) ? CastlingStatus.WK : CastlingStatus.WQ; 92 | } 93 | 94 | return (To > From) ? CastlingStatus.BK : CastlingStatus.BQ; 95 | } 96 | 97 | /// 98 | /// Returns the generic string representation of a move, which is just the move's From square, the To square, 99 | /// and the piece that the move is promoting to if applicable. 100 | ///

101 | /// For example, the opening moves "e4 e5, Nf3 Nc6, ..." would be "e2e4 e7e5, g1f3 b8c6, ..." 102 | ///
103 | public string SmithNotation(bool is960 = false) 104 | { 105 | IndexToCoord(From, out int fx, out int fy); 106 | IndexToCoord(To, out int tx, out int ty); 107 | 108 | if (IsCastle && !is960) 109 | { 110 | tx = (tx > fx) ? Files.G : Files.C; 111 | } 112 | 113 | if (IsPromotion) 114 | { 115 | return "" + GetFileChar(fx) + (fy + 1) + GetFileChar(tx) + (ty + 1) + char.ToLower(PieceToFENChar(PromotionTo)); 116 | } 117 | else 118 | { 119 | return "" + GetFileChar(fx) + (fy + 1) + GetFileChar(tx) + (ty + 1); 120 | } 121 | } 122 | 123 | public string ToString(Position position) 124 | { 125 | StringBuilder sb = new StringBuilder(); 126 | ref Bitboard bb = ref position.bb; 127 | 128 | int moveTo = To; 129 | int moveFrom = From; 130 | 131 | int pt = bb.GetPieceAtIndex(moveFrom); 132 | 133 | if (IsCastle) 134 | { 135 | if (moveTo > moveFrom) 136 | { 137 | sb.Append("O-O"); 138 | } 139 | else 140 | { 141 | sb.Append("O-O-O"); 142 | } 143 | } 144 | else 145 | { 146 | bool cap = bb.GetPieceAtIndex(moveTo) != None; 147 | 148 | if (pt == Piece.Pawn) 149 | { 150 | if (cap || IsEnPassant) 151 | { 152 | sb.Append(GetFileChar(GetIndexFile(moveFrom))); 153 | } 154 | } 155 | else 156 | { 157 | sb.Append(PieceToFENChar(pt)); 158 | } 159 | 160 | // If multiple of the same piece type can move to the same square, then we have to 161 | // differentiate them by including either the file, rank, or both, that this piece is moving from. 162 | ulong multPieces = bb.AttackersTo(moveTo, bb.Occupancy) & bb.Colors[bb.GetColorAtIndex(moveFrom)] & bb.Pieces[pt]; 163 | 164 | // This isn't done for pawns though since their notation is always unambiguous 165 | if (popcount(multPieces) > 1 && pt != Pawn) 166 | { 167 | if ((multPieces & GetFileBB(moveFrom)) == SquareBB[moveFrom]) 168 | { 169 | // If this piece is alone on its file, we only specify the file. 170 | sb.Append(GetFileChar(GetIndexFile(moveFrom))); 171 | } 172 | else if ((multPieces & GetRankBB(moveFrom)) == SquareBB[moveFrom]) 173 | { 174 | // If this piece wasn't alone on its file, but is alone on its rank, then include the rank. 175 | sb.Append(GetIndexRank(moveFrom) + 1); 176 | } 177 | else 178 | { 179 | // If neither the rank/file alone could differentiate this move, then we need both the file and rank 180 | sb.Append(GetFileChar(GetIndexFile(moveFrom))); 181 | sb.Append(GetIndexRank(moveFrom) + 1); 182 | } 183 | } 184 | 185 | if (cap || IsEnPassant) 186 | { 187 | sb.Append('x'); 188 | } 189 | 190 | 191 | sb.Append(IndexToString(moveTo)); 192 | 193 | if (IsPromotion) 194 | { 195 | sb.Append("=" + PieceToFENChar(PromotionTo)); 196 | } 197 | } 198 | 199 | return sb.ToString(); 200 | } 201 | 202 | public string ToString(bool is960 = false) 203 | { 204 | return SmithNotation(is960); 205 | } 206 | 207 | public override string ToString() 208 | { 209 | return ToString(false); 210 | } 211 | 212 | 213 | /// 214 | /// Returns true if the has the same From/To squares, the same "Castle" flag, and the same PromotionTo piece. 215 | /// 216 | [MethodImpl(Inline)] 217 | public bool Equals(Move move) 218 | { 219 | // Today we learned that the JIT doesn't appear to create separate paths for Equals(object) and Equals(Move/CondensedMove). 220 | // This meant that every time we did move.Equals(other) the generated IL had ~8 extra instructions, 221 | // plus a pair of lengthy "box/unbox" instructions since a Move is a value type being passes as an object. 222 | 223 | return (move.GetData() == GetData()); 224 | } 225 | 226 | 227 | 228 | [MethodImpl(Inline)] 229 | public bool Equals(ScoredMove move) 230 | { 231 | return move.Move.Equals(this); 232 | } 233 | 234 | 235 | public static bool operator ==(Move left, Move right) 236 | { 237 | return left.Equals(right); 238 | } 239 | 240 | public static bool operator !=(Move left, Move right) 241 | { 242 | return !left.Equals(right); 243 | } 244 | 245 | public static bool operator ==(Move left, ScoredMove right) 246 | { 247 | return left.Equals(right); 248 | } 249 | 250 | public static bool operator !=(Move left, ScoredMove right) 251 | { 252 | return !left.Equals(right); 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /Logic/Core/Bitboard.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Lizard.Logic.Core 4 | { 5 | /// 6 | /// Manages the bitboards for a position, which are 64-bit number arrays for each piece type and color. 7 | /// 8 | /// This implementation uses 6 ulongs for the 6 piece types, and 2 for White/Black. 9 | /// This struct also has an array for the piece type that exists on each square. 10 | /// 11 | public unsafe struct Bitboard 12 | { 13 | /// 14 | /// Bitboard array for Pieces, from Piece.Pawn to Piece.King 15 | /// 16 | public fixed ulong Pieces[6]; 17 | 18 | /// 19 | /// Bitboard array for Colors, from Color.White to Color.Black 20 | /// 21 | public fixed ulong Colors[2]; 22 | 23 | /// 24 | /// Piece array indexed by square 25 | /// 26 | public fixed int PieceTypes[64]; 27 | 28 | /// 29 | /// Mask of occupied squares, which is always equal to Colors[White] | Colors[Black] 30 | /// 31 | public ulong Occupancy = 0; 32 | 33 | public Bitboard() 34 | { 35 | Reset(); 36 | } 37 | 38 | public string SquareToString(int idx) 39 | { 40 | return ColorToString(GetColorAtIndex(idx)) + " " + 41 | PieceToString(PieceTypes[idx]) + " on " + 42 | IndexToString(idx); 43 | } 44 | 45 | /// 46 | /// 0's the Piece and Color arrays and fills the PieceType array with Piece.None . 47 | /// 48 | public void Reset() 49 | { 50 | for (int i = 0; i < PieceNB; i++) 51 | { 52 | Pieces[i] = 0UL; 53 | } 54 | 55 | for (int i = 0; i < ColorNB; i++) 56 | { 57 | Colors[i] = 0UL; 58 | } 59 | 60 | Occupancy = 0UL; 61 | 62 | for (int i = 0; i < SquareNB; i++) 63 | { 64 | PieceTypes[i] = Piece.None; 65 | } 66 | } 67 | 68 | public void CopyTo(ref Bitboard other) 69 | { 70 | fixed (ulong* srcPieces = Pieces, dstPieces = other.Pieces) 71 | { 72 | Unsafe.CopyBlock(dstPieces, srcPieces, sizeof(ulong) * PieceNB); 73 | } 74 | 75 | fixed (ulong* srcColors = Colors, dstColors = other.Colors) 76 | { 77 | Unsafe.CopyBlock(dstColors, srcColors, sizeof(ulong) * ColorNB); 78 | } 79 | } 80 | 81 | 82 | /// 83 | /// Adds a piece of type and color on the square . 84 | /// 85 | public void AddPiece(int idx, int pc, int pt) 86 | { 87 | PieceTypes[idx] = pt; 88 | 89 | Assert((Colors[pc] & SquareBB[idx]) == 0, $"{ColorToString(pc)} already has a piece on the square {IndexToString(idx)}"); 90 | Assert((Pieces[pt] & SquareBB[idx]) == 0, $"A {PieceToString(pt)} already exists on the square {IndexToString(idx)}"); 91 | 92 | Colors[pc] ^= SquareBB[idx]; 93 | Pieces[pt] ^= SquareBB[idx]; 94 | 95 | Occupancy |= SquareBB[idx]; 96 | } 97 | 98 | /// 99 | /// Removes the piece of type and color on the square . 100 | /// 101 | public void RemovePiece(int idx, int pc, int pt) 102 | { 103 | PieceTypes[idx] = Piece.None; 104 | 105 | Assert((Colors[pc] & SquareBB[idx]) != 0, $"{ColorToString(pc)} doesn't have a piece to remove on the square {IndexToString(idx)}"); 106 | Assert((Pieces[pt] & SquareBB[idx]) != 0, $"The square {IndexToString(idx)} doesn't have a {PieceToString(pt)} to remove"); 107 | 108 | Colors[pc] ^= SquareBB[idx]; 109 | Pieces[pt] ^= SquareBB[idx]; 110 | 111 | Occupancy ^= SquareBB[idx]; 112 | } 113 | 114 | /// 115 | /// Moves the piece at index to index , where is an empty square. 116 | /// 117 | /// The square the piece is moving from 118 | /// The square the piece is moving to 119 | /// The color of the piece that is moving 120 | /// The type of the piece that is moving 121 | public void MoveSimple(int from, int to, int pieceColor, int pieceType) 122 | { 123 | RemovePiece(from, pieceColor, pieceType); 124 | AddPiece(to, pieceColor, pieceType); 125 | } 126 | 127 | /// 128 | /// Returns the of the piece on the square 129 | /// 130 | [MethodImpl(Inline)] 131 | public int GetColorAtIndex(int idx) 132 | { 133 | return ((Colors[Color.White] & SquareBB[idx]) != 0) ? Color.White : Color.Black; 134 | } 135 | 136 | /// 137 | /// Returns the type of the on the square 138 | /// 139 | [MethodImpl(Inline)] 140 | public int GetPieceAtIndex(int idx) 141 | { 142 | return PieceTypes[idx]; 143 | } 144 | 145 | 146 | /// 147 | /// Returns the index of the square that the 's king is on. 148 | /// 149 | [MethodImpl(Inline)] 150 | public int KingIndex(int pc) 151 | { 152 | return lsb(Colors[pc] & Pieces[Piece.King]); 153 | } 154 | 155 | 156 | /// 157 | /// Returns a mask of the pieces 158 | /// 159 | /// is a mask of the other side's pieces that would be 160 | /// putting 's king in check if a blocker of color wasn't in the way 161 | /// 162 | public ulong BlockingPieces(int pc, ulong* pinners) 163 | { 164 | ulong blockers = 0UL; 165 | *pinners = 0; 166 | 167 | ulong temp; 168 | ulong us = Colors[pc]; 169 | ulong them = Colors[Not(pc)]; 170 | 171 | int ourKing = KingIndex(pc); 172 | 173 | // Candidates are their pieces that are on the same rank/file/diagonal as our king. 174 | ulong candidates = them & ((RookRays[ourKing] & (Pieces[Queen] | Pieces[Rook])) | 175 | (BishopRays[ourKing] & (Pieces[Queen] | Pieces[Bishop]))); 176 | 177 | ulong occ = us | them; 178 | 179 | while (candidates != 0) 180 | { 181 | int idx = poplsb(&candidates); 182 | 183 | temp = BetweenBB[ourKing][idx] & occ; 184 | 185 | if (temp != 0 && !MoreThanOne(temp)) 186 | { 187 | // If there is one and only one piece between the candidate and our king, that piece is a blocker 188 | blockers |= temp; 189 | 190 | if ((temp & us) != 0) 191 | { 192 | // If the blocker is ours, then the candidate on the square "idx" is a pinner 193 | *pinners |= SquareBB[idx]; 194 | } 195 | } 196 | } 197 | 198 | return blockers; 199 | } 200 | 201 | /// 202 | /// Returns a ulong with bits set at the positions of any piece that can attack the square , 203 | /// given the board occupancy . 204 | /// 205 | [MethodImpl(Inline)] 206 | public ulong AttackersTo(int idx, ulong occupied) 207 | { 208 | return (GetBishopMoves(occupied, idx) & (Pieces[Bishop] | Pieces[Queen])) 209 | | (GetRookMoves(occupied, idx) & (Pieces[Rook] | Pieces[Queen])) 210 | | (KnightMasks[idx] & Pieces[Knight]) 211 | | (WhitePawnAttackMasks[idx] & Colors[Black] & Pieces[Pawn]) 212 | | (BlackPawnAttackMasks[idx] & Colors[White] & Pieces[Pawn]); 213 | } 214 | 215 | 216 | /// 217 | /// Returns a mask of the squares that a piece of type and color 218 | /// on the square attacks, given the board occupancy 219 | /// 220 | [MethodImpl(Inline)] 221 | public ulong AttackMask(int idx, int pc, int pt, ulong occupied) 222 | { 223 | return pt switch 224 | { 225 | Pawn => PawnAttackMasks[pc][idx], 226 | Knight => KnightMasks[idx], 227 | Bishop => GetBishopMoves(occupied, idx), 228 | Rook => GetRookMoves(occupied, idx), 229 | Queen => GetBishopMoves(occupied, idx) | GetRookMoves(occupied, idx), 230 | King => NeighborsMask[idx], 231 | _ => 0, 232 | }; 233 | } 234 | 235 | 236 | public ulong ThreatsBy(int pc, int pt) { 237 | ulong mask = 0; 238 | var pieces = Pieces[pt] & Colors[pc]; 239 | while (pieces != 0) 240 | mask |= AttackMask(poplsb(&pieces), pc, pt, Occupancy); 241 | 242 | return mask; 243 | } 244 | 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /Logic/Transposition/TranspositionTable.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Lizard.Logic.Transposition 5 | { 6 | public unsafe class TranspositionTable 7 | { 8 | public const int TT_BOUND_MASK = 0x3; 9 | public const int TT_PV_MASK = 0x4; 10 | public const int TT_AGE_MASK = 0xF8; 11 | public const int TT_AGE_INC = 0x8; 12 | public const int TT_AGE_CYCLE = 255 + TT_AGE_INC; 13 | 14 | /// 15 | /// The minimum number of TTClusters for hashfull to work properly. 16 | /// 17 | private const int MinTTClusters = 1000; 18 | 19 | /// 20 | /// The minimum number of TTEntry's within a TTCluster. 21 | /// 22 | public const int EntriesPerCluster = 3; 23 | 24 | private TTCluster* Clusters; 25 | public ulong ClusterCount { get; private set; } 26 | 27 | public byte Age = 0; 28 | 29 | public TranspositionTable(int mb) 30 | { 31 | Initialize(mb); 32 | } 33 | 34 | /// 35 | /// Allocates megabytes of memory for the Transposition Table, and zeroes out each entry. 36 | /// 37 | /// 1 mb fits 32,768 clusters, which is 98,304 TTEntry's. 38 | /// 39 | public unsafe void Initialize(int mb) 40 | { 41 | if (Clusters != null) 42 | NativeMemory.AlignedFree(Clusters); 43 | 44 | ClusterCount = (ulong)mb * 0x100000UL / (ulong)sizeof(TTCluster); 45 | nuint allocSize = ((nuint)sizeof(TTCluster) * (nuint)ClusterCount); 46 | 47 | // On Linux, also inform the OS that we want it to use large pages 48 | Clusters = AlignedAllocZeroed((nuint)ClusterCount, (1024 * 1024)); 49 | AdviseHugePage(Clusters, allocSize); 50 | } 51 | 52 | /// 53 | /// Reinitializes each within the table. 54 | /// 55 | public void Clear() 56 | { 57 | int numThreads = SearchOptions.Threads; 58 | ulong clustersPerThread = (ClusterCount / (ulong)numThreads); 59 | 60 | Parallel.For(0, numThreads, new ParallelOptions { MaxDegreeOfParallelism = numThreads }, (int i) => 61 | { 62 | ulong start = clustersPerThread * (ulong)i; 63 | 64 | // Only clear however many remaining clusters there are if this is the last thread 65 | ulong length = (i == numThreads - 1) ? ClusterCount - start : clustersPerThread; 66 | 67 | NativeMemory.Clear(&Clusters[start], ((nuint)sizeof(TTCluster) * (nuint)length)); 68 | }); 69 | 70 | Age = 0; 71 | } 72 | 73 | /// 74 | /// Returns a pointer to the that the maps to. 75 | /// 76 | [MethodImpl(Inline)] 77 | public TTCluster* GetCluster(ulong hash) 78 | { 79 | return Clusters + ((ulong)(((UInt128)hash * (UInt128)ClusterCount) >> 64)); 80 | } 81 | 82 | 83 | /// 84 | /// Sets to the address of a . 85 | /// 86 | /// If the that the maps to contains an entry, 87 | /// then is the address of that entry and this method returns true, signifying that this was a TT Hit. 88 | ///

89 | /// Otherwise, this method sets to the address of the within the cluster that should 90 | /// be overwritten with new information, and this method returns false. 91 | ///
92 | public bool Probe(ulong hash, out TTEntry* tte) 93 | { 94 | TTCluster* cluster = GetCluster(hash); 95 | tte = (TTEntry*)cluster; 96 | 97 | var key = (ushort)hash; 98 | 99 | for (int i = 0; i < EntriesPerCluster; i++) 100 | { 101 | // If the entry's key matches, or the entry is empty, then pick this one. 102 | if (tte[i].Key == key || tte[i].IsEmpty) 103 | { 104 | tte = &tte[i]; 105 | 106 | // We return true if the entry isn't empty, which means that tte is valid. 107 | // Check tte[0] here, not tte[i]. 108 | return !tte[0].IsEmpty; 109 | } 110 | } 111 | 112 | // We didn't find an entry for this hash, so instead we will choose one of the 113 | // non-working entries in this cluster to possibly be overwritten / updated, and return false. 114 | 115 | // Replace the first entry, unless the 2nd or 3rd is a better option. 116 | TTEntry* replace = tte; 117 | for (int i = 1; i < EntriesPerCluster; i++) 118 | { 119 | if ((replace->RawDepth - replace->RelAge(Age)) > 120 | ( tte[i].RawDepth - tte[i].RelAge(Age))) 121 | { 122 | replace = &tte[i]; 123 | } 124 | } 125 | 126 | tte = replace; 127 | return false; 128 | } 129 | 130 | 131 | 132 | 133 | /// 134 | /// Increases the age that TT entries must have to be considered a "TT Hit". 135 | ///

136 | /// This is done on every call to to prevent the transposition table 137 | /// from spilling into another search. 138 | ///
139 | public void TTUpdate() 140 | { 141 | Age += TT_AGE_INC; 142 | } 143 | 144 | 145 | /// 146 | /// Returns the "hashfull" for the TT, which is an estimation of how many valid entries are present. 147 | ///

148 | /// This returns the number of recent entries (which have a == ) 149 | /// present in the first thousand TTClusters. 150 | /// 151 | /// A hashfull of 400 means that there were 1200 TTEntry's with the correct age out of the first 3000, so we can estimate that 152 | /// about 40% of the entire TT has valid entries in it. 153 | ///
154 | public int GetHashFull() 155 | { 156 | int entries = 0; 157 | 158 | for (int i = 0; i < MinTTClusters; i++) 159 | { 160 | TTEntry* cluster = (TTEntry*)&Clusters[i]; 161 | 162 | for (int j = 0; j < EntriesPerCluster; j++) 163 | { 164 | if (!cluster[j].IsEmpty && (cluster[j].Age) == (Age & TT_AGE_MASK)) 165 | { 166 | entries++; 167 | } 168 | } 169 | } 170 | return entries / EntriesPerCluster; 171 | } 172 | 173 | /// 174 | /// Prints statistics about the state of the TT, such as how many nodes of each type are present. 175 | /// 176 | public void PrintClusterStatus() 177 | { 178 | int recentEntries = 0; 179 | int Beta = 0; 180 | int Alpha = 0; 181 | int Exact = 0; 182 | int Invalid = 0; 183 | 184 | int NullMoves = 0; 185 | 186 | int[] slots = new int[3]; 187 | 188 | for (ulong i = 0; i < ClusterCount; i++) 189 | { 190 | TTEntry* cluster = (TTEntry*)&Clusters[i]; 191 | for (int j = 0; j < EntriesPerCluster; j++) 192 | { 193 | var tt = cluster[j]; 194 | if (tt.NodeType == TTNodeType.Beta) 195 | { 196 | Beta++; 197 | } 198 | else if (tt.NodeType == TTNodeType.Alpha) 199 | { 200 | Alpha++; 201 | } 202 | else if (tt.NodeType == TTNodeType.Exact) 203 | { 204 | Exact++; 205 | } 206 | else 207 | { 208 | Invalid++; 209 | } 210 | 211 | if (tt.NodeType != TTNodeType.Invalid) 212 | { 213 | slots[j]++; 214 | } 215 | 216 | if (tt.BestMove.IsNull() && tt.NodeType != TTNodeType.Invalid) 217 | { 218 | NullMoves++; 219 | } 220 | 221 | if (tt.Age == Age) 222 | { 223 | recentEntries++; 224 | } 225 | } 226 | } 227 | 228 | int entries = Beta + Alpha + Exact; 229 | 230 | // "Full" is the total number of entries of any age in the TT. 231 | Log($"Full:\t {entries} / {ClusterCount * EntriesPerCluster} = {(double)entries / (ClusterCount * EntriesPerCluster) * 100}%"); 232 | 233 | // "Recent" is the number of entries that have the same age as the TT's Age. 234 | Log($"Recent:\t {recentEntries} /{ClusterCount * EntriesPerCluster} = {(double)recentEntries / (ClusterCount * EntriesPerCluster) * 100}%"); 235 | 236 | // "Slots[0,1,2]" are the number of entries that exist in each TTCluster slot 237 | Log($"Slots:\t {slots[0]} / {slots[1]} / {slots[2]}"); 238 | Log($"Alpha:\t {Alpha}"); 239 | Log($"Beta:\t {Beta}"); 240 | Log($"Exact:\t {Exact}"); 241 | Log($"Invalid: {Alpha}"); 242 | Log($"Null:\t {NullMoves}"); 243 | } 244 | } 245 | } 246 | --------------------------------------------------------------------------------