├── genmany.sh ├── nets ├── bingshan.nnue └── xuebeng.nnue ├── Makefile ├── scripts ├── score_games.sh ├── convert_json.py └── score_games.py ├── .gitignore ├── gen.sh ├── src ├── engine │ ├── weights.zig │ ├── parameters.zig │ ├── tt.zig │ ├── bench.zig │ ├── movepick.zig │ ├── see.zig │ ├── nnue.zig │ ├── datagen.zig │ ├── hce.zig │ ├── interface.zig │ └── search.zig ├── chess │ ├── utils.zig │ ├── zobrist.zig │ ├── perft.zig │ ├── types.zig │ └── tables.zig ├── main.zig └── tests.zig ├── LICENSE ├── .github └── workflows │ └── CI.yml ├── README.md ├── CHANGELOG.md └── nets.txt /genmany.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | parallel -X --ungroup -j 7 bash ./gen.sh ::: {1..7} -------------------------------------------------------------------------------- /nets/bingshan.nnue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnowballSH/Avalanche/HEAD/nets/bingshan.nnue -------------------------------------------------------------------------------- /nets/xuebeng.nnue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnowballSH/Avalanche/HEAD/nets/xuebeng.nnue -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := default 2 | 3 | MV=mv bin/Avalanche $(EXE) 4 | 5 | default: 6 | zig build -Drelease-fast --prefix ./ -Dtarget-name="Avalanche" 7 | 8 | ifdef EXE 9 | $(MV) 10 | endif -------------------------------------------------------------------------------- /scripts/score_games.sh: -------------------------------------------------------------------------------- 1 | (trap 'kill 0' SIGINT; \ 2 | python3 ./score_games.py ./games/t1 ./t1.txt & \ 3 | python3 ./score_games.py ./games/t2 ./t2.txt & \ 4 | python3 ./score_games.py ./games/t3 ./t3.txt & \ 5 | python3 ./score_games.py ./games/t4 ./t4.txt & \ 6 | python3 ./score_games.py ./games/t5 ./t5.txt & \ 7 | wait) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/zig-cache 2 | **/zig-out 3 | **/artifacts 4 | .DS_Store 5 | **/*.bin 6 | **/*.pgn 7 | **/*.bson 8 | *.epd 9 | 10 | # Private for now 11 | marlinflow/**/* 12 | marlinflow 13 | lichess-bot 14 | zig/ 15 | .vscode 16 | old_binaries 17 | 18 | weather-factory/**/* 19 | weather-factory 20 | 21 | testing/ 22 | cutechess-cli 23 | 24 | *.json -------------------------------------------------------------------------------- /gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | success=false 4 | attempt_num=1 5 | 6 | while [ $success = false ]; do 7 | ./zig-out/bin/Avalanche datagen_single 8 | 9 | if [ $? -eq 0 ]; then 10 | success=true 11 | else 12 | echo "Attempt $attempt_num failed. Trying again..." 13 | sleep 2 14 | attempt_num=$(( attempt_num + 1 )) 15 | fi 16 | done 17 | -------------------------------------------------------------------------------- /src/engine/weights.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const NNUE_SOURCE = @embedFile("../../nets/bingshan.nnue"); 4 | 5 | pub const INPUT_SIZE: usize = 768; 6 | pub const HIDDEN_SIZE: usize = 512; 7 | pub const OUTPUT_SIZE: usize = 8; 8 | 9 | pub const NNUEWeights = struct { 10 | layer_1: [INPUT_SIZE * HIDDEN_SIZE]i16 align(64), 11 | layer_1_bias: [HIDDEN_SIZE]i16 align(64), 12 | layer_2: [OUTPUT_SIZE][HIDDEN_SIZE * 2]i16 align(64), 13 | layer_2_bias: [OUTPUT_SIZE]i16 align(64), 14 | }; 15 | 16 | pub var MODEL: NNUEWeights = undefined; 17 | 18 | pub fn do_nnue() void { 19 | if (@sizeOf(NNUEWeights) != NNUE_SOURCE.len) { 20 | std.debug.panic("Incompatible sizes Model={} vs Net={}", .{ @sizeOf(NNUEWeights), NNUE_SOURCE.len }); 21 | } 22 | MODEL = std.mem.bytesAsValue(NNUEWeights, NNUE_SOURCE[0..@sizeOf(NNUEWeights)]).*; 23 | } 24 | -------------------------------------------------------------------------------- /src/chess/utils.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | // Pseudo-random Number Generator 4 | pub const PRNG = struct { 5 | seed: u128, 6 | 7 | pub fn rand64(self: *PRNG) u64 { 8 | var x = self.seed; 9 | x ^= x >> 12; 10 | x ^= x << 25; 11 | x ^= x >> 27; 12 | self.seed = x; 13 | var r = @truncate(u64, x); 14 | r = r ^ @truncate(u64, x >> 64); 15 | return r; 16 | } 17 | 18 | // Less bits 19 | pub fn sparse_rand64(self: *PRNG) u64 { 20 | return self.rand64() & self.rand64() & self.rand64(); 21 | } 22 | 23 | pub fn new(seed: u128) PRNG { 24 | return PRNG{ .seed = seed }; 25 | } 26 | }; 27 | 28 | pub fn first_index(comptime T: type, arr: []const T, val: T) ?usize { 29 | var i: usize = 0; 30 | var end = arr.len; 31 | while (i < end) : (i += 1) { 32 | if (arr[i] == val) { 33 | return i; 34 | } 35 | } 36 | return null; 37 | } 38 | -------------------------------------------------------------------------------- /src/chess/zobrist.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const types = @import("types.zig"); 3 | const utils = @import("utils.zig"); 4 | 5 | pub var ZobristTable: [types.N_PIECES][types.N_SQUARES]u64 = std.mem.zeroes([types.N_PIECES][types.N_SQUARES]u64); 6 | pub var TurnHash: u64 = 0; 7 | pub var EnPassantHash: [8]u64 = std.mem.zeroes([8]u64); 8 | pub var DepthHash: [64]u64 = std.mem.zeroes([64]u64); 9 | 10 | pub fn init_zobrist() void { 11 | var prng = utils.PRNG.new(0x246C_CB2D_3B40_2853_9918_0A6D_BC3A_F444); 12 | var i: usize = 0; 13 | while (i < types.N_PIECES - 1) : (i += 1) { 14 | var j: usize = 0; 15 | while (j < types.N_SQUARES) : (j += 1) { 16 | ZobristTable[i][j] = prng.rand64(); 17 | } 18 | } 19 | TurnHash = prng.rand64(); 20 | 21 | var l: usize = 0; 22 | while (l < 8) : (l += 1) { 23 | EnPassantHash[l] = prng.rand64(); 24 | } 25 | 26 | var k: usize = 0; 27 | while (k < 64) : (k += 1) { 28 | DepthHash[k] = prng.rand64(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Yinuo Huang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | on: 6 | push: 7 | branches: [master] 8 | schedule: 9 | - cron: "0 0 * * *" #Makes sense, we are testing against master 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-13, windows-latest] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 20 | - uses: actions/checkout@v3 21 | - name: Setup Zig 22 | # You may pin to the exact commit or the version. 23 | # uses: goto-bus-stop/setup-zig@41ae19e72e21b9a1380e86ff9f058db709fc8fc6 24 | uses: goto-bus-stop/setup-zig@v2 25 | with: 26 | version: 0.10.1 27 | 28 | - run: zig version 29 | - run: zig env 30 | 31 | - name: Debug Build 32 | run: zig build 33 | 34 | - name: Release Build 35 | run: zig build -Drelease-fast 36 | 37 | - name: Bench 38 | run: ./zig-out/bin/Avalanche bench 39 | 40 | - name: Build artifacts 41 | if: ${{ matrix.os == 'ubuntu-latest' }} 42 | run: | 43 | chmod +x build_all_v2.sh 44 | ./build_all_v2.sh 45 | - name: Upload artifacts 46 | if: ${{ matrix.os == 'ubuntu-latest' }} 47 | uses: actions/upload-artifact@v3 48 | with: 49 | name: builds 50 | path: artifacts/*.zip 51 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const types = @import("chess/types.zig"); 3 | const tables = @import("chess/tables.zig"); 4 | const zobrist = @import("chess/zobrist.zig"); 5 | const position = @import("chess/position.zig"); 6 | const search = @import("engine/search.zig"); 7 | const tt = @import("engine/tt.zig"); 8 | const interface = @import("engine/interface.zig"); 9 | const weights = @import("engine/weights.zig"); 10 | const bench = @import("engine/bench.zig"); 11 | const datagen = @import("engine/datagen.zig"); 12 | 13 | const arch = @import("build_options"); 14 | 15 | pub fn main() anyerror!void { 16 | tables.init_all(); 17 | zobrist.init_zobrist(); 18 | tt.GlobalTT.reset(16); 19 | defer tt.GlobalTT.data.deinit(); 20 | weights.do_nnue(); 21 | search.init_lmr(); 22 | 23 | var args = try std.process.argsWithAllocator(std.heap.page_allocator); 24 | 25 | _ = args.next(); 26 | var second = args.next(); 27 | if (second != null) { 28 | if (std.mem.eql(u8, second.?, "bench")) { 29 | try bench.bench(); 30 | return; 31 | } 32 | if (std.mem.eql(u8, second.?, "datagen")) { 33 | var gen = datagen.Datagen.new(); 34 | defer gen.deinit(); 35 | 36 | tt.LOCK_GLOBAL_TT = true; 37 | tt.GlobalTT.reset(512); 38 | try gen.start(7); 39 | return; 40 | } 41 | 42 | if (std.mem.eql(u8, second.?, "datagen_single")) { 43 | var gen = datagen.Datagen.new(); 44 | defer gen.deinit(); 45 | 46 | tt.LOCK_GLOBAL_TT = true; 47 | tt.GlobalTT.reset(256); 48 | try gen.startSingleThreaded(); 49 | return; 50 | } 51 | } 52 | 53 | var inter = interface.UciInterface.new(); 54 | return inter.main_loop(); 55 | } 56 | -------------------------------------------------------------------------------- /src/engine/parameters.zig: -------------------------------------------------------------------------------- 1 | pub var LMRWeight: f64 = 0.429; 2 | pub var LMRBias: f64 = 0.769; 3 | 4 | pub var RFPDepth: i32 = 8; 5 | pub var RFPMultiplier: i32 = 58; 6 | pub var RFPImprovingDeduction: i32 = 69; 7 | 8 | pub var NMPImprovingMargin: i32 = 72; 9 | pub var NMPBase: usize = 3; 10 | pub var NMPDepthDivisor: usize = 3; 11 | pub var NMPBetaDivisor: i32 = 206; 12 | 13 | pub var RazoringBase: i32 = 68; 14 | pub var RazoringMargin: i32 = 191; 15 | 16 | pub var AspirationWindow: i32 = 11; 17 | 18 | pub const Tunable = struct { 19 | name: []const u8, 20 | value: []const u8, 21 | min_value: []const u8, 22 | max_value: []const u8, 23 | id: usize, 24 | }; 25 | 26 | pub const TunableParams = [_]Tunable{ 27 | Tunable{ .name = "LMRWeight", .value = "429", .min_value = "1", .max_value = "999", .id = 0 }, 28 | Tunable{ .name = "LMRBias", .value = "769", .min_value = "1", .max_value = "9999", .id = 1 }, 29 | Tunable{ .name = "RFPDepth", .value = "8", .min_value = "1", .max_value = "16", .id = 2 }, 30 | Tunable{ .name = "RFPMultiplier", .value = "58", .min_value = "1", .max_value = "999", .id = 3 }, 31 | Tunable{ .name = "RFPImprovingDeduction", .value = "69", .min_value = "1", .max_value = "999", .id = 4 }, 32 | Tunable{ .name = "NMPImprovingMargin", .value = "72", .min_value = "1", .max_value = "999", .id = 5 }, 33 | Tunable{ .name = "NMPBase", .value = "3", .min_value = "1", .max_value = "16", .id = 6 }, 34 | Tunable{ .name = "NMPDepthDivisor", .value = "3", .min_value = "1", .max_value = "16", .id = 7 }, 35 | Tunable{ .name = "NMPBetaDivisor", .value = "206", .min_value = "1", .max_value = "999", .id = 8 }, 36 | Tunable{ .name = "RazoringBase", .value = "68", .min_value = "1", .max_value = "999", .id = 9 }, 37 | Tunable{ .name = "RazoringMargin", .value = "191", .min_value = "1", .max_value = "999", .id = 10 }, 38 | Tunable{ .name = "AspirationWindow", .value = "11", .min_value = "1", .max_value = "999", .id = 11 }, 39 | }; 40 | -------------------------------------------------------------------------------- /scripts/convert_json.py: -------------------------------------------------------------------------------- 1 | # Script modified from Carp https://github.com/dede1751/carp 2 | # https://github.com/dede1751/carp/blob/1fe26d7092fdc776226506cd54e0dcebb807861d/src/engine/nnue/convert_json.py 3 | 4 | import sys 5 | import json 6 | import struct 7 | 8 | FEATURES = 768 9 | HIDDEN = 256 10 | QA = 255 11 | QB = 64 12 | QAB = QA * QB 13 | PARAM_SIZE = 2 # param size in bytes 14 | 15 | 16 | def write_bytes(array): 17 | with open("net.nnue", "ab") as file: 18 | for num in array: 19 | file.write(struct.pack("") 56 | sys.exit(1) 57 | 58 | json_file = sys.argv[1] 59 | with open(json_file, "r") as file: 60 | data = json.load(file) 61 | 62 | feature_weights = convert_weight( 63 | data["perspective.weight"], HIDDEN, HIDDEN * FEATURES, QA, True 64 | ) 65 | feature_biases = convert_bias(data["perspective.bias"], QA) 66 | output_weights = convert_weight( 67 | data["out.weight"], HIDDEN * 2, HIDDEN * 2, QB, False) 68 | output_biases = convert_bias(data["out.bias"], QAB) 69 | 70 | # Clear the old net and write the new data (ordering is important!) 71 | open("net.nnue", "w").close() 72 | write_bytes(feature_weights) 73 | write_bytes(feature_biases) 74 | write_bytes(output_weights) 75 | write_bytes(output_biases) 76 | -------------------------------------------------------------------------------- /scripts/score_games.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import chess 3 | import chess.engine 4 | import chess.pgn 5 | import os 6 | 7 | 8 | # Function to process a single game 9 | def process_game(game: chess.pgn.Game): 10 | board = game.board() 11 | fen_eval_result = [] 12 | 13 | # Setup the engine 14 | with chess.engine.SimpleEngine.popen_uci("../old_binaries/Avalanche") as engine: 15 | ply = 0 16 | for move in game.mainline_moves(): 17 | # Check the conditions specified 18 | if not ( 19 | ply <= 7 20 | or board.is_check() 21 | or board.is_capture(move) 22 | or move.promotion is not None 23 | or board.gives_check(move) 24 | ): 25 | # Run the engine at depth 8 26 | info = engine.analyse(board, chess.engine.Limit(depth=8)) 27 | score = info["score"].relative.score() 28 | if score is not None: 29 | if board.turn == chess.BLACK: 30 | score = -score 31 | fen_eval_result.append((board.fen(), score, None)) 32 | 33 | board.push(move) 34 | ply += 1 35 | 36 | # Update the result field for the positions 37 | result = game.headers["Result"] 38 | if result == "1-0": 39 | updated_result = 1.0 40 | elif result == "0-1": 41 | updated_result = 0.0 42 | else: 43 | updated_result = 0.5 44 | 45 | fen_eval_result = [ 46 | (fen, eval_, updated_result) for fen, eval_, _ in fen_eval_result 47 | ] 48 | 49 | return fen_eval_result 50 | 51 | 52 | def main(): 53 | directory_path = sys.argv[1] 54 | 55 | with open(sys.argv[2], "w") as file: 56 | pass # Clear the file 57 | 58 | # Open data.txt for appending 59 | with open(sys.argv[2], "a") as file: 60 | # Iterate over each file in the directory 61 | for filename in os.listdir(directory_path): 62 | if filename.endswith(".pgn"): 63 | with open(os.path.join(directory_path, filename)) as pgn_file: 64 | while True: 65 | game = chess.pgn.read_game(pgn_file) 66 | if game is None: 67 | break # Reached end of file 68 | fen_eval_results = process_game(game) 69 | for fen, eval_, result in fen_eval_results: 70 | file.write(f"{fen} | {eval_} | {result}\n") 71 | 72 | 73 | if __name__ == "__main__": 74 | main() 75 | -------------------------------------------------------------------------------- /src/chess/perft.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const types = @import("types.zig"); 3 | const tables = @import("tables.zig"); 4 | const zobrist = @import("zobrist.zig"); 5 | const position = @import("position.zig"); 6 | 7 | pub fn perft(comptime color: types.Color, pos: *position.Position, depth: u32) usize { 8 | if (depth == 0) { 9 | return 1; 10 | } 11 | 12 | var nodes: usize = 0; 13 | comptime var opp = if (color == types.Color.White) types.Color.Black else types.Color.White; 14 | 15 | var list = std.ArrayList(types.Move).initCapacity(std.heap.c_allocator, 48) catch unreachable; 16 | defer list.deinit(); 17 | 18 | pos.generate_legal_moves(color, &list); 19 | if (depth == 1) { 20 | return @intCast(usize, list.items.len); 21 | } 22 | 23 | for (list.items) |move| { 24 | pos.play_move(color, move); 25 | nodes += perft(opp, pos, depth - 1); 26 | pos.undo_move(color, move); 27 | } 28 | 29 | return nodes; 30 | } 31 | 32 | pub fn perft_div(comptime color: types.Color, pos: *position.Position, depth: u32) void { 33 | var nodes: usize = 0; 34 | var branch: usize = 0; 35 | comptime var opp = if (color == types.Color.White) types.Color.Black else types.Color.White; 36 | 37 | var list = std.ArrayList(types.Move).initCapacity(std.heap.c_allocator, 48) catch unreachable; 38 | defer list.deinit(); 39 | 40 | pos.generate_legal_moves(color, &list); 41 | 42 | for (list.items) |move| { 43 | pos.play_move(color, move); 44 | branch = perft(opp, pos, depth - 1); 45 | nodes += branch; 46 | pos.undo_move(color, move); 47 | 48 | move.debug_print(); 49 | std.debug.print(": {}\n", .{branch}); 50 | } 51 | 52 | std.debug.print("\nTotal: {}\n", .{nodes}); 53 | } 54 | 55 | pub fn perft_test(pos: *position.Position, depth: u32) void { 56 | pos.debug_print(); 57 | 58 | std.debug.print("Running Perft {}:\n", .{depth}); 59 | 60 | var timer = std.time.Timer.start() catch unreachable; 61 | var nodes: usize = 0; 62 | 63 | if (pos.turn == types.Color.White) { 64 | nodes = perft(types.Color.White, pos, depth); 65 | } else { 66 | nodes = perft(types.Color.Black, pos, depth); 67 | } 68 | 69 | var elapsed = timer.read(); 70 | std.debug.print("\n", .{}); 71 | std.debug.print("Nodes: {}\n", .{nodes}); 72 | var mcs = @intToFloat(f64, elapsed) / 1000.0; 73 | std.debug.print("Elapsed: {d:.2} microseconds (or {d:.6} seconds)\n", .{ mcs, mcs / 1000.0 / 1000.0 }); 74 | var nps = @intToFloat(f64, nodes) / (@intToFloat(f64, elapsed) / 1000.0 / 1000.0 / 1000.0); 75 | std.debug.print("NPS: {d:.2} nodes/s (or {d:.4} mn/s)\n", .{ nps, nps / 1000.0 / 1000.0 }); 76 | } 77 | -------------------------------------------------------------------------------- /src/engine/tt.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const types = @import("../chess/types.zig"); 3 | const position = @import("../chess/position.zig"); 4 | const search = @import("search.zig"); 5 | const hce = @import("hce.zig"); 6 | 7 | pub const MB: usize = 1 << 20; 8 | pub const KB: usize = 1 << 10; 9 | 10 | pub var LOCK_GLOBAL_TT = false; 11 | 12 | pub const Bound = enum(u2) { 13 | None, 14 | Exact, // PV Nodes 15 | Lower, // Cut Nodes 16 | Upper, // All Nodes 17 | }; 18 | 19 | pub const Item = packed struct { 20 | hash: u64, 21 | eval: i32, 22 | bestmove: types.Move, 23 | flag: Bound, 24 | depth: u8, 25 | age: u6, 26 | }; 27 | 28 | pub var TTArena = std.heap.ArenaAllocator.init(std.heap.c_allocator); 29 | 30 | pub const TranspositionTable = struct { 31 | data: std.ArrayList(i128), 32 | size: usize, 33 | age: u6, 34 | 35 | pub fn new() TranspositionTable { 36 | return TranspositionTable{ 37 | .data = std.ArrayList(i128).init(TTArena.allocator()), 38 | .size = 16 * MB / @sizeOf(Item), 39 | .age = 0, 40 | }; 41 | } 42 | 43 | pub fn reset(self: *TranspositionTable, mb: u64) void { 44 | self.data.deinit(); 45 | var tt = TranspositionTable{ 46 | .data = std.ArrayList(i128).init(TTArena.allocator()), 47 | .size = mb * MB / @sizeOf(Item), 48 | .age = 0, 49 | }; 50 | 51 | tt.data.ensureTotalCapacity(tt.size) catch {}; 52 | tt.data.expandToCapacity(); 53 | 54 | // std.debug.print("{}\n", .{@sizeOf(Item)}); 55 | // std.debug.print("Allocated {} KB, {} items for TT\n", .{ tt.size * @sizeOf(Item) / KB, tt.size }); 56 | 57 | self.* = tt; 58 | } 59 | 60 | pub inline fn clear(self: *TranspositionTable) void { 61 | for (self.data.items) |*ptr| { 62 | ptr.* = 0; 63 | } 64 | } 65 | 66 | pub inline fn do_age(self: *TranspositionTable) void { 67 | self.age +%= 1; 68 | } 69 | 70 | pub inline fn index(self: *TranspositionTable, hash: u64) u64 { 71 | return @intCast(u64, @intCast(u128, hash) * @intCast(u128, self.size) >> 64); 72 | } 73 | 74 | pub inline fn set(self: *TranspositionTable, entry: Item) void { 75 | var p = &self.data.items[self.index(entry.hash)]; 76 | var p_val: Item = @ptrCast(*Item, p).*; 77 | // We overwrite entry if: 78 | // 1. It's empty 79 | // 2. New entry is exact 80 | // 3. Previous entry is from older search 81 | // 4. It is a different position 82 | // 5. Previous entry is from same search but has lower depth 83 | if (p.* == 0 or entry.flag == Bound.Exact or p_val.age != self.age or p_val.hash != entry.hash or p_val.depth <= entry.depth + 4) { 84 | //_ = @atomicRmw(i128, p, .Xchg, @ptrCast(*const i128, @alignCast(@alignOf(i128), &entry)).*, .Acquire); 85 | _ = @atomicRmw(i64, @intToPtr(*i64, @ptrToInt(p)), .Xchg, @intToPtr(*const i64, @ptrToInt(&entry)).*, .Acquire); 86 | _ = @atomicRmw(i64, @intToPtr(*i64, @ptrToInt(p) + 8), .Xchg, @intToPtr(*const i64, @ptrToInt(&entry) + 8).*, .Acquire); 87 | } 88 | } 89 | 90 | pub inline fn prefetch(self: *TranspositionTable, hash: u64) void { 91 | @prefetch(&self.data.items[self.index(hash)], .{ 92 | .rw = .read, 93 | .locality = 1, 94 | .cache = .data, 95 | }); 96 | } 97 | 98 | pub inline fn get(self: *TranspositionTable, hash: u64) ?Item { 99 | // self.data.items[hash % self.size].lock.lock(); 100 | // defer self.data.items[hash % self.size].lock.unlock(); 101 | var entry = @ptrCast(*Item, &self.data.items[self.index(hash)]); 102 | if (entry.flag != Bound.None and entry.hash == hash) { 103 | return entry.*; 104 | } 105 | return null; 106 | } 107 | }; 108 | 109 | pub var GlobalTT = TranspositionTable.new(); 110 | -------------------------------------------------------------------------------- /src/engine/bench.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const types = @import("../chess/types.zig"); 3 | const position = @import("../chess/position.zig"); 4 | const search = @import("search.zig"); 5 | 6 | // zig fmt: off 7 | // FENs from Stormphrax who got them from Alexandria who got them from Bit-Genie 8 | const FENS = [_][:0]const u8{ 9 | "r3k2r/2pb1ppp/2pp1q2/p7/1nP1B3/1P2P3/P2N1PPP/R2QK2R w KQkq a6 0 14", 10 | "4rrk1/2p1b1p1/p1p3q1/4p3/2P2n1p/1P1NR2P/PB3PP1/3R1QK1 b - - 2 24", 11 | "r3qbrk/6p1/2b2pPp/p3pP1Q/PpPpP2P/3P1B2/2PB3K/R5R1 w - - 16 42", 12 | "6k1/1R3p2/6p1/2Bp3p/3P2q1/P7/1P2rQ1K/5R2 b - - 4 44", 13 | "8/8/1p2k1p1/3p3p/1p1P1P1P/1P2PK2/8/8 w - - 3 54", 14 | "7r/2p3k1/1p1p1qp1/1P1Bp3/p1P2r1P/P7/4R3/Q4RK1 w - - 0 36", 15 | "r1bq1rk1/pp2b1pp/n1pp1n2/3P1p2/2P1p3/2N1P2N/PP2BPPP/R1BQ1RK1 b - - 2 10", 16 | "3r3k/2r4p/1p1b3q/p4P2/P2Pp3/1B2P3/3BQ1RP/6K1 w - - 3 87", 17 | "2r4r/1p4k1/1Pnp4/3Qb1pq/8/4BpPp/5P2/2RR1BK1 w - - 0 42", 18 | "4q1bk/6b1/7p/p1p4p/PNPpP2P/KN4P1/3Q4/4R3 b - - 0 37", 19 | "2q3r1/1r2pk2/pp3pp1/2pP3p/P1Pb1BbP/1P4Q1/R3NPP1/4R1K1 w - - 2 34", 20 | "1r2r2k/1b4q1/pp5p/2pPp1p1/P3Pn2/1P1B1Q1P/2R3P1/4BR1K b - - 1 37", 21 | "r3kbbr/pp1n1p1P/3ppnp1/q5N1/1P1pP3/P1N1B3/2P1QP2/R3KB1R b KQkq b3 0 17", 22 | "8/6pk/2b1Rp2/3r4/1R1B2PP/P5K1/8/2r5 b - - 16 42", 23 | "1r4k1/4ppb1/2n1b1qp/pB4p1/1n1BP1P1/7P/2PNQPK1/3RN3 w - - 8 29", 24 | "8/p2B4/PkP5/4p1pK/4Pb1p/5P2/8/8 w - - 29 68", 25 | "3r4/ppq1ppkp/4bnp1/2pN4/2P1P3/1P4P1/PQ3PBP/R4K2 b - - 2 20", 26 | "5rr1/4n2k/4q2P/P1P2n2/3B1p2/4pP2/2N1P3/1RR1K2Q w - - 1 49", 27 | "1r5k/2pq2p1/3p3p/p1pP4/4QP2/PP1R3P/6PK/8 w - - 1 51", 28 | "q5k1/5ppp/1r3bn1/1B6/P1N2P2/BQ2P1P1/5K1P/8 b - - 2 34", 29 | "r1b2k1r/5n2/p4q2/1ppn1Pp1/3pp1p1/NP2P3/P1PPBK2/1RQN2R1 w - - 0 22", 30 | "r1bqk2r/pppp1ppp/5n2/4b3/4P3/P1N5/1PP2PPP/R1BQKB1R w KQkq - 0 5", 31 | "r1bqr1k1/pp1p1ppp/2p5/8/3N1Q2/P2BB3/1PP2PPP/R3K2n b Q - 1 12", 32 | "r1bq2k1/p4r1p/1pp2pp1/3p4/1P1B3Q/P2B1N2/2P3PP/4R1K1 b - - 2 19", 33 | "r4qk1/6r1/1p4p1/2ppBbN1/1p5Q/P7/2P3PP/5RK1 w - - 2 25", 34 | "r7/6k1/1p6/2pp1p2/7Q/8/p1P2K1P/8 w - - 0 32", 35 | "r3k2r/ppp1pp1p/2nqb1pn/3p4/4P3/2PP4/PP1NBPPP/R2QK1NR w KQkq - 1 5", 36 | "3r1rk1/1pp1pn1p/p1n1q1p1/3p4/Q3P3/2P5/PP1NBPPP/4RRK1 w - - 0 12", 37 | "5rk1/1pp1pn1p/p3Brp1/8/1n6/5N2/PP3PPP/2R2RK1 w - - 2 20", 38 | "8/1p2pk1p/p1p1r1p1/3n4/8/5R2/PP3PPP/4R1K1 b - - 3 27", 39 | "8/4pk2/1p1r2p1/p1p4p/Pn5P/3R4/1P3PP1/4RK2 w - - 1 33", 40 | "8/5k2/1pnrp1p1/p1p4p/P6P/4R1PK/1P3P2/4R3 b - - 1 38", 41 | "8/8/1p1kp1p1/p1pr1n1p/P6P/1R4P1/1P3PK1/1R6 b - - 15 45", 42 | "8/8/1p1k2p1/p1prp2p/P2n3P/6P1/1P1R1PK1/4R3 b - - 5 49", 43 | "8/8/1p4p1/p1p2k1p/P2npP1P/4K1P1/1P6/3R4 w - - 6 54", 44 | "8/8/1p4p1/p1p2k1p/P2n1P1P/4K1P1/1P6/6R1 b - - 6 59", 45 | "8/5k2/1p4p1/p1pK3p/P2n1P1P/6P1/1P6/4R3 b - - 14 63", 46 | "8/1R6/1p1K1kp1/p6p/P1p2P1P/6P1/1Pn5/8 w - - 0 67", 47 | "1rb1rn1k/p3q1bp/2p3p1/2p1p3/2P1P2N/PP1RQNP1/1B3P2/4R1K1 b - - 4 23", 48 | "4rrk1/pp1n1pp1/q5p1/P1pP4/2n3P1/7P/1P3PB1/R1BQ1RK1 w - - 3 22", 49 | "r2qr1k1/pb1nbppp/1pn1p3/2ppP3/3P4/2PB1NN1/PP3PPP/R1BQR1K1 w - - 4 12", 50 | "2r2k2/8/4P1R1/1p6/8/P4K1N/7b/2B5 b - - 0 55", 51 | "6k1/5pp1/8/2bKP2P/2P5/p4PNb/B7/8 b - - 1 44", 52 | "2rqr1k1/1p3p1p/p2p2p1/P1nPb3/2B1P3/5P2/1PQ2NPP/R1R4K w - - 3 25", 53 | "r1b2rk1/p1q1ppbp/6p1/2Q5/8/4BP2/PPP3PP/2KR1B1R b - - 2 14", 54 | "6r1/5k2/p1b1r2p/1pB1p1p1/1Pp3PP/2P1R1K1/2P2P2/3R4 w - - 1 36", 55 | "rnbqkb1r/pppppppp/5n2/8/2PP4/8/PP2PPPP/RNBQKBNR b KQkq c3 0 2", 56 | "2rr2k1/1p4bp/p1q1p1p1/4Pp1n/2PB4/1PN3P1/P3Q2P/2RR2K1 w - f6 0 20", 57 | "3br1k1/p1pn3p/1p3n2/5pNq/2P1p3/1PN3PP/P2Q1PB1/4R1K1 w - - 0 23", 58 | "2r2b2/5p2/5k2/p1r1pP2/P2pB3/1P3P2/K1P3R1/7R w - - 23 93" 59 | }; 60 | // zig fmt: on 61 | 62 | pub fn bench() !void { 63 | var stdout = std.io.getStdOut().writer(); 64 | 65 | const depth = 15; 66 | var nodes: u64 = 0; 67 | var timer = try std.time.Timer.start(); 68 | var searcher = search.Searcher.new(); 69 | defer searcher.deinit(); 70 | searcher.force_thinking = true; 71 | searcher.silent_output = true; 72 | 73 | for (FENS) |fen| { 74 | var pos = position.Position.new(); 75 | pos.set_fen(fen); 76 | searcher.stop = false; 77 | searcher.reset_heuristics(true); 78 | if (pos.turn == types.Color.White) { 79 | _ = searcher.iterative_deepening(&pos, types.Color.White, depth); 80 | } else { 81 | _ = searcher.iterative_deepening(&pos, types.Color.Black, depth); 82 | } 83 | nodes += searcher.nodes; 84 | } 85 | 86 | try stdout.print("{} nodes {} nps\n", .{ nodes, nodes * std.time.ns_per_s / timer.read() }); 87 | } 88 | -------------------------------------------------------------------------------- /src/engine/movepick.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const types = @import("../chess/types.zig"); 3 | const tables = @import("../chess/tables.zig"); 4 | const position = @import("../chess/position.zig"); 5 | const hce = @import("hce.zig"); 6 | const search = @import("search.zig"); 7 | const see = @import("see.zig"); 8 | 9 | pub const MVV_LVA = [6][6]i32{ .{ 205, 204, 203, 202, 201, 200 }, .{ 305, 304, 303, 302, 301, 300 }, .{ 405, 404, 403, 402, 401, 400 }, .{ 505, 504, 503, 502, 501, 500 }, .{ 605, 604, 603, 602, 601, 600 }, .{ 705, 704, 703, 702, 701, 700 } }; 10 | 11 | pub const SortHash: i32 = 6_000_000; 12 | pub const SortWinningCapture: i32 = 1_000_000; 13 | pub const SortLosingCapture: i32 = 0; 14 | pub const SortQuiet: i32 = 0; 15 | pub const SortKiller1: i32 = 900_000; 16 | pub const SortKiller2: i32 = 800_000; 17 | pub const SortCounterMove: i32 = 600_000; 18 | 19 | pub fn scoreMoves(searcher: *search.Searcher, pos: *position.Position, list: *std.ArrayList(types.Move), hashmove: types.Move, comptime is_null: bool) std.ArrayList(i32) { 20 | var res: std.ArrayList(i32) = std.ArrayList(i32).initCapacity(std.heap.c_allocator, list.items.len) catch unreachable; 21 | 22 | var hm = hashmove.to_u16(); 23 | 24 | for (list.items) |move_| { 25 | var move: *const types.Move = &move_; 26 | var score: i32 = 0; 27 | if (move.is_promotion()) { 28 | if (move.get_flags().promote_type() == types.PieceType.Queen) { 29 | score += 1_000_000; 30 | } else if (move.get_flags().promote_type() == types.PieceType.Knight) { 31 | score += 650_000; 32 | } 33 | } 34 | if (hm == move.to_u16()) { 35 | score += SortHash; 36 | } else if (move.is_capture()) { 37 | if (pos.mailbox[move.to] == types.Piece.NO_PIECE) { 38 | score += SortWinningCapture + MVV_LVA[0][0]; 39 | } else { 40 | var see_value = see.see_threshold(pos, move.*, -90); 41 | 42 | score += MVV_LVA[pos.mailbox[move.to].piece_type().index()][pos.mailbox[move.from].piece_type().index()]; 43 | 44 | if (see_value) { 45 | score += SortWinningCapture; 46 | // recapture 47 | // var last = if (searcher.ply > 0) searcher.move_history[searcher.ply - 1] else types.Move.empty(); 48 | // var last_last_last = if (searcher.ply > 2) searcher.move_history[searcher.ply - 3] else types.Move.empty(); 49 | // if (last.to == move.to) { 50 | // score += 30; 51 | // } 52 | // if (last_last_last.to == move.to) { 53 | // score += 10; 54 | // } 55 | } else { 56 | score += SortLosingCapture; 57 | } 58 | } 59 | } else { 60 | var last = if (searcher.ply > 0) searcher.move_history[searcher.ply - 1] else types.Move.empty(); 61 | if (searcher.killer[searcher.ply][0].to_u16() == move.to_u16()) { 62 | score += SortKiller1; 63 | } else if (searcher.killer[searcher.ply][1].to_u16() == move.to_u16()) { 64 | score += SortKiller2; 65 | } else if (searcher.ply >= 1 and searcher.counter_moves[@enumToInt(pos.turn)][last.from][last.to].to_u16() == move.to_u16()) { 66 | score += SortCounterMove; 67 | } else { 68 | score += SortQuiet; 69 | score += searcher.history[@enumToInt(pos.turn)][move.from][move.to]; 70 | if (!is_null and searcher.ply >= 1) { 71 | const plies: [3]usize = .{ 0, 1, 3 }; 72 | for (plies) |plies_ago| { 73 | const divider: i32 = 1; 74 | if (searcher.ply >= plies_ago + 1) { 75 | const prev = searcher.move_history[searcher.ply - plies_ago - 1]; 76 | if (prev.to_u16() == 0) continue; 77 | 78 | score += @divTrunc(searcher.continuation[searcher.moved_piece_history[searcher.ply - plies_ago - 1].pure_index()][prev.to][move.from][move.to], divider); 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | res.appendAssumeCapacity(score); 86 | } 87 | 88 | return res; 89 | } 90 | 91 | pub inline fn getNextBest(list: *std.ArrayList(types.Move), evals: *std.ArrayList(i32), i: usize) types.Move { 92 | var move_size = list.items.len; 93 | var j = i + 1; 94 | while (j < move_size) : (j += 1) { 95 | if (evals.items[i] < evals.items[j]) { 96 | std.mem.swap(types.Move, &list.items[i], &list.items[j]); 97 | std.mem.swap(i32, &evals.items[i], &evals.items[j]); 98 | } 99 | } 100 | return list.items[i]; 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Avalanche 2 | 3 |
4 | 5 |

6 | Logo 7 |

8 | 9 |
10 | 11 | ### Avalanche is the first and strongest UCI Chess Engine written in [Zig](https://ziglang.org/) 12 | 13 | ## Strength 14 | 15 | **Official [40/15 CCRL ELO (v2.1.0)](https://computerchess.org.uk/ccrl/4040/cgi/engine_details.cgi?match_length=30&each_game=0&print=Details&each_game=0&eng=Avalanche%202.1.0%2064-bit#Avalanche_2_1_0_64-bit): 3346** 16 | 17 | **Official [Blitz CCRL ELO (v2.1.0)](https://computerchess.org.uk/ccrl/404/cgi/engine_details.cgi?match_length=30&each_game=0&print=Details&each_game=0&eng=Avalanche%202.1.0%2064-bit#Avalanche_2_1_0_64-bit): 3400** 18 | 19 | Version 2.1.0 participated in TCEC Swiss 6. 20 | 21 | ## About 22 | 23 | Avalanche is the **first and strongest chess engine** written in the [Zig programming language](https://ziglang.org/), proving Zig's ability to succeed in real-world, competitive applications. 24 | 25 | Avalanche v1.4.0 was the **sole winner** of the 102nd CCRL Amateur Series Tournament (Division 5), having a score of **29.5/44**. See [Tournament Page](https://kirill-kryukov.com/chess/discussion-board/viewtopic.php?f=7&t=15613&sid=8ada67b5589f716aaf477dd1befe051b). 26 | 27 | Avalanche uses the new **NNUE** (Efficiently Updatable Neural Network) technology for its evaluation. 28 | 29 | This project isn't possible without the help of the Zig community, since this is the first and only Zig code I've ever written. Thank you! 30 | 31 | ## License 32 | 33 | Good Old MIT License. In short, feel free to use this program anywhere, but please credit this repository somewhere in your project :) 34 | 35 | ## Compile 36 | 37 | `zig build -Drelease-fast` 38 | 39 | Avalanche is only guaranteed to compile using Zig v0.10.x. Newer versions will not work as Avalanche still uses Stage1. 40 | 41 | Avalanche also has a lichess account (though not often played): https://lichess.org/@/IceBurnEngine 42 | 43 | ## Usage 44 | 45 | Avalanche follows the UCI protocol and is not a full chess application. You should use Avalanche with a UCI-compatible GUI interface. If you need to use the CLI, make sure to send \n at the end of your input (^\n on windows command prompt). 46 | 47 | ## Past Versions 48 | 49 | 50 | 51 | ## Credits 52 | 53 | - [Dan Ellis Echavarria](https://github.com/Deecellar) for writing the github action CI and helping me with Zig questions 54 | - [Ciekce](https://github.com/Ciekce) for guiding me with migrating to the new Marlinflow and answering my stupid questions related to NNUE 55 | - Many other developers in the computer chess community for guiding me through new things like SPRT testing. 56 | 57 | - https://www.chessprogramming.org/ for explanation on everything I need, including search, tt, pruning, reductions... everything. 58 | - https://github.com/nkarve/surge for movegen inspiration. 59 | - Maksim Korzh, https://www.youtube.com/channel/UCB9-prLkPwgvlKKqDgXhsMQ for getting me started on chess programming. 60 | - https://github.com/dsekercioglu/blackmarlin for NNUE structure and trainer skeleton (1.5.0 and older) 61 | - https://github.com/Disservin/Smallbrain and https://github.com/cosmobobak/viridithas for search ideas 62 | - https://openai.com/dall-e-2/ for generating the beautiful logo image 63 | 64 | ## Originality Status 65 | 66 | - General 67 | - This is the first released chess engine written in the **Zig Programming Language**. Although there are Zig libraries for chess, Avalanche is completely stand-alone and does not use any external libraries. 68 | - Move Generator 69 | - Algorithm is inspired by Surge, but code is 100% hand-written in Zig. 70 | - Search 71 | - Avalanche has a simple Search written 100% by myself, but is probably a subset of many other engines. Some ideas are borrowed from other chess engines as in comments. However many ideas and parameters are tuned manually and automatically using my own scripts. 72 | - Evaluation 73 | - The Hand-Crafted Evaluation is based on https://www.chessprogramming.org/PeSTO%27s_Evaluation_Function with adaptation to endgames. The HCE is only activated at late endgames when finding checkmate against a lone king is needed. 74 | - NNUE since 2.0.0 is trained with https://github.com/jw1912/bullet 75 | - The NNUE data since 2.0.0 is purely generated from self-play games. Currently, the latest dev network is trained on 600 million self-play positions at depth 8. 76 | - UCI Interface/Communication code 77 | - 100% original 78 | 79 | ## Alternative Square Logos 80 | Logo 2 81 | Logo 3 82 | Logo 4 83 | 84 | -------------------------------------------------------------------------------- /src/engine/see.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const types = @import("../chess/types.zig"); 3 | const tables = @import("../chess/tables.zig"); 4 | const position = @import("../chess/position.zig"); 5 | const hce = @import("hce.zig"); 6 | const movepick = @import("movepick.zig"); 7 | 8 | pub const SeeWeight = [_]i32{ 93, 308, 346, 521, 994, 20000 }; 9 | 10 | pub fn see_score(pos: *position.Position, move: types.Move) i32 { 11 | var max_depth: usize = 0; 12 | var defenders: types.Bitboard = 0; 13 | var piece_bb: types.Bitboard = 0; 14 | 15 | var to_sq = move.get_to(); 16 | var all_pieces = pos.all_pieces(types.Color.White) | pos.all_pieces(types.Color.Black); 17 | var gains: [16]i32 = undefined; 18 | var opp = pos.turn.invert(); 19 | var blockers = all_pieces & ~types.SquareIndexBB[move.to]; 20 | 21 | gains[0] = SeeWeight[pos.mailbox[move.to].piece_type().index()]; 22 | var last_piece_pts = SeeWeight[pos.mailbox[move.from].piece_type().index()]; 23 | 24 | var depth: usize = 1; 25 | outer: while (depth < gains.len) : (depth += 1) { 26 | gains[depth] = last_piece_pts - gains[depth - 1]; 27 | if (opp == types.Color.White) { 28 | defenders = pos.all_pieces(types.Color.White) & blockers; 29 | } else { 30 | defenders = pos.all_pieces(types.Color.Black) & blockers; 31 | } 32 | var pt = types.PieceType.Pawn.index(); 33 | var ending = types.PieceType.King.index(); 34 | while (pt <= ending) : (pt += 1) { 35 | last_piece_pts = SeeWeight[pt]; 36 | piece_bb = (if (pt == 0) (if (opp == types.Color.White) tables.get_pawn_attacks(types.Color.Black, to_sq) else tables.get_pawn_attacks(types.Color.White, to_sq)) else (tables.get_attacks(@intToEnum(types.PieceType, pt), to_sq, blockers))) & defenders & (pos.piece_bitboards[pt] | pos.piece_bitboards[pt + 8]); 37 | if (piece_bb != 0) { 38 | blockers &= ~(types.SquareIndexBB[@intCast(usize, types.lsb(piece_bb))]); 39 | opp = opp.invert(); 40 | continue :outer; 41 | } 42 | } 43 | 44 | max_depth = depth; 45 | break; 46 | } 47 | 48 | depth = max_depth - 1; 49 | while (depth >= 1) : (depth -= 1) { 50 | gains[depth - 1] = -@max(-gains[depth - 1], gains[depth]); 51 | } 52 | 53 | return gains[0]; 54 | } 55 | 56 | // Logic https://github.com/TerjeKir/weiss 57 | pub fn see_threshold(pos: *position.Position, move: types.Move, threshold: i32) bool { 58 | var from = move.from; 59 | var to = move.to; 60 | var attacker = pos.mailbox[from].piece_type().index(); 61 | var victim = pos.mailbox[to].piece_type().index(); 62 | var swap = SeeWeight[victim] - threshold; 63 | if (swap < 0) { 64 | return false; 65 | } 66 | swap -= SeeWeight[attacker]; 67 | if (swap >= 0) { 68 | return true; 69 | } 70 | 71 | var all = pos.all_pieces(types.Color.White) | pos.all_pieces(types.Color.Black); 72 | 73 | var occ = (all ^ types.SquareIndexBB[from]) | types.SquareIndexBB[to]; 74 | var attackers = (pos.attackers_from(types.Color.White, @intToEnum(types.Square, to), occ) | pos.attackers_from(types.Color.Black, @intToEnum(types.Square, to), occ)) & occ; 75 | 76 | var bishops = pos.diagonal_sliders(types.Color.White) | pos.diagonal_sliders(types.Color.Black); 77 | var rooks = pos.orthogonal_sliders(types.Color.White) | pos.orthogonal_sliders(types.Color.Black); 78 | 79 | var stm = pos.mailbox[from].color().invert(); 80 | 81 | while (true) { 82 | attackers &= occ; 83 | var my_attackers = attackers; 84 | if (stm == types.Color.White) { 85 | my_attackers &= pos.all_pieces(types.Color.White); 86 | } else { 87 | my_attackers &= pos.all_pieces(types.Color.Black); 88 | } 89 | if (my_attackers == 0) { 90 | break; 91 | } 92 | 93 | var pt: usize = 0; 94 | while (pt <= 5) : (pt += 1) { 95 | if (my_attackers & (pos.piece_bitboards[pt] | pos.piece_bitboards[pt + 8]) != 0) { 96 | break; 97 | } 98 | } 99 | 100 | stm = stm.invert(); 101 | 102 | swap = -swap - 1 - SeeWeight[pt]; 103 | 104 | if (swap >= 0) { 105 | if (pt == 5) { 106 | if (stm == types.Color.White) { 107 | if (attackers & pos.all_pieces(types.Color.White) != 0) { 108 | stm = stm.invert(); 109 | } 110 | } else { 111 | if (attackers & pos.all_pieces(types.Color.Black) != 0) { 112 | stm = stm.invert(); 113 | } 114 | } 115 | } 116 | break; 117 | } 118 | 119 | occ ^= types.SquareIndexBB[@intCast(usize, types.lsb(my_attackers & (pos.piece_bitboards[pt] | pos.piece_bitboards[pt + 8])))]; 120 | 121 | if (pt == 0 or pt == 2 or pt == 4) { 122 | attackers |= tables.get_bishop_attacks(@intToEnum(types.Square, to), occ) & bishops; 123 | } else if (pt == 3 or pt == 4) { 124 | attackers |= tables.get_rook_attacks(@intToEnum(types.Square, to), occ) & rooks; 125 | } 126 | } 127 | 128 | return stm != pos.mailbox[from].color(); 129 | } 130 | -------------------------------------------------------------------------------- /src/engine/nnue.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | pub const weights = @import("weights.zig"); 3 | const types = @import("../chess/types.zig"); 4 | const position = @import("../chess/position.zig"); 5 | const search = @import("search.zig"); 6 | 7 | const QA: i32 = 255; 8 | const QB: i32 = 64; 9 | const QAB: i32 = QA * QB; 10 | 11 | const SCALE: i32 = 400; 12 | 13 | const SQUARED_ACTIVATION: bool = true; 14 | 15 | pub const WhiteBlackPair = packed struct { 16 | white: usize, 17 | black: usize, 18 | }; 19 | 20 | pub inline fn get_bucket(pos: *position.Position) usize { 21 | return (types.popcount_usize(pos.all_all_pieces()) - 2) / 4; 22 | } 23 | 24 | pub fn nnue_index(piece: types.Piece, sq: types.Square) WhiteBlackPair { 25 | const p: usize = piece.piece_type().index(); 26 | const c: types.Color = piece.color(); 27 | 28 | const white = @intCast(usize, @enumToInt(c)) * 64 * 6 + p * 64 + @intCast(usize, sq.index()); 29 | const black = @intCast(usize, @enumToInt(c.invert())) * 64 * 6 + p * 64 + @intCast(usize, sq.index() ^ 56); 30 | 31 | return WhiteBlackPair{ 32 | .white = white * weights.HIDDEN_SIZE, 33 | .black = black * weights.HIDDEN_SIZE, 34 | }; 35 | } 36 | 37 | pub inline fn clipped_relu(input: i16) i32 { 38 | const k = @as(i32, std.math.clamp(input, 0, 255)); 39 | if (SQUARED_ACTIVATION) { 40 | return k * k; 41 | } else { 42 | return k; 43 | } 44 | } 45 | 46 | pub const Accumulator = packed struct { 47 | white: [weights.HIDDEN_SIZE]i16, 48 | black: [weights.HIDDEN_SIZE]i16, 49 | 50 | pub inline fn clear(self: *Accumulator) void { 51 | self.white = weights.MODEL.layer_1_bias; 52 | self.black = weights.MODEL.layer_1_bias; 53 | } 54 | 55 | pub fn update_weights(self: *Accumulator, comptime on: bool, data: WhiteBlackPair) void { 56 | var i: usize = 0; 57 | while (i < weights.HIDDEN_SIZE) : (i += 1) { 58 | if (on) { 59 | self.white[i] += weights.MODEL.layer_1[data.white + i]; 60 | self.black[i] += weights.MODEL.layer_1[data.black + i]; 61 | } else { 62 | self.white[i] -= weights.MODEL.layer_1[data.white + i]; 63 | self.black[i] -= weights.MODEL.layer_1[data.black + i]; 64 | } 65 | } 66 | } 67 | 68 | pub fn exchange_weights(self: *Accumulator, from: WhiteBlackPair, to: WhiteBlackPair) void { 69 | var i: usize = 0; 70 | while (i < weights.HIDDEN_SIZE) : (i += 1) { 71 | self.white[i] += weights.LAYER_1[to.white + i] - weights.LAYER_1[from.white + i]; 72 | self.black[i] += weights.LAYER_1[to.black + i] - weights.LAYER_1[from.black + i]; 73 | } 74 | } 75 | }; 76 | 77 | pub const NNUE = struct { 78 | accumulator_stack: [search.MAX_PLY + 2]Accumulator, 79 | stack_index: usize, 80 | 81 | pub fn new() NNUE { 82 | return NNUE{ 83 | .accumulator_stack = undefined, 84 | .stack_index = 0, 85 | }; 86 | } 87 | 88 | pub inline fn toggle(self: *NNUE, comptime on: bool, piece: types.Piece, sq: types.Square) void { 89 | self.accumulator_stack[self.stack_index].update_weights(on, nnue_index(piece, sq)); 90 | } 91 | 92 | pub fn refresh_accumulator(self: *NNUE, pos: *position.Position) void { 93 | self.stack_index = 0; 94 | self.accumulator_stack[0].clear(); 95 | 96 | for (pos.mailbox) |pc, i| { 97 | if (pc == types.Piece.NO_PIECE) { 98 | continue; 99 | } 100 | 101 | self.toggle(true, pc, @intToEnum(types.Square, i)); 102 | } 103 | } 104 | 105 | pub inline fn pop(self: *NNUE) void { 106 | self.stack_index -= 1; 107 | } 108 | 109 | pub inline fn push(self: *NNUE) void { 110 | self.accumulator_stack[self.stack_index + 1] = self.accumulator_stack[self.stack_index]; 111 | self.stack_index += 1; 112 | } 113 | 114 | pub inline fn move(self: *NNUE, pc: types.Piece, from: types.Square, to: types.Square) void { 115 | self.accumulator_stack[self.stack_index].exchange_weights(nnue_index(pc, from), nnue_index(pc, to)); 116 | } 117 | 118 | pub inline fn evaluate(self: *NNUE, turn: types.Color, pos: *position.Position) i32 { 119 | return if (turn == types.Color.White) self.evaluate_comptime(types.Color.White, pos) else self.evaluate_comptime(types.Color.Black, pos); 120 | } 121 | 122 | pub inline fn evaluate_comptime(self: *NNUE, comptime turn: types.Color, pos: *position.Position) i32 { 123 | const acc = &self.accumulator_stack[self.stack_index]; 124 | 125 | const bucket = get_bucket(pos); 126 | 127 | var res = @as(i32, weights.MODEL.layer_2_bias[bucket]); 128 | 129 | var i: usize = 0; 130 | while (i < weights.HIDDEN_SIZE) : (i += 1) { 131 | if (turn == types.Color.White) { 132 | res += clipped_relu(acc.white[i]) * weights.MODEL.layer_2[bucket][i]; 133 | res += clipped_relu(acc.black[i]) * weights.MODEL.layer_2[bucket][weights.HIDDEN_SIZE..][i]; 134 | } else { 135 | res += clipped_relu(acc.black[i]) * weights.MODEL.layer_2[bucket][i]; 136 | res += clipped_relu(acc.white[i]) * weights.MODEL.layer_2[bucket][weights.HIDDEN_SIZE..][i]; 137 | } 138 | } 139 | 140 | if (SQUARED_ACTIVATION) { 141 | return @divTrunc(@divTrunc(res, QA) * SCALE, QAB); 142 | } else { 143 | return @divTrunc(res * SCALE, QAB); 144 | } 145 | } 146 | }; 147 | -------------------------------------------------------------------------------- /src/tests.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const types = @import("chess/types.zig"); 3 | const tables = @import("chess/tables.zig"); 4 | const position = @import("chess/position.zig"); 5 | const zobrist = @import("chess/zobrist.zig"); 6 | const hce = @import("engine/hce.zig"); 7 | const expect = std.testing.expect; 8 | 9 | test "Basic Piece and Color" { 10 | try expect(types.Color.White.invert() == types.Color.Black); 11 | try expect(types.Color.Black.invert() == types.Color.White); 12 | 13 | var p = types.Piece.new(types.Color.Black, types.PieceType.Knight); 14 | try expect(p.piece_type() == types.PieceType.Knight); 15 | try expect(p.color() == types.Color.Black); 16 | } 17 | 18 | test "Directions" { 19 | try expect(types.Direction.North.relative_dir(types.Color.Black) == types.Direction.South); 20 | try expect(types.Direction.SouthEast.relative_dir(types.Color.Black) == types.Direction.NorthWest); 21 | 22 | try expect(types.Direction.SouthSouth.relative_dir(types.Color.White) == types.Direction.SouthSouth); 23 | } 24 | 25 | test "Square" { 26 | var sq: types.Square = types.Square.d4; 27 | try expect(sq.inc().* == types.Square.e4); 28 | try expect(sq == types.Square.e4); 29 | 30 | try expect(sq.add(types.Direction.North) == types.Square.e5); 31 | try expect(sq.add(types.Direction.East) == types.Square.f4); 32 | try expect(sq.add(types.Direction.SouthWest) == types.Square.d3); 33 | try expect(sq.add(types.Direction.SouthSouth) == types.Square.e2); 34 | 35 | try expect(sq.sub(types.Direction.North) == types.Square.e3); 36 | try expect(sq.sub(types.Direction.SouthWest) == types.Square.f5); 37 | } 38 | 39 | test "Rank & File" { 40 | try expect(types.Rank.RANK2.relative_rank(types.Color.Black) == types.Rank.RANK7); 41 | try expect(types.Rank.RANK5.relative_rank(types.Color.Black) == types.Rank.RANK4); 42 | try expect(types.Rank.RANK8.relative_rank(types.Color.White) == types.Rank.RANK8); 43 | } 44 | 45 | test "Square & Rank & File" { 46 | try expect(types.Square.b2.rank() == types.Rank.RANK2); 47 | try expect(types.Square.b2.file() == types.File.BFILE); 48 | 49 | try expect(types.Square.new(types.File.EFILE, types.Rank.RANK4) == types.Square.e4); 50 | try expect(types.Square.e4.rank() == types.Rank.RANK4); 51 | try expect(types.Square.e4.file() == types.File.EFILE); 52 | } 53 | 54 | test "Bitboard general" { 55 | try expect(types.popcount(0b0110111010010) == 7); 56 | try expect(types.lsb(0b01101000) == 3); 57 | var b: types.Bitboard = 0b01101000; 58 | try expect(@enumToInt(types.pop_lsb(&b)) == 3); 59 | try expect(b == 0b01100000); 60 | 61 | var bb_i: types.Bitboard = 0x3c18183c0000; 62 | try expect(types.shift_bitboard(bb_i, types.Direction.North) == 0x3c18183c000000); 63 | try expect(types.shift_bitboard(bb_i, types.Direction.NorthNorth) == 0x3c18183c00000000); 64 | try expect(types.shift_bitboard(bb_i, types.Direction.South) == 0x3c18183c00); 65 | try expect(types.shift_bitboard(bb_i, types.Direction.SouthSouth) == 0x3c18183c); 66 | try expect(types.shift_bitboard(bb_i, types.Direction.East) == 0x783030780000); 67 | try expect(types.shift_bitboard(bb_i, types.Direction.West) == 0x1e0c0c1e0000); 68 | try expect(types.shift_bitboard(bb_i, types.Direction.SouthEast) == 0x7830307800); 69 | try expect(types.shift_bitboard(bb_i, types.Direction.NorthWest) == 0x1e0c0c1e000000); 70 | } 71 | 72 | test "Move" { 73 | try expect(@sizeOf(types.Move) == 2); 74 | try expect(@bitSizeOf(types.Move) == 16); 75 | 76 | { 77 | var m = types.Move.empty(); 78 | try expect(m.get_from() == types.Square.a1); 79 | try expect(m.get_to() == types.Square.a1); 80 | } 81 | 82 | { 83 | var m = types.Move.new_from_to(types.Square.g1, types.Square.f3); 84 | try expect(m.get_from() == types.Square.g1); 85 | try expect(m.get_to() == types.Square.f3); 86 | try expect(m.get_flags() == types.MoveFlags.QUIET); 87 | } 88 | 89 | { 90 | var m = types.Move.new_from_to_flag(types.Square.e2, types.Square.e4, types.MoveFlags.DOUBLE_PUSH); 91 | try expect(m.get_from() == types.Square.e2); 92 | try expect(m.get_to() == types.Square.e4); 93 | try expect(m.get_flags() == types.MoveFlags.DOUBLE_PUSH); 94 | } 95 | 96 | { 97 | var m = types.Move.new_from_to_flag(types.Square.e4, types.Square.e5, types.MoveFlags.CAPTURE); 98 | try expect(m.is_capture()); 99 | try expect(m.equals_to(m)); 100 | } 101 | } 102 | 103 | test "Bitboard operations" { 104 | try expect(tables.reverse_bitboard(0x24180000000000) == 0x182400); 105 | try expect(tables.reverse_bitboard(0x40040000200200) == 0x40040000200200); 106 | } 107 | 108 | test "Magic Bitboard Slidng Pieces" { 109 | tables.init_rook_attacks(); 110 | tables.init_bishop_attacks(); 111 | 112 | try expect(tables.get_rook_attacks(types.Square.a1, 0x80124622004420) == 0x10101010101013e); 113 | try expect(tables.get_rook_attacks(types.Square.e4, 0x80124622004420) == 0x10102e101010); 114 | 115 | try expect(tables.get_bishop_attacks(types.Square.e4, 0x80124622004420) == 0x182442800284400); 116 | try expect(tables.get_bishop_attacks(types.Square.a1, 0x80124622004420) == 0x8040201008040200); 117 | 118 | try expect(tables.get_xray_rook_attacks(types.Square.e4, 0x11014004a10d3d0, 0x100048100000) == 0x10000086001000); 119 | try expect(tables.get_xray_bishop_attacks(types.Square.e4, 0x108a48c0c1294500, 0x2400000280000) == 0x180000000004400); 120 | } 121 | 122 | test "Squares and Line Between" { 123 | tables.init_squares_between(); 124 | tables.init_line_between(); 125 | try expect(tables.SquaresBetween[types.Square.b4.index()][types.Square.f4.index()] == 0x1c000000); 126 | try expect(tables.SquaresBetween[types.Square.e3.index()][types.Square.e7.index()] == 0x101010000000); 127 | try expect(tables.SquaresBetween[types.Square.b2.index()][types.Square.g7.index()] == 0x201008040000); 128 | try expect(tables.SquaresBetween[types.Square.b7.index()][types.Square.g2.index()] == 0x40810200000); 129 | try expect(tables.SquaresBetween[types.Square.a1.index()][types.Square.g4.index()] == 0); 130 | 131 | try expect(tables.LineOf[types.Square.b4.index()][types.Square.f4.index()] == 0xff000000); 132 | try expect(tables.LineOf[types.Square.e3.index()][types.Square.e7.index()] == 0x1010101010101010); 133 | try expect(tables.LineOf[types.Square.b2.index()][types.Square.g7.index()] == 0x8040201008040201); 134 | try expect(tables.LineOf[types.Square.b7.index()][types.Square.g2.index()] == 0x102040810204080); 135 | try expect(tables.LineOf[types.Square.a1.index()][types.Square.g4.index()] == 0); 136 | } 137 | 138 | test "Pawn Attacks" { 139 | tables.init_pseudo_legal(); 140 | 141 | try expect(tables.get_pawn_attacks(types.Color.Black, types.Square.e5) == 0x28000000); 142 | try expect(tables.get_pawn_attacks(types.Color.White, types.Square.e4) == 0x2800000000); 143 | try expect(tables.get_pawn_attacks(types.Color.White, types.Square.a5) == 0x20000000000); 144 | 145 | try expect(tables.get_pawn_attacks_bb(types.Color.White, 0x2800000200400) == 0x5400000500a0000); 146 | try expect(tables.get_pawn_attacks_bb(types.Color.Black, 0x2800000200400) == 0x5400000500a); 147 | } 148 | 149 | test "Position" { 150 | zobrist.init_zobrist(); 151 | 152 | var pos = position.Position.new(); 153 | 154 | try expect(pos.hash == 0); 155 | try expect(pos.turn == types.Color.White); 156 | try expect(pos.game_ply == 0); 157 | try expect(pos.mailbox[0] == types.Piece.NO_PIECE); 158 | 159 | pos.add_piece(types.Piece.WHITE_KNIGHT, types.Square.f3); 160 | try expect(pos.mailbox[types.Square.f3.index()] == types.Piece.WHITE_KNIGHT); 161 | try expect(pos.piece_bitboards[types.Piece.WHITE_KNIGHT.index()] == 0x200000); 162 | 163 | pos.remove_piece(types.Square.f3); 164 | try expect(pos.mailbox[types.Square.f3.index()] == types.Piece.NO_PIECE); 165 | try expect(pos.piece_bitboards[types.Piece.WHITE_KNIGHT.index()] == 0); 166 | 167 | pos = position.Position.new(); 168 | 169 | pos.set_fen("rnbqkbnr/1ppp1pp1/p6p/4p3/8/1P3N2/PBPPPPPP/RN1QKB1R w KQkq"[0..]); 170 | try expect(pos.attackers_from(types.Color.White, types.Square.e5, 0) == 0x200200); 171 | try expect(!pos.in_check(types.Color.White)); 172 | 173 | // queen check 174 | pos.set_fen("rnb1kbnr/pppp1ppp/8/4p3/4PP1q/8/PPPP2PP/RNBQKBNR w KQkq"[0..]); 175 | try expect(pos.attackers_from(types.Color.Black, types.Square.e1, 0) == 0x80000000); 176 | try expect(pos.in_check(types.Color.White)); 177 | try expect(!pos.in_check(types.Color.Black)); 178 | 179 | // blocked 180 | pos.set_fen("rnb1kbnr/pppp1ppp/8/4p3/4PP1q/6P1/PPPP3P/RNBQKBNR b KQkq"[0..]); 181 | try expect(!pos.in_check(types.Color.White)); 182 | 183 | pos.set_fen(types.DEFAULT_FEN[0..]); 184 | var score = hce.evaluate(&pos); 185 | 186 | var m1 = types.Move.new_from_string(&pos, "e2e4"[0..]); 187 | pos.play_move(types.Color.White, m1); 188 | var m2 = types.Move.new_from_string(&pos, "d7d5"[0..]); 189 | pos.play_move(types.Color.Black, m2); 190 | var m3 = types.Move.new_from_string(&pos, "e4d5"[0..]); 191 | pos.play_move(types.Color.White, m3); 192 | 193 | pos.undo_move(types.Color.White, m3); 194 | pos.undo_move(types.Color.Black, m2); 195 | pos.undo_move(types.Color.White, m1); 196 | 197 | try expect(score == hce.evaluate(&pos)); 198 | } 199 | -------------------------------------------------------------------------------- /src/engine/datagen.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const types = @import("../chess/types.zig"); 3 | const utils = @import("../chess/utils.zig"); 4 | const hce = @import("hce.zig"); 5 | const position = @import("../chess/position.zig"); 6 | const search = @import("search.zig"); 7 | const tt = @import("tt.zig"); 8 | 9 | pub const FileLock = struct { 10 | file: std.fs.File, 11 | lock: std.Thread.Mutex, 12 | }; 13 | 14 | const MAX_DEPTH: ?u8 = null; 15 | const MAX_NODES: ?u64 = 8000000; 16 | const SOFT_MAX_NODES: ?u64 = 5000; 17 | 18 | pub const DatagenSingle = struct { 19 | id: u64, 20 | timer: std.time.Timer, 21 | count: u64, 22 | searcher: search.Searcher, 23 | fileLock: *FileLock, 24 | prng: *utils.PRNG, 25 | 26 | pub fn new(lock: *FileLock, prng: *utils.PRNG, id: u64) DatagenSingle { 27 | var searcher = search.Searcher.new(); 28 | searcher.max_millis = 1000; 29 | searcher.ideal_time = 500; 30 | searcher.max_nodes = MAX_NODES; 31 | searcher.soft_max_nodes = SOFT_MAX_NODES; 32 | searcher.min_depth = 2; 33 | searcher.silent_output = true; 34 | return DatagenSingle{ 35 | .id = id, 36 | .timer = undefined, 37 | .count = 0, 38 | .searcher = searcher, 39 | .fileLock = lock, 40 | .prng = prng, 41 | }; 42 | } 43 | 44 | pub fn deinit(self: *DatagenSingle) void { 45 | self.searcher.deinit(); 46 | self.fileLock.file.close(); 47 | } 48 | 49 | pub fn playGame(self: *DatagenSingle) !void { 50 | self.searcher.root_board = position.Position.new(); 51 | var pos = &self.searcher.root_board; 52 | pos.set_fen(types.DEFAULT_FEN); 53 | tt.GlobalTT.reset(64); 54 | 55 | self.searcher.reset_heuristics(true); 56 | self.searcher.stop = false; 57 | self.searcher.force_thinking = false; 58 | 59 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 60 | defer arena.deinit(); 61 | 62 | var fens = try std.ArrayList([]u8).initCapacity(arena.allocator(), 128); 63 | var evals = try std.ArrayList(i32).initCapacity(arena.allocator(), 128); 64 | defer fens.deinit(); 65 | defer evals.deinit(); 66 | 67 | var result: f32 = 0.5; 68 | var draw_count: usize = 0; 69 | var white_win_count: usize = 0; 70 | var black_win_count: usize = 0; 71 | var ply: usize = 0; 72 | var random_plies: u64 = 9 + (self.prng.rand64() % 4); 73 | while (true) : (ply += 1) { 74 | if (self.searcher.is_draw(pos, true)) { 75 | result = 0.5; 76 | break; 77 | } 78 | var movelist = try std.ArrayList(types.Move).initCapacity(arena.allocator(), 32); 79 | if (pos.turn == types.Color.White) { 80 | pos.generate_legal_moves(types.Color.White, &movelist); 81 | } else { 82 | pos.generate_legal_moves(types.Color.Black, &movelist); 83 | } 84 | var move_size = movelist.items.len; 85 | if (move_size == 0) { 86 | if (pos.turn == types.Color.White) { 87 | if (pos.in_check(types.Color.White)) { 88 | result = 0.0; 89 | } else { 90 | result = 0.5; 91 | } 92 | } else { 93 | if (pos.in_check(types.Color.Black)) { 94 | result = 1.0; 95 | } else { 96 | result = 0.5; 97 | } 98 | } 99 | movelist.deinit(); 100 | break; 101 | } 102 | 103 | // play a random move if we're in the first few plies or with a 0.01% chance 104 | if (ply < random_plies or self.prng.rand64() % 10000 == 0) { 105 | var move = movelist.items[self.prng.rand64() % move_size]; 106 | if (pos.turn == types.Color.White) { 107 | pos.play_move(types.Color.White, move); 108 | } else { 109 | pos.play_move(types.Color.Black, move); 110 | } 111 | movelist.deinit(); 112 | continue; 113 | } 114 | movelist.deinit(); 115 | 116 | var res: i32 = if (pos.turn == types.Color.White) 117 | self.searcher.iterative_deepening(pos, types.Color.White, MAX_DEPTH) 118 | else 119 | -self.searcher.iterative_deepening(pos, types.Color.Black, MAX_DEPTH); 120 | 121 | if (ply == random_plies and (res > 1000 or res < -1000)) { 122 | break; 123 | } 124 | 125 | var best_move = self.searcher.best_move; 126 | 127 | var fen = pos.basic_fen(arena.allocator()); 128 | var in_check = if (pos.turn == types.Color.White) pos.in_check(types.Color.White) else pos.in_check(types.Color.Black); 129 | 130 | if (pos.turn == types.Color.White) { 131 | pos.play_move(types.Color.White, best_move); 132 | } else { 133 | pos.play_move(types.Color.Black, best_move); 134 | } 135 | 136 | const limit: i32 = if (pos.phase() >= 6) 850 else 500; 137 | 138 | if (res > limit) { 139 | white_win_count += 1; 140 | 141 | if (white_win_count >= 8) { 142 | result = 1.0; 143 | break; 144 | } 145 | } else { 146 | white_win_count = 0; 147 | } 148 | 149 | if (res < -limit) { 150 | black_win_count += 1; 151 | 152 | if (black_win_count >= 8) { 153 | result = 0.0; 154 | break; 155 | } 156 | } else { 157 | black_win_count = 0; 158 | } 159 | 160 | if (ply >= 40 and -1 < res and res < 1) { 161 | draw_count += 1; 162 | if (draw_count >= 10) { 163 | result = 0.5; 164 | break; 165 | } 166 | } else { 167 | draw_count = 0; 168 | } 169 | 170 | if (res > 2000 or res < -2000) { 171 | continue; 172 | } 173 | 174 | if (ply <= 16) { 175 | continue; 176 | } 177 | 178 | var gave_check = if (pos.turn == types.Color.White) pos.in_check(types.Color.White) else pos.in_check(types.Color.Black); 179 | if (!in_check and !gave_check and !best_move.is_capture() and !best_move.is_promotion()) { 180 | // pretty quiet 181 | try fens.append(fen); 182 | try evals.append(res); 183 | } 184 | } 185 | 186 | if (fens.items.len == 0) { 187 | return; 188 | } 189 | 190 | self.fileLock.lock.lock(); 191 | var writer = self.fileLock.file.writer(); 192 | var i: usize = 0; 193 | while (i < fens.items.len) : (i += 1) { 194 | try writer.print("{s}", .{fens.items[i]}); 195 | var s = if (result == 0.0) "0.0" else if (result == 1.0) "1.0" else "0.5"; 196 | try writer.print(" | {} | {s}\n", .{ evals.items[i], s }); 197 | } 198 | self.count += fens.items.len; 199 | self.fileLock.lock.unlock(); 200 | } 201 | 202 | pub fn startMany(self: *DatagenSingle) !void { 203 | self.timer = try std.time.Timer.start(); 204 | var game_count: usize = 0; 205 | while (true) { 206 | try self.playGame(); 207 | game_count += 1; 208 | if (game_count % 50 == 0) { 209 | var elapsed = @intToFloat(f64, self.timer.read()) / std.time.ns_per_s; 210 | var pps = @intToFloat(f64, self.count) / elapsed; 211 | var gps = @intToFloat(f64, game_count) / elapsed; 212 | 213 | std.debug.print("id {}: {} games, {} pos, {d:.4} pos/s, {d:.4} games/s\n", .{ self.id, game_count, self.count, pps, gps }); 214 | } 215 | } 216 | } 217 | }; 218 | 219 | pub const Datagen = struct { 220 | fileLock: FileLock, 221 | prng: utils.PRNG, 222 | datagens: std.ArrayList(DatagenSingle), 223 | 224 | pub fn new() Datagen { 225 | var prng = std.rand.DefaultPrng.init(blk: { 226 | var seed: u64 = undefined; 227 | std.os.getrandom(std.mem.asBytes(&seed)) catch unreachable; 228 | break :blk seed; 229 | }); 230 | const rand = prng.random(); 231 | return Datagen{ 232 | .fileLock = undefined, 233 | .prng = utils.PRNG.new(rand.int(u128)), 234 | .datagens = std.ArrayList(DatagenSingle).init(std.heap.c_allocator), 235 | }; 236 | } 237 | 238 | pub fn deinit(self: *Datagen) void { 239 | var i: usize = 0; 240 | while (i < self.datagens.items.len) : (i += 1) { 241 | self.datagens.items[i].deinit(); 242 | } 243 | self.datagens.deinit(); 244 | } 245 | 246 | pub fn start(self: *Datagen, num_threads: usize) !void { 247 | const path = try std.fmt.allocPrint(std.heap.page_allocator, "data_{}.txt", .{std.time.timestamp()}); 248 | const file = std.fs.cwd().createFile( 249 | path, 250 | .{ .read = true }, 251 | ) catch { 252 | std.debug.panic("Unable to open {s}", .{path}); 253 | }; 254 | var lock = std.Thread.Mutex{}; 255 | self.fileLock = FileLock{ .file = file, .lock = lock }; 256 | self.datagens.clearAndFree(); 257 | 258 | var threads = std.ArrayList(std.Thread).init(std.heap.c_allocator); 259 | defer threads.deinit(); 260 | 261 | var th: usize = 0; 262 | while (th < num_threads) : (th += 1) { 263 | var datagen = DatagenSingle.new(&self.fileLock, &self.prng, th); 264 | try self.datagens.append(datagen); 265 | var thread = std.Thread.spawn( 266 | .{ .stack_size = 64 * 1024 * 1024 }, 267 | DatagenSingle.startMany, 268 | .{&self.datagens.items[th]}, 269 | ) catch |e| { 270 | std.debug.panic("Could not spawn thread!\n{}", .{e}); 271 | unreachable; 272 | }; 273 | try threads.append(thread); 274 | } 275 | 276 | for (threads.items) |thread| { 277 | thread.join(); 278 | } 279 | } 280 | 281 | pub fn startSingleThreaded(self: *Datagen) !void { 282 | var id: u64 = @intCast(u64, std.time.timestamp()); 283 | const path = try std.fmt.allocPrint(std.heap.page_allocator, "data_{}.txt", .{id}); 284 | const file = std.fs.cwd().createFile( 285 | path, 286 | .{ .read = true }, 287 | ) catch { 288 | std.debug.panic("Unable to open {s}", .{path}); 289 | }; 290 | var lock = std.Thread.Mutex{}; 291 | self.fileLock = FileLock{ .file = file, .lock = lock }; 292 | self.datagens.clearAndFree(); 293 | 294 | var datagen = DatagenSingle.new(&self.fileLock, &self.prng, id); 295 | try datagen.startMany(); 296 | } 297 | }; 298 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v2.1.0 2 | 3 | ## 1/11/2024 4 | 5 | - Regression 6 | 7 | STC 8.0+0.08 | noob_4moves 8 | ``` 9 | Score of New vs v2.0.0: 608 - 303 - 1089 [0.576] 2000 10 | Elo difference: 53.4 +/- 10.2, LOS: 100.0 %, DrawRatio: 54.4 % 11 | ``` 12 | 13 | LTC 40.0+0.40 | noob_4moves 14 | ``` 15 | Score of New vs v2.0.0: 249 - 68 - 683 [0.591] 1000 16 | Elo difference: 63.6 +/- 11.9, LOS: 100.0 %, DrawRatio: 68.3 % 17 | ``` 18 | 19 | ## 1/9/2024 20 | 21 | - More LTC Tune 22 | 23 | LTC 40.0+0.40 | Pohl 24 | ``` 25 | Score of New vs Master: 112 - 100 - 270 [0.512] 482 26 | Elo difference: 8.7 +/- 20.6, LOS: 79.5 %, DrawRatio: 56.0 % 27 | ``` 28 | 29 | ## 1/7/2024 30 | 31 | - Regression vs v2.0.0 32 | 33 | LTC 40.0+0.40 | 8moves_v3 34 | ``` 35 | Score of New vs v2.0.0: 201 - 57 - 542 [0.590] 800 36 | Elo difference: 63.2 +/- 13.4, LOS: 100.0 %, DrawRatio: 67.8 % 37 | ``` 38 | 39 | SMP 30.0+0.30 | 8moves_v3 40 | ``` 41 | Score of New_4T_256 vs v2.0.0_4T_256: 80 - 7 - 113 [0.682] 200 42 | Elo difference: 132.9 +/- 30.6, LOS: 100.0 %, DrawRatio: 56.5 % 43 | Ordo: +134.2 44 | ``` 45 | 46 | ## 1/6/2024 47 | 48 | - Tune and limit stability in time management 49 | 50 | STC 8.0+0.08 | Pohl 51 | ``` 52 | Score of New vs Master: 267 - 246 - 500 [0.510] 1013 53 | Elo difference: 7.2 +/- 15.2 54 | ``` 55 | 56 | ## 1/5/2024 57 | 58 | - Add cutnode conditions 59 | 60 | STC 8.0+0.08 | Pohl 61 | ``` 62 | Score of New_16 vs Master_16: 345 - 318 - 680 [0.510] 1343 63 | Elo difference: 7.0 +/- 13.0 64 | ``` 65 | 66 | ## 1/2/2024 67 | 68 | - 30+0.3 5k games tunes 69 | 70 | Probably didn't lose elo but whatever 71 | 72 | LMR wasn't tuned due to an initial bug 73 | 74 | LTC 40.0+0.40 | Pohl 75 | ``` 76 | Score of New vs Master: 211 - 205 - 535 [0.503] 951 77 | Elo difference: 2.2 +/- 14.6, LOS: 61.6 %, DrawRatio: 56.3 % 78 | Ordo: +2.4 79 | ``` 80 | 81 | ## 11/2/2023 82 | 83 | - New bucketed net: bingshan 84 | 85 | Positive elo gain, just trust me 86 | 87 | ## 10/30/2023 88 | 89 | - Simplify History (formula from Stormphrax) 90 | 91 | STC 8.0+0.08 | Pohl 92 | ``` 93 | Score of New vs Master: 228 - 199 - 490 [0.516] 917 94 | Elo difference: 11.0 +/- 15.3, LOS: 92.0 %, DrawRatio: 53.4 % 95 | Ordo: +12.0 96 | ``` 97 | 98 | ## 10/22/2023 99 | 100 | - Continuation History 101 | 102 | STC 8.0+0.08 | Pohl 103 | ``` 104 | Score of New vs Master: 878 - 841 - 1714 [0.505] 3433 105 | Elo difference: 3.7 +/- 8.2, LOS: 81.4 %, DrawRatio: 49.9 % 106 | Ordo: +4.2 107 | ``` 108 | 109 | LTC 40.0+0.40 | Pohl 110 | ``` 111 | Score of New vs Master: 97 - 79 - 205 [0.524] 381 112 | Elo difference: 16.4 +/- 23.7 113 | ``` 114 | 115 | ## 10/2/2023 116 | 117 | - Change to standard aspiration window algorithm 118 | 119 | STC 8.0+0.08 | Pohl 120 | ``` 121 | Score of New vs Master: 388 - 326 - 795 [0.521] 1509 122 | Elo difference: 14.3 +/- 12.0, LOS: 99.0 %, DrawRatio: 52.7 % 123 | Ordo: +15.8 124 | ``` 125 | 126 | LTC 40.0+0.40 | Pohl 127 | ``` 128 | Score of New vs Master: 158 - 142 - 376 [0.512] 676 129 | Ordo: +9.6 130 | ``` 131 | 132 | # v2.0.0 133 | 134 | ## 9/23/2023 135 | 136 | - New net: Xuebeng 137 | - 512 hidden neurons 138 | - Squared Clipped ReLU 139 | 140 | LTC 40.0+0.40 | Pohl 141 | ``` 142 | Score of New vs Master: 616 - 497 - 1088 [0.527] 2201 143 | Elo difference: 18.8 +/- 10.3, LOS: 100.0 %, DrawRatio: 49.4 % 144 | SPRT: llr 2.95 (100.1%), lbound -2.94, ubound 2.94 - H1 was accepted 145 | Ordo: +21.6 146 | ``` 147 | 148 | ~+2 in STC 149 | 150 | ## 9/20/2023 151 | 152 | - Progress Check/Regression 153 | 154 | LTC 40.0+0.40 | Pohl 155 | ``` 156 | Score of Dev vs v1.5.0: 274 - 40 - 186 [0.734] 500 157 | Elo difference: 176.3 +/- 25.0, LOS: 100.0 %, DrawRatio: 37.2 % 158 | Ordo: +215.2 159 | 160 | Score of Dev vs Defenchess 2.2: 222 - 139 - 139 [0.583] 500 161 | Elo difference: 58.2 +/- 26.1, LOS: 100.0 %, DrawRatio: 27.8 % 162 | Ordo: +67.8 163 | ``` 164 | 165 | SMP 4CPU 20.0+0.20 | Pohl 166 | ``` 167 | Score of Dev vs v1.5.0: 57 - 12 - 31 [0.725] 100 168 | Elo difference: 168.4 +/- 60.5, LOS: 100.0 %, DrawRatio: 31.0 % 169 | Ordo: +190.8 170 | ``` 171 | 172 | Estimate CCRL 3350 1CPU Blitz, 3350 4CPU 40/15, 3260 1CPU 40/15. 173 | 174 | ## 9/16/2023 175 | 176 | - Consider History in LMR 177 | 178 | STC 8.0+0.08 | Pohl 179 | ``` 180 | Score of New_16 vs Master_16: 326 - 322 - 715 [0.501] 1363 181 | Elo difference: 1.0 +/- 12.7, LOS: 56.2 %, DrawRatio: 52.5 % 182 | ``` 183 | 184 | ## 9/15/2023 185 | 186 | - Simplify LMR logic for captures 187 | 188 | STC 8.0+0.08 | Pohl 189 | ``` 190 | Score of New vs Master: 1402 - 1353 - 3346 [0.504] 6101 191 | Elo difference: 2.8 +/- 5.9, LOS: 82.5 %, DrawRatio: 54.8 % 192 | SPRT: llr 2.96 (100.6%), lbound -2.94, ubound 2.94 - H1 was accepted 193 | Ordo: +3.2 194 | ``` 195 | 196 | LTC 40.0+0.40 | Pohl 197 | ``` 198 | Score of New vs Master: 128 - 124 - 296 [0.504] 548 199 | Elo difference: 2.5 +/- 19.7, LOS: 59.9 %, DrawRatio: 54.0 % 200 | Ordo: +2.4 201 | ``` 202 | 203 | ## 9/14/2023 204 | 205 | - Flip LMR condition for pv 206 | 207 | STC 8.0+0.08 | Pohl 208 | ``` 209 | Score of New vs Master: 331 - 291 - 748 [0.515] 1370 210 | Elo difference: 10.1 +/- 12.4, LOS: 94.6 %, DrawRatio: 54.6 % 211 | Ordo: +11.0 212 | ``` 213 | 214 | ## 9/5/2023 215 | 216 | - Fix SEE bug where threshold > pawn 217 | 218 | STC 8.0+0.08 | Pohl 219 | ``` 220 | Score of New vs Master: 305 - 205 - 617 [0.544] 1127 221 | Elo difference: 30.9 +/- 13.6, LOS: 100.0 %, DrawRatio: 54.7 % 222 | SPRT: llr 2.96 (100.7%), lbound -2.94, ubound 2.94 - H1 was accepted 223 | ``` 224 | 225 | ## 9/4/2023 226 | 227 | - Scale history down ("Gravity") 228 | - Suggested by Engine Programming Discord server 229 | 230 | STC 8.0+0.08 | Pohl 231 | ``` 232 | Score of New vs Master: 730 - 696 - 1705 [0.505] 3131 233 | Elo difference: 3.8 +/- 8.2, LOS: 81.6 %, DrawRatio: 54.5 % 234 | Ordo: +4.2 235 | ``` 236 | 237 | ## 9/2/2023 238 | 239 | - Age History instead of clearing it after each search 240 | - Mostly STC gains because LTC recalculates histories 241 | 242 | STC 8.0+0.08 | Pohl 243 | ``` 244 | Score of New vs Master: 218 - 159 - 430 [0.537] 807 245 | Elo difference: 25.4 +/- 16.4, LOS: 99.9 %, DrawRatio: 53.3 % 246 | SPRT: llr 2.95 (100.1%), lbound -2.94, ubound 2.94 - H1 was accepted 247 | Ordo: +27.8 248 | ``` 249 | 250 | LTC 40.0+0.40 | Pohl 251 | ``` 252 | Score of New vs Master: 69 - 65 - 226 [0.506] 360 253 | Elo difference: 3.9 +/- 21.9, LOS: 63.5 %, DrawRatio: 62.8 % 254 | Ordo: +4.2 255 | ``` 256 | 257 | ## 8/30/2023 258 | 259 | - Regression Test vs 1.5.0 260 | 261 | STC 8.0+0.08 | Pohl 262 | ``` 263 | Score of New vs v1.5.0: 161 - 34 - 83 [0.728] 278 264 | Elo difference: 171.4 +/- 36.4, LOS: 100.0 %, DrawRatio: 29.9 % 265 | SPRT: llr 2.96 (100.4%), lbound -2.94, ubound 2.94 - H1 was accepted 266 | Ordo: +188.3 267 | ``` 268 | 269 | LTC 40.0+0.40 | Pohl 270 | ``` 271 | Score of New vs v1.5.0: 162 - 43 - 115 [0.686] 320 272 | Elo difference: 135.7 +/- 31.4, LOS: 100.0 %, DrawRatio: 35.9 % 273 | SPRT: llr 2.95 (100.1%), lbound -2.94, ubound 2.94 - H1 was accepted 274 | Ordo: +154.5 275 | ``` 276 | 277 | ## 8/29/2023 278 | 279 | - TT Aging 280 | 281 | STC 8.0+0.08 | Pohl 282 | ``` 283 | Score of New vs Master: 608 - 575 - 1304 [0.507] 2487 284 | Elo difference: 4.6 +/- 9.4, LOS: 83.1 %, DrawRatio: 52.4 % 285 | Ordo: +5.0 286 | ``` 287 | 288 | LTC 40.0+0.40 | Pohl 289 | ``` 290 | Score of New vs Master: 42 - 34 - 82 [0.525] 158 291 | Elo difference: 17.6 +/- 37.7, LOS: 82.1 %, DrawRatio: 51.9 % 292 | Ordo: +21.3 293 | ``` 294 | 295 | ## 8/26/2023 296 | 297 | - Material Scaling 298 | 299 | STC 10.0+0.10 | 8moves_v3 300 | ``` 301 | Score of New vs Master: 178 - 163 - 659 [0.507] 1000 302 | Elo difference: 5.2 +/- 12.6, LOS: 79.2 %, DrawRatio: 65.9 % 303 | Ordo: +5.2 304 | ``` 305 | 306 | LTC 40.0+0.40 | Pohl 307 | ``` 308 | Score of New vs Master: 161 - 143 - 361 [0.514] 665 309 | Cutechess output lost 310 | Ordo: +11.0 311 | ``` 312 | 313 | ## 8/25/2023 314 | 315 | - 50 move count scaling 316 | 317 | STC 10.0+0.10 | 8moves_v3 318 | ``` 319 | Score of New vs Master: 1152 - 1072 - 4332 [0.506] 6556 320 | Elo difference: 4.2 +/- 4.9, LOS: 95.5 %, DrawRatio: 66.1 % 321 | SPRT: llr 1.39 (47.3%), lbound -2.94, ubound 2.94 322 | Ordo: +4.3 323 | ``` 324 | 325 | LTC 40.0+0.40 | Pohl 326 | ``` 327 | Score of New vs Master: 339 - 325 - 885 [0.505] 1549 328 | Elo difference: 3.1 +/- 11.3, LOS: 70.7 %, DrawRatio: 57.1 % 329 | SPRT: llr 0.0958 (3.3%), lbound -2.94, ubound 2.94 330 | Ordo: +3.6 331 | ``` 332 | 333 | ## 8/18/2023 334 | 335 | - New net: net008b 336 | 337 | STC 15.0+0.10 | 1000 games | 8moves_v3 338 | ``` 339 | Score of New vs Master: 284 - 174 - 542 [0.555] 1000 340 | Elo difference: 38.4 +/- 14.5, LOS: 100.0 %, DrawRatio: 54.2 % 341 | Ordo: +39.1 342 | ``` 343 | 344 | ## 8/16/2023 345 | 346 | - Switch to new NNUE architecture 347 | - New net: net007b 348 | 349 | STC 20.0+0.10 | 1000 games | 8moves_v3 350 | ``` 351 | Score of New vs Master: 404 - 162 - 434 [0.621] 1000 352 | Elo difference: 85.8 +/- 16.3, LOS: 100.0 %, DrawRatio: 43.4 % 353 | Ordo: +86.9 354 | ``` 355 | 356 | LTC 40.0+0.40 | 300 games | Pohl 357 | ``` 358 | Score of New vs Master: 127 - 74 - 99 [0.588] 300 359 | Elo difference: 62.0 +/- 32.5, LOS: 100.0 %, DrawRatio: 33.0 % 360 | Ordo: +76.8 361 | ``` 362 | 363 | ## 8/5/2023 364 | 365 | - Fix NMP bug 366 | 367 | STC 20.0+0.10 | 1000 games | 8moves_v3 368 | ``` 369 | Score of New vs Master: 170 - 127 - 703 [0.521] 1000 370 | Elo difference: 14.9 +/- 11.7, LOS: 99.4 %, DrawRatio: 70.3 % 371 | ``` 372 | 373 | LTC 60.0+0.60 | 1000 games | 8moves_v3 374 | ``` 375 | Score of New vs Master: 134 - 144 - 722 [0.495] 1000 376 | Elo difference: -3.5 +/- 11.3, LOS: 27.4 %, DrawRatio: 72.2 % 377 | ``` 378 | 379 | 30.0+0.30 VS 1.5.0: 380 | ``` 381 | Score of Master vs 1.5.0: 328 - 258 - 1414 [0.517] 2000 382 | Elo difference: 12.2 +/- 8.2, LOS: 99.8 %, DrawRatio: 70.7 % 383 | ``` 384 | 385 | - Do more NMP when position is improving 386 | 387 | STC 30.0+0.20 | 1000 games | noob_4moves 388 | ``` 389 | Score of New vs Master: 140 - 121 - 739 [0.509] 1000 390 | Elo difference: 6.6 +/- 11.0, LOS: 88.0 %, DrawRatio: 73.9 % 391 | ``` 392 | 393 | ## 8/4/2023 394 | 395 | - New RFP parameters from Viridithas 396 | 397 | STC 15.0+0.10 | 2000 games | 8moves_v3 398 | ``` 399 | Score of New vs Master: 357 - 337 - 1306 [0.505] 2000 400 | Elo difference: 3.5 +/- 9.0, LOS: 77.6 %, DrawRatio: 65.3 % 401 | ``` 402 | 403 | LTC 60.0+0.60 | 1000 games | 8moves_v3 404 | ``` 405 | Score of New vs Master: 143 - 127 - 730 [0.508] 1000 406 | Elo difference: 5.6 +/- 11.2, LOS: 83.5 %, DrawRatio: 73.0 % 407 | ``` 408 | 409 | ## 8/3/2023 410 | 411 | - New LMR parameters from Viridithas 412 | 413 | STC 14.0+0.10 | 1000 games | 8moves_v3 414 | ``` 415 | 181 - 173 - 646 [0.504] 1000 416 | Elo difference: 2.8 +/- 12.8, LOS: 66.5 %, DrawRatio: 64.6 % 417 | ``` 418 | 419 | LTC 50.0+0.50 | 1000 games | 8moves_v3 420 | ``` 421 | [0.510] 1000 422 | Elo difference: 7.0 423 | DrawRatio: 76.52 % 424 | ``` 425 | 426 | 427 | ## v1.5.0 428 | 429 | - Optimizations 430 | - Search Tuning 431 | - Stronger Neural Network 432 | - Trained on over 25 Million depth 8 positions from lichess elite database 433 | - Trained on 1.5 Million depth 10 endgame positions 434 | - LazySMP Implementation 435 | 436 | ## v1.4.0 437 | 438 | - Search Improvements 439 | - Manual Tuning 440 | - NNUE Optimizations 441 | - Time Management 442 | 443 | ## v1.3.1 444 | 445 | - Search Improvements 446 | - Countermove heuristic fix 447 | - Tuning 448 | 449 | ## v1.3.0 450 | 451 | - Stronger Neural Network trained on 2GB of data 452 | - Countermove Heuristics 453 | - Higher bounds for History Heuristics 454 | - Improved Aspiration Window 455 | 456 | ## v1.2.0 457 | 458 | - Movegen Bug fixes 459 | - Tuned Search parameters 460 | - Search Rewrite 461 | - Better SEE 462 | - Stronger Neural Network (depth 8, 500 epoch) featuring 8 buckets 463 | 464 | ## v1.1.0 465 | 466 | - NNUE Optimizations 467 | - Singular Extension / MultiCut 468 | - More Aggressive Prunings 469 | 470 | ## v1.0.0 471 | 472 | - Faster Movegen: heavily inspired by Surge 473 | - Complete Core Rewrite 474 | - 512-neuron NNUE trained on 50 million positions on depth 4 475 | 476 | ## v0.2.2 477 | 478 | - Bug fixes 479 | - LMR tuning 480 | - New SEE algorithm 481 | - Aspiration Windows 482 | 483 | ## v0.2.1 484 | 485 | - Bug fixes 486 | - UCI options 487 | - Improvements on Search 488 | 489 | ## v0.2: Search 490 | 491 | - History heuristics, killer heuristics 492 | - Better LMR 493 | - Reversed Futility Pruning 494 | - Null Move Pruning 495 | - Razoring 496 | - Time management 497 | - Better Transposition Table 498 | - Static Exchange Evaluation 499 | - Stronger NNUE network: Flake 2 500 | - Trained on human games on https://database.lichess.org/ and more engine games. 501 | - Trained on one million endgame positions 502 | - 728 -> dense -> 512 -> clipped_relu -> 512 -> dense -> 1 + PSQT 503 | 504 | ## v0.1: NNUE, ~1700 ELO 505 | 506 | - Efficiently Updatable Neural Network trained on top-level engine tournaments 507 | - Current model: 728 -> dense -> 128 -> clipped_relu -> 128 -> dense -> 5 + PSQT 508 | - Forward Pass 509 | - Tuned LMR 510 | - Bishop pair, doubled pawns, etc. 511 | 512 | ## v0.0: Base, ~1400 ELO 513 | 514 | - Bitboard board representation 515 | - Magic bitboards 516 | - Negamax Search with Alpha-Beta pruning 517 | - Quiescence Search with stand-pat pruning 518 | - MVV_LVA 519 | - LMR 520 | - HCE PSQT Evaluation 521 | -------------------------------------------------------------------------------- /src/chess/types.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const position = @import("position.zig"); 3 | 4 | pub const N_COLORS: usize = 2; 5 | pub const Color = enum(u8) { 6 | White, 7 | Black, 8 | pub inline fn invert(self: Color) Color { 9 | return @intToEnum(Color, @enumToInt(self) ^ 1); 10 | } 11 | }; 12 | 13 | pub const N_DIRS: usize = 8; 14 | pub const Direction = enum(i32) { 15 | North = 8, 16 | NorthEast = 9, 17 | East = 1, 18 | SouthEast = -7, 19 | South = -8, 20 | SouthWest = -9, 21 | West = -1, 22 | NorthWest = 7, 23 | 24 | // Double Push 25 | NorthNorth = 16, 26 | SouthSouth = -16, 27 | 28 | pub inline fn relative_dir(self: Direction, comptime c: Color) Direction { 29 | return if (c == Color.White) self else @intToEnum(Direction, -@enumToInt(self)); 30 | } 31 | }; 32 | 33 | pub const N_PT: usize = 6; 34 | pub const PieceType = enum(u8) { 35 | Pawn, 36 | Knight, 37 | Bishop, 38 | Rook, 39 | Queen, 40 | King, 41 | 42 | pub inline fn index(self: PieceType) u8 { 43 | return @enumToInt(self); 44 | } 45 | }; 46 | 47 | pub const PieceString = "PNBRQK~>pnbrqk."; 48 | pub const DEFAULT_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -"; 49 | // Tricky position 50 | pub const KIWIPETE_FEN = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -"; 51 | pub const ENDGAME_FEN = "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - "; 52 | 53 | pub const N_PIECES: usize = 15; 54 | pub const Piece = enum(u8) { 55 | WHITE_PAWN, 56 | WHITE_KNIGHT, 57 | WHITE_BISHOP, 58 | WHITE_ROOK, 59 | WHITE_QUEEN, 60 | WHITE_KING, 61 | BLACK_PAWN = 8, 62 | BLACK_KNIGHT, 63 | BLACK_BISHOP, 64 | BLACK_ROOK, 65 | BLACK_QUEEN, 66 | BLACK_KING, 67 | NO_PIECE, 68 | 69 | pub inline fn new(c: Color, pt: PieceType) Piece { 70 | return @intToEnum(Piece, (@enumToInt(c) << 3) + @enumToInt(pt)); 71 | } 72 | 73 | pub inline fn new_comptime(comptime c: Color, comptime pt: PieceType) Piece { 74 | return @intToEnum(Piece, (@enumToInt(c) << 3) + @enumToInt(pt)); 75 | } 76 | 77 | pub inline fn piece_type(self: Piece) PieceType { 78 | return @intToEnum(PieceType, @enumToInt(self) & 0b111); 79 | } 80 | 81 | pub inline fn color(self: Piece) Color { 82 | return @intToEnum(Color, (@enumToInt(self) & 0b1000) >> 3); 83 | } 84 | 85 | pub inline fn index(self: Piece) u8 { 86 | return @enumToInt(self); 87 | } 88 | 89 | pub inline fn pure_index(self: Piece) usize { 90 | return if (@enumToInt(self) <= 5) @enumToInt(self) else @enumToInt(self) - 2; 91 | } 92 | }; 93 | 94 | // Square & Bitboard 95 | 96 | pub const Bitboard = u64; 97 | 98 | pub const N_SQUARES = 64; 99 | 100 | pub const Square = enum(u8) { 101 | // zig fmt: off 102 | a1, b1, c1, d1, e1, f1, g1, h1, 103 | a2, b2, c2, d2, e2, f2, g2, h2, 104 | a3, b3, c3, d3, e3, f3, g3, h3, 105 | a4, b4, c4, d4, e4, f4, g4, h4, 106 | a5, b5, c5, d5, e5, f5, g5, h5, 107 | a6, b6, c6, d6, e6, f6, g6, h6, 108 | a7, b7, c7, d7, e7, f7, g7, h7, 109 | a8, b8, c8, d8, e8, f8, g8, h8, 110 | NO_SQUARE, 111 | // zig fmt: on 112 | 113 | pub inline fn inc(self: *Square) *Square { 114 | self.* = @intToEnum(Square, @enumToInt(self.*) + 1); 115 | return self; 116 | } 117 | 118 | pub inline fn add(self: Square, d: Direction) Square { 119 | return @intToEnum(Square, @enumToInt(self) + @enumToInt(d)); 120 | } 121 | 122 | pub inline fn sub(self: Square, d: Direction) Square { 123 | return @intToEnum(Square, @enumToInt(self) - @enumToInt(d)); 124 | } 125 | 126 | pub inline fn rank(self: Square) Rank { 127 | return @intToEnum(Rank, @enumToInt(self) >> 3); 128 | } 129 | 130 | pub inline fn file(self: Square) File { 131 | return @intToEnum(File, @enumToInt(self) & 0b111); 132 | } 133 | 134 | pub inline fn diagonal(self: Square) i32 { 135 | return @intCast(i32, 7 + @enumToInt(self.rank()) - @enumToInt(self.file())); 136 | } 137 | 138 | pub inline fn anti_diagonal(self: Square) i32 { 139 | return @intCast(i32, @enumToInt(self.rank()) + @enumToInt(self.file())); 140 | } 141 | 142 | pub inline fn new(f: File, r: Rank) Square { 143 | return @intToEnum(Square, @enumToInt(f) | (@enumToInt(r) << 3)); 144 | } 145 | 146 | pub inline fn index(self: Square) u8 { 147 | return @intCast(u8, @enumToInt(self)); 148 | } 149 | }; 150 | 151 | pub inline fn rank_plain(sq: usize) usize { 152 | return sq >> 3; 153 | } 154 | 155 | pub inline fn file_plain(sq: usize) usize { 156 | return sq & 0b111; 157 | } 158 | 159 | pub inline fn diagonal_plain(sq: usize) usize { 160 | return 7 + rank_plain(sq) - file_plain(sq); 161 | } 162 | 163 | pub inline fn anti_diagonal_plain(sq: usize) usize { 164 | return rank_plain(sq) + file_plain(sq); 165 | } 166 | 167 | pub const File = enum(u8) { 168 | AFILE, 169 | BFILE, 170 | CFILE, 171 | DFILE, 172 | EFILE, 173 | FFILE, 174 | GFILE, 175 | HFILE, 176 | 177 | pub inline fn index(self: File) u8 { 178 | return @enumToInt(self); 179 | } 180 | }; 181 | 182 | pub const Rank = enum(u8) { 183 | RANK1, 184 | RANK2, 185 | RANK3, 186 | RANK4, 187 | RANK5, 188 | RANK6, 189 | RANK7, 190 | RANK8, 191 | 192 | pub inline fn index(self: Rank) u8 { 193 | return @enumToInt(self); 194 | } 195 | 196 | pub inline fn relative_rank(self: Rank, comptime c: Color) Rank { 197 | return if (c == Color.White) self else @intToEnum(Rank, @enumToInt(Rank.RANK8) - @enumToInt(self)); 198 | } 199 | }; 200 | 201 | // Magic stuff 202 | 203 | // zig fmt: off 204 | pub const SquareToString = [_][:0]const u8{ 205 | "a1", "b1", "c1", "d1", "e1", "f1", "g1", "h1", 206 | "a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2", 207 | "a3", "b3", "c3", "d3", "e3", "f3", "g3", "h3", 208 | "a4", "b4", "c4", "d4", "e4", "f4", "g4", "h4", 209 | "a5", "b5", "c5", "d5", "e5", "f5", "g5", "h5", 210 | "a6", "b6", "c6", "d6", "e6", "f6", "g6", "h6", 211 | "a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7", 212 | "a8", "b8", "c8", "d8", "e8", "f8", "g8", "h8", 213 | "None" 214 | }; 215 | 216 | pub const MaskFile = [_]Bitboard{ 217 | 0x101010101010101, 0x202020202020202, 0x404040404040404, 0x808080808080808, 218 | 0x1010101010101010, 0x2020202020202020, 0x4040404040404040, 0x8080808080808080, 219 | }; 220 | 221 | pub const MaskRank = [_]Bitboard{ 222 | 0xff, 0xff00, 0xff0000, 0xff000000, 223 | 0xff00000000, 0xff0000000000, 0xff000000000000, 0xff00000000000000 224 | }; 225 | 226 | pub const MaskDiagonal = [_]Bitboard{ 227 | 0x80, 0x8040, 0x804020, 228 | 0x80402010, 0x8040201008, 0x804020100804, 229 | 0x80402010080402, 0x8040201008040201, 0x4020100804020100, 230 | 0x2010080402010000, 0x1008040201000000, 0x804020100000000, 231 | 0x402010000000000, 0x201000000000000, 0x100000000000000, 232 | }; 233 | 234 | pub const MaskAntiDiagonal = [_]Bitboard{ 235 | 0x1, 0x102, 0x10204, 236 | 0x1020408, 0x102040810, 0x10204081020, 237 | 0x1020408102040, 0x102040810204080, 0x204081020408000, 238 | 0x408102040800000, 0x810204080000000, 0x1020408000000000, 239 | 0x2040800000000000, 0x4080000000000000, 0x8000000000000000, 240 | }; 241 | 242 | pub const SquareIndexBB = [_]Bitboard{ 243 | 0x1, 0x2, 0x4, 0x8, 244 | 0x10, 0x20, 0x40, 0x80, 245 | 0x100, 0x200, 0x400, 0x800, 246 | 0x1000, 0x2000, 0x4000, 0x8000, 247 | 0x10000, 0x20000, 0x40000, 0x80000, 248 | 0x100000, 0x200000, 0x400000, 0x800000, 249 | 0x1000000, 0x2000000, 0x4000000, 0x8000000, 250 | 0x10000000, 0x20000000, 0x40000000, 0x80000000, 251 | 0x100000000, 0x200000000, 0x400000000, 0x800000000, 252 | 0x1000000000, 0x2000000000, 0x4000000000, 0x8000000000, 253 | 0x10000000000, 0x20000000000, 0x40000000000, 0x80000000000, 254 | 0x100000000000, 0x200000000000, 0x400000000000, 0x800000000000, 255 | 0x1000000000000, 0x2000000000000, 0x4000000000000, 0x8000000000000, 256 | 0x10000000000000, 0x20000000000000, 0x40000000000000, 0x80000000000000, 257 | 0x100000000000000, 0x200000000000000, 0x400000000000000, 0x800000000000000, 258 | 0x1000000000000000, 0x2000000000000000, 0x4000000000000000, 0x8000000000000000, 259 | 0x0 260 | }; 261 | 262 | // zig fmt: on 263 | 264 | pub fn debug_print_bitboard(b: Bitboard) void { 265 | var i: i32 = 56; 266 | while (i >= 0) : (i -= 8) { 267 | var j: i32 = 0; 268 | while (j < 8) : (j += 1) { 269 | if ((b >> @intCast(u6, i + j)) & 1 != 0) { 270 | std.debug.print("1 ", .{}); 271 | } else { 272 | std.debug.print("0 ", .{}); 273 | } 274 | } 275 | std.debug.print("\n", .{}); 276 | } 277 | std.debug.print("\n", .{}); 278 | } 279 | 280 | pub const k1: Bitboard = 0x5555555555555555; 281 | pub const k2: Bitboard = 0x3333333333333333; 282 | pub const k3: Bitboard = 0x0f0f0f0f0f0f0f0f; 283 | pub const kf: Bitboard = 0x0101010101010101; 284 | 285 | pub inline fn popcount(x: Bitboard) i32 { 286 | return @intCast(i32, @popCount(x)); 287 | } 288 | 289 | pub inline fn popcount_usize(x: Bitboard) usize { 290 | return @intCast(usize, @popCount(x)); 291 | } 292 | 293 | pub inline fn lsb(x: Bitboard) i32 { 294 | return @intCast(i32, @ctz(x)); 295 | // Ancient machines: 296 | //const DEBRUIJN64: [64]i32 = .{ 297 | // // zig fmt: off 298 | // 0, 47, 1, 56, 48, 27, 2, 60, 299 | // 57, 49, 41, 37, 28, 16, 3, 61, 300 | // 54, 58, 35, 52, 50, 42, 21, 44, 301 | // 38, 32, 29, 23, 17, 11, 4, 62, 302 | // 46, 55, 26, 59, 40, 36, 15, 53, 303 | // 34, 51, 20, 43, 31, 22, 10, 45, 304 | // 25, 39, 14, 33, 19, 30, 9, 24, 305 | // 13, 18, 8, 12, 7, 6, 5, 63 306 | // // zig fmt: on 307 | //}; 308 | // const MagicNumber: Bitboard = 0x03f79d71b4cb0a89; 309 | // return DEBRUIJN64[(MagicNumber *% (x ^ (x-%1))) >> 58]; 310 | } 311 | 312 | pub inline fn pop_lsb(x: *Bitboard) Square { 313 | var l = lsb(x.*); 314 | x.* &= x.* - 1; 315 | return @intToEnum(Square, l); 316 | } 317 | 318 | pub inline fn shift_bitboard(x: Bitboard, comptime d: Direction) Bitboard { 319 | return switch (d) { 320 | Direction.North => x << 8, 321 | Direction.South => x >> 8, 322 | Direction.NorthNorth => x << 16, 323 | Direction.SouthSouth => x >> 16, 324 | Direction.East => (x & ~MaskFile[@enumToInt(File.HFILE)]) << 1, 325 | Direction.West => (x & ~MaskFile[@enumToInt(File.AFILE)]) >> 1, 326 | Direction.NorthEast => (x & ~MaskFile[@enumToInt(File.HFILE)]) << 9, 327 | Direction.NorthWest => (x & ~MaskFile[@enumToInt(File.AFILE)]) << 7, 328 | Direction.SouthEast => (x & ~MaskFile[@enumToInt(File.HFILE)]) >> 7, 329 | Direction.SouthWest => (x & ~MaskFile[@enumToInt(File.AFILE)]) >> 9, 330 | }; 331 | } 332 | 333 | pub const MoveTypeString = [_][:0]const u8{ "", "", " O-O", " O-O-O", "N", "B", "R", "Q", " (capture)", "", " e.p.", "", "N (capture)", "B (capture)", "R (capture)", "Q (capture)" }; 334 | pub const PromMoveTypeString = [_][:0]const u8{ "", "", "", " ", "n", "b", "r", "q", "", "", "", "", "n", "b", "r", "q" }; 335 | 336 | pub const MoveFlags = enum(u4) { 337 | QUIET = 0b0000, 338 | DOUBLE_PUSH = 0b0001, 339 | OO = 0b0010, 340 | OOO = 0b0011, 341 | CAPTURE = 0b1000, 342 | CAPTURES = 0b1111, 343 | EN_PASSANT = 0b1010, 344 | PROMOTIONS = 0b0111, 345 | PROMOTION_CAPTURES = 0b1100, 346 | 347 | PR_KNIGHT = 0b0100, 348 | PR_BISHOP = 0b0101, 349 | PR_ROOK = 0b0110, 350 | PC_BISHOP = 0b1101, 351 | PC_ROOK = 0b1110, 352 | 353 | pub inline fn promote_type(self: MoveFlags) PieceType { 354 | return switch (@enumToInt(self) & @enumToInt(MoveFlags.PROMOTIONS)) { 355 | PR_KNIGHT => PieceType.Knight, 356 | PR_BISHOP => PieceType.Bishop, 357 | PR_ROOK => PieceType.Rook, 358 | PR_QUEEN => PieceType.Queen, 359 | else => unreachable, 360 | }; 361 | } 362 | }; 363 | 364 | pub const PR_KNIGHT: u4 = 0b0100; 365 | pub const PR_BISHOP: u4 = 0b0101; 366 | pub const PR_ROOK: u4 = 0b0110; 367 | pub const PR_QUEEN: u4 = 0b0111; 368 | pub const PC_KNIGHT: u4 = 0b1100; 369 | pub const PC_BISHOP: u4 = 0b1101; 370 | pub const PC_ROOK: u4 = 0b1110; 371 | pub const PC_QUEEN: u4 = 0b1111; 372 | 373 | // Packed Struct makes it fit into a 16-bit integer. 374 | pub const Move = packed struct { 375 | flags: u4, 376 | from: u6, 377 | to: u6, 378 | 379 | pub inline fn to_u16(self: Move) u16 { 380 | return @bitCast(u16, self); 381 | } 382 | 383 | pub inline fn get_flags(self: Move) MoveFlags { 384 | return @intToEnum(MoveFlags, self.flags); 385 | } 386 | 387 | pub inline fn get_from(self: Move) Square { 388 | return @intToEnum(Square, self.from); 389 | } 390 | 391 | pub inline fn get_to(self: Move) Square { 392 | return @intToEnum(Square, self.to); 393 | } 394 | 395 | pub inline fn empty() Move { 396 | return Move{ .flags = 0, .from = 0, .to = 0 }; 397 | } 398 | 399 | pub inline fn clone(self: Move) Move { 400 | return Move{ .flags = self.flags, .from = self.from, .to = self.to }; 401 | } 402 | 403 | pub inline fn new_from_to(from: Square, to: Square) Move { 404 | return Move{ .flags = 0, .from = @intCast(u6, @enumToInt(from)), .to = @intCast(u6, @enumToInt(to)) }; 405 | } 406 | 407 | pub inline fn new_from_to_flag(from: Square, to: Square, flag: MoveFlags) Move { 408 | return Move{ .flags = @enumToInt(flag), .from = @intCast(u6, @enumToInt(from)), .to = @intCast(u6, @enumToInt(to)) }; 409 | } 410 | 411 | pub fn new_from_string(pos: *position.Position, move: []const u8) Move { 412 | var list = std.ArrayList(Move).initCapacity(std.heap.c_allocator, 8) catch unreachable; 413 | defer list.deinit(); 414 | var f = @intCast(u6, @enumToInt(Square.new(@intToEnum(File, move[0] - 'a'), @intToEnum(Rank, move[1] - '1')))); 415 | var t = @intCast(u6, @enumToInt(Square.new(@intToEnum(File, move[2] - 'a'), @intToEnum(Rank, move[3] - '1')))); 416 | var p: ?u8 = if (move.len >= 5) move[4] else null; 417 | if (pos.turn == Color.White) { 418 | pos.generate_legal_moves(Color.White, &list); 419 | } else { 420 | pos.generate_legal_moves(Color.Black, &list); 421 | } 422 | 423 | for (list.items) |m| { 424 | if (m.from == f and m.to == t) { 425 | if (p != null) { 426 | if (p.? != PromMoveTypeString[m.flags][0]) { 427 | continue; 428 | } 429 | } 430 | return m; 431 | } 432 | } 433 | std.debug.panic("move not found: {s}", .{move}); 434 | return Move{ 435 | .flags = 0, 436 | .from = f, 437 | .to = t, 438 | }; 439 | } 440 | 441 | pub fn make_all(comptime flag: MoveFlags, from: Square, to: Bitboard, list: *std.ArrayList(Move)) void { 442 | var to_t = to; 443 | while (to_t != 0) { 444 | list.append(Move.new_from_to_flag(from, pop_lsb(&to_t), flag)) catch {}; 445 | } 446 | } 447 | 448 | pub inline fn is_capture(self: Move) bool { 449 | return (self.flags == 8) or (self.flags == 10) or (self.flags >= 12 and self.flags <= 15); 450 | } 451 | 452 | pub inline fn is_promotion(self: Move) bool { 453 | return (self.flags >= 4 and self.flags <= 7) or (self.flags >= 12 and self.flags <= 15); 454 | } 455 | 456 | pub inline fn equals_to(self: Move, other: Move) bool { 457 | return self.from == other.from and self.to == other.to; 458 | } 459 | 460 | pub fn debug_print(self: Move) void { 461 | std.debug.print("{s}{s}{s}", .{ 462 | SquareToString[self.from], 463 | SquareToString[self.to], 464 | MoveTypeString[self.flags], 465 | }); 466 | } 467 | 468 | pub fn uci_print(self: Move, writer: anytype) void { 469 | writer.print("{s}{s}", .{ 470 | SquareToString[self.from], 471 | SquareToString[self.to], 472 | }) catch {}; 473 | if (self.is_promotion()) { 474 | writer.print("{c}", .{ 475 | PromMoveTypeString[self.flags][0], 476 | }) catch {}; 477 | } 478 | } 479 | }; 480 | 481 | pub const WhiteOOMask: Bitboard = 0x90; 482 | pub const WhiteOOOMask: Bitboard = 0x11; 483 | 484 | pub const WhiteOOBetweenMask: Bitboard = 0x60; 485 | pub const WhiteOOOBetweenMask: Bitboard = 0xe; 486 | 487 | pub const BlackOOMask: Bitboard = 0x9000000000000000; 488 | pub const BlackOOOMask: Bitboard = 0x1100000000000000; 489 | 490 | pub const BlackOOBetweenMask: Bitboard = 0x6000000000000000; 491 | pub const BlackOOOBetweenMask: Bitboard = 0xe00000000000000; 492 | 493 | pub const AllCastlingMask: Bitboard = 0x9100000000000091; 494 | 495 | pub inline fn get_oo_mask(comptime color: Color) Bitboard { 496 | return switch (color) { 497 | Color.White => WhiteOOMask, 498 | Color.Black => BlackOOMask, 499 | }; 500 | } 501 | 502 | pub inline fn get_ooo_mask(comptime color: Color) Bitboard { 503 | return switch (color) { 504 | Color.White => WhiteOOOMask, 505 | Color.Black => BlackOOOMask, 506 | }; 507 | } 508 | 509 | pub inline fn get_oo_blocker_mask(comptime color: Color) Bitboard { 510 | return switch (color) { 511 | Color.White => WhiteOOBetweenMask, 512 | Color.Black => BlackOOBetweenMask, 513 | }; 514 | } 515 | 516 | pub inline fn get_ooo_blocker_mask(comptime color: Color) Bitboard { 517 | return switch (color) { 518 | Color.White => WhiteOOOBetweenMask, 519 | Color.Black => BlackOOOBetweenMask, 520 | }; 521 | } 522 | 523 | pub inline fn ignore_ooo_danger(comptime color: Color) Bitboard { 524 | return switch (color) { 525 | Color.White => 0x2, 526 | Color.Black => 0x200000000000000, 527 | }; 528 | } 529 | -------------------------------------------------------------------------------- /src/engine/hce.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const types = @import("../chess/types.zig"); 3 | const position = @import("../chess/position.zig"); 4 | const nnue = @import("nnue.zig"); 5 | 6 | pub const MateScore: i32 = 888888; 7 | 8 | pub const UseNNUE = true; 9 | 10 | pub const Mateiral: [6][2]i32 = .{ 11 | .{ 82, 94 }, 12 | .{ 337, 281 }, 13 | .{ 365, 297 }, 14 | .{ 477, 512 }, 15 | .{ 1025, 936 }, 16 | .{ 0, 0 }, 17 | }; 18 | 19 | // PeSTO PSQT for testing purposes 20 | pub const PSQT: [6][2][64]i32 = .{ 21 | .{ 22 | .{ 23 | 0, 0, 0, 0, 0, 0, 0, 0, 24 | 98, 134, 61, 95, 68, 126, 34, -11, 25 | -6, 7, 26, 31, 65, 56, 25, -20, 26 | -14, 13, 6, 21, 23, 12, 17, -23, 27 | -27, -2, -5, 12, 17, 6, 10, -25, 28 | -26, -4, -4, -10, 3, 3, 33, -12, 29 | -35, -1, -20, -23, -15, 24, 38, -22, 30 | 0, 0, 0, 0, 0, 0, 0, 0, 31 | }, 32 | .{ 33 | 0, 0, 0, 0, 0, 0, 0, 0, 34 | 178, 173, 158, 134, 147, 132, 165, 187, 35 | 94, 100, 85, 67, 56, 53, 82, 84, 36 | 32, 24, 13, 5, -2, 4, 17, 17, 37 | 13, 9, -3, -7, -7, -8, 3, -1, 38 | 4, 7, -6, 1, 0, -5, -1, -8, 39 | 13, 8, 8, 10, 13, 0, 2, -7, 40 | 0, 0, 0, 0, 0, 0, 0, 0, 41 | }, 42 | }, 43 | .{ 44 | .{ 45 | -167, -89, -34, -49, 61, -97, -15, -107, 46 | -73, -41, 72, 36, 23, 62, 7, -17, 47 | -47, 60, 37, 65, 84, 129, 73, 44, 48 | -9, 17, 19, 53, 37, 69, 18, 22, 49 | -13, 4, 16, 13, 28, 19, 21, -8, 50 | -23, -9, 12, 10, 19, 17, 25, -16, 51 | -29, -53, -12, -3, -1, 18, -14, -19, 52 | -105, -21, -58, -33, -17, -28, -19, -23, 53 | }, 54 | .{ 55 | -58, -38, -13, -28, -31, -27, -63, -99, 56 | -25, -8, -25, -2, -9, -25, -24, -52, 57 | -24, -20, 10, 9, -1, -9, -19, -41, 58 | -17, 3, 22, 22, 22, 11, 8, -18, 59 | -18, -6, 16, 25, 16, 17, 4, -18, 60 | -23, -3, -1, 15, 10, -3, -20, -22, 61 | -42, -20, -10, -5, -2, -20, -23, -44, 62 | -29, -51, -23, -15, -22, -18, -50, -64, 63 | }, 64 | }, 65 | .{ 66 | .{ 67 | -29, 4, -82, -37, -25, -42, 7, -8, 68 | -26, 16, -18, -13, 30, 59, 18, -47, 69 | -16, 37, 43, 40, 35, 50, 37, -2, 70 | -4, 5, 19, 50, 37, 37, 7, -2, 71 | -6, 13, 13, 26, 34, 12, 10, 4, 72 | 0, 15, 15, 15, 14, 27, 18, 10, 73 | 4, 15, 16, 0, 7, 21, 33, 1, 74 | -33, -3, -14, -21, -13, -12, -39, -21, 75 | }, 76 | .{ 77 | -14, -21, -11, -8, -7, -9, -17, -24, 78 | -8, -4, 7, -12, -3, -13, -4, -14, 79 | 2, -8, 0, -1, -2, 6, 0, 4, 80 | -3, 9, 12, 9, 14, 10, 3, 2, 81 | -6, 3, 13, 19, 7, 10, -3, -9, 82 | -12, -3, 8, 10, 13, 3, -7, -15, 83 | -14, -18, -7, -1, 4, -9, -15, -27, 84 | -23, -9, -23, -5, -9, -16, -5, -17, 85 | }, 86 | }, 87 | .{ 88 | .{ 89 | 32, 42, 32, 51, 63, 9, 31, 43, 90 | 27, 32, 58, 62, 80, 67, 26, 44, 91 | -5, 19, 26, 36, 17, 45, 61, 16, 92 | -24, -11, 7, 26, 24, 35, -8, -20, 93 | -36, -26, -12, -1, 9, -7, 6, -23, 94 | -45, -25, -16, -17, 3, 0, -5, -33, 95 | -44, -16, -20, -9, -1, 11, -6, -71, 96 | -19, -13, 1, 17, 16, 7, -37, -26, 97 | }, 98 | .{ 99 | 13, 10, 18, 15, 12, 12, 8, 5, 100 | 11, 13, 13, 11, -3, 3, 8, 3, 101 | 7, 7, 7, 5, 4, -3, -5, -3, 102 | 4, 3, 13, 1, 2, 1, -1, 2, 103 | 3, 5, 8, 4, -5, -6, -8, -11, 104 | -4, 0, -5, -1, -7, -12, -8, -16, 105 | -6, -6, 0, 2, -9, -9, -11, -3, 106 | -9, 2, 3, -1, -5, -13, 4, -20, 107 | }, 108 | }, 109 | .{ 110 | .{ 111 | -28, 0, 29, 12, 59, 44, 43, 45, 112 | -24, -39, -5, 1, -16, 57, 28, 54, 113 | -13, -17, 7, 8, 29, 56, 47, 57, 114 | -27, -27, -16, -16, -1, 17, -2, 1, 115 | -9, -26, -9, -10, -2, -4, 3, -3, 116 | -14, 2, -11, -2, -5, 2, 14, 5, 117 | -35, -8, 11, 2, 8, 15, -3, 1, 118 | -1, -18, -9, 10, -15, -25, -31, -50, 119 | }, 120 | .{ 121 | -9, 22, 22, 27, 27, 19, 10, 20, 122 | -17, 20, 32, 41, 58, 25, 30, 0, 123 | -20, 6, 9, 49, 47, 35, 19, 9, 124 | 3, 22, 24, 45, 57, 40, 57, 36, 125 | -18, 28, 19, 47, 31, 34, 39, 23, 126 | -16, -27, 15, 6, 9, 17, 10, 5, 127 | -22, -23, -30, -16, -16, -23, -36, -32, 128 | -33, -28, -22, -43, -5, -32, -20, -41, 129 | }, 130 | }, 131 | .{ 132 | .{ 133 | -65, 23, 16, -15, -56, -34, 2, 13, 134 | 29, -1, -20, -7, -8, -4, -38, -29, 135 | -9, 24, 2, -16, -20, 6, 22, -22, 136 | -17, -20, -12, -27, -30, -25, -14, -36, 137 | -49, -1, -27, -39, -46, -44, -33, -51, 138 | -14, -14, -22, -46, -44, -30, -15, -27, 139 | 1, 7, -8, -64, -43, -16, 9, 8, 140 | -15, 36, 12, -54, 8, -28, 24, 14, 141 | }, 142 | .{ 143 | -74, -35, -18, -18, -11, 15, 4, -17, 144 | -12, 17, 14, 17, 17, 38, 23, 11, 145 | 10, 17, 23, 15, 20, 45, 44, 13, 146 | -8, 22, 24, 27, 26, 33, 26, 3, 147 | -18, -4, 21, 24, 27, 23, 9, -11, 148 | -19, -3, 11, 21, 23, 16, 7, -9, 149 | -27, -11, 4, 13, 14, 4, -5, -17, 150 | -53, -34, -21, -11, -28, -14, -24, -43, 151 | }, 152 | }, 153 | }; 154 | 155 | const CenterManhattanDistance = [64]i32{ 156 | 6, 5, 4, 3, 3, 4, 5, 6, 157 | 5, 4, 3, 2, 2, 3, 4, 5, 158 | 4, 3, 2, 1, 1, 2, 3, 4, 159 | 3, 2, 1, 0, 0, 1, 2, 3, 160 | 3, 2, 1, 0, 0, 1, 2, 3, 161 | 4, 3, 2, 1, 1, 2, 3, 4, 162 | 5, 4, 3, 2, 2, 3, 4, 5, 163 | 6, 5, 4, 3, 3, 4, 5, 6, 164 | }; 165 | 166 | pub const DynamicEvaluator = struct { 167 | score_mg: i32 = 0, 168 | score_eg_non_mat: i32 = 0, 169 | score_eg_material: i32 = 0, 170 | nnue_evaluator: nnue.NNUE = nnue.NNUE.new(), 171 | need_hce: bool = false, 172 | 173 | pub inline fn add_piece(self: *DynamicEvaluator, pc: types.Piece, sq: types.Square, _: *position.Position) void { 174 | if (UseNNUE) { 175 | self.nnue_evaluator.toggle(true, pc, sq); 176 | } 177 | if (self.need_hce) { 178 | const i = pc.piece_type().index(); 179 | if (pc.color() == types.Color.White) { 180 | self.score_mg += Mateiral[i][0]; 181 | self.score_mg += PSQT[i][0][sq.index() ^ 56]; 182 | self.score_eg_material += Mateiral[i][1]; 183 | self.score_eg_non_mat += PSQT[i][1][sq.index() ^ 56]; 184 | } else { 185 | self.score_mg -= Mateiral[i][0]; 186 | self.score_mg -= PSQT[i][0][sq.index()]; 187 | self.score_eg_material -= Mateiral[i][1]; 188 | self.score_eg_non_mat -= PSQT[i][1][sq.index()]; 189 | } 190 | } 191 | } 192 | 193 | pub inline fn remove_piece(self: *DynamicEvaluator, sq: types.Square, pos: *position.Position) void { 194 | const pc = pos.mailbox[sq.index()]; 195 | 196 | if (pc != types.Piece.NO_PIECE) { 197 | if (UseNNUE) { 198 | self.nnue_evaluator.toggle(false, pc, sq); 199 | } 200 | if (self.need_hce) { 201 | const i = pc.piece_type().index(); 202 | if (pc.color() == types.Color.White) { 203 | self.score_mg -= Mateiral[i][0]; 204 | self.score_mg -= PSQT[i][0][sq.index() ^ 56]; 205 | self.score_eg_material -= Mateiral[i][1]; 206 | self.score_eg_non_mat -= PSQT[i][1][sq.index() ^ 56]; 207 | } else { 208 | self.score_mg += Mateiral[i][0]; 209 | self.score_mg += PSQT[i][0][sq.index()]; 210 | self.score_eg_material += Mateiral[i][1]; 211 | self.score_eg_non_mat += PSQT[i][1][sq.index()]; 212 | } 213 | } 214 | } 215 | } 216 | 217 | pub inline fn move_piece(self: *DynamicEvaluator, from: types.Square, to: types.Square, pos: *position.Position) void { 218 | self.remove_piece(to, pos); 219 | self.move_piece_quiet(from, to, pos); 220 | } 221 | 222 | pub inline fn move_piece_quiet(self: *DynamicEvaluator, from: types.Square, to: types.Square, pos: *position.Position) void { 223 | const pc = pos.mailbox[from.index()]; 224 | if (pc != types.Piece.NO_PIECE) { 225 | if (UseNNUE) { 226 | self.nnue_evaluator.toggle(false, pc, from); 227 | self.nnue_evaluator.toggle(true, pc, to); 228 | } 229 | if (self.need_hce) { 230 | const i = pc.piece_type().index(); 231 | if (pc.color() == types.Color.White) { 232 | self.score_mg -= PSQT[i][0][from.index() ^ 56]; 233 | self.score_mg += PSQT[i][0][to.index() ^ 56]; 234 | self.score_eg_non_mat -= PSQT[i][1][from.index() ^ 56]; 235 | self.score_eg_non_mat += PSQT[i][1][to.index() ^ 56]; 236 | } else { 237 | self.score_mg += PSQT[i][0][from.index()]; 238 | self.score_mg -= PSQT[i][0][to.index()]; 239 | self.score_eg_non_mat += PSQT[i][1][from.index()]; 240 | self.score_eg_non_mat -= PSQT[i][1][to.index()]; 241 | } 242 | } 243 | } 244 | } 245 | 246 | pub fn full_refresh(self: *DynamicEvaluator, pos: *position.Position) void { 247 | if (UseNNUE) { 248 | self.nnue_evaluator.refresh_accumulator(pos); 249 | } 250 | self.refresh_hce(pos); 251 | } 252 | 253 | pub fn refresh_hce(self: *DynamicEvaluator, pos: *position.Position) void { 254 | var mg: i32 = 0; 255 | var eg_material: i32 = 0; 256 | var eg_non_mat: i32 = 0; 257 | for (pos.mailbox) |piece, index| { 258 | if (piece == types.Piece.NO_PIECE) { 259 | continue; 260 | } 261 | var i = piece.piece_type().index(); 262 | if (piece.color() == types.Color.White) { 263 | mg += Mateiral[i][0]; 264 | mg += PSQT[i][0][index ^ 56]; 265 | eg_material += Mateiral[i][1]; 266 | eg_non_mat += PSQT[i][1][index ^ 56]; 267 | } else { 268 | mg -= Mateiral[i][0]; 269 | mg -= PSQT[i][0][index]; 270 | eg_material -= Mateiral[i][1]; 271 | eg_non_mat -= PSQT[i][1][index]; 272 | } 273 | } 274 | 275 | self.score_mg = mg; 276 | self.score_eg_material = eg_material; 277 | self.score_eg_non_mat = eg_non_mat; 278 | } 279 | }; 280 | 281 | pub inline fn distance_eval(pos: *position.Position, comptime white_winning: bool) i32 { 282 | var k1 = @intToEnum(types.Square, types.lsb(pos.piece_bitboards[types.Piece.WHITE_KING.index()])); 283 | var k2 = @intToEnum(types.Square, types.lsb(pos.piece_bitboards[types.Piece.BLACK_KING.index()])); 284 | 285 | var r1 = @intCast(i32, k1.rank().index()); 286 | var r2 = @intCast(i32, k2.rank().index()); 287 | var c1 = @intCast(i32, k1.file().index()); 288 | var c2 = @intCast(i32, k2.file().index()); 289 | 290 | var score: i32 = 0; 291 | var m_dist: i32 = (std.math.absInt(r1 - r2) catch 0) + (std.math.absInt(c1 - c2) catch 0); 292 | 293 | if (white_winning) { 294 | score -= m_dist * 5; 295 | score += CenterManhattanDistance[k2.index()] * 10; 296 | } else { 297 | score += m_dist * 5; 298 | score -= CenterManhattanDistance[k1.index()] * 10; 299 | } 300 | 301 | return score; 302 | } 303 | 304 | pub fn evaluate_comptime(pos: *position.Position, comptime color: types.Color) i32 { 305 | var phase = pos.phase(); 306 | var result: i32 = 0; 307 | if (UseNNUE and (phase >= 3 or pos.has_pawns())) { 308 | result = evaluate_nnue_comptime(pos, color); 309 | } else { 310 | if (!pos.evaluator.need_hce) { 311 | pos.evaluator.need_hce = true; 312 | pos.evaluator.refresh_hce(pos); 313 | } 314 | // Tapered eval 315 | 316 | var mg_phase: i32 = 0; 317 | var eg_phase: i32 = 0; 318 | var mg_score: i32 = 0; 319 | var eg_score: i32 = 0; 320 | 321 | mg_phase = @intCast(i32, phase); 322 | if (mg_phase > 24) { 323 | mg_phase = 24; 324 | } 325 | eg_phase = 24 - mg_phase; 326 | 327 | mg_score = pos.evaluator.score_mg; 328 | eg_score = pos.evaluator.score_eg_material; 329 | 330 | while (true) { 331 | // Late endgame with one side winning 332 | if (phase <= 4 and phase >= 1 and !pos.has_pawns()) { 333 | if (pos.piece_bitboards[types.Piece.BLACK_KING.index()] == pos.all_pieces(types.Color.Black)) { 334 | // White is winning 335 | eg_score += distance_eval(pos, true); 336 | eg_score += @divTrunc(pos.evaluator.score_eg_non_mat, 2); 337 | eg_score = @max(100, eg_score - @intCast(i32, pos.history[pos.game_ply].fifty)); 338 | break; 339 | } else if (pos.piece_bitboards[types.Piece.WHITE_KING.index()] == pos.all_pieces(types.Color.White)) { 340 | // Black is winning 341 | eg_score += distance_eval(pos, false); 342 | eg_score += @divTrunc(pos.evaluator.score_eg_non_mat, 2); 343 | eg_score = @min(-100, eg_score + @intCast(i32, pos.history[pos.game_ply].fifty)); 344 | break; 345 | } 346 | } 347 | 348 | eg_score += pos.evaluator.score_eg_non_mat; 349 | 350 | break; 351 | } 352 | 353 | var score = @divTrunc(mg_score * mg_phase + eg_score * eg_phase, 24); 354 | if (color == types.Color.White) { 355 | result = score; 356 | } else { 357 | result = -score; 358 | } 359 | } 360 | 361 | if (phase <= 5 and std.math.absInt(result) catch 0 >= 16 and is_material_drawish(pos)) { 362 | const drawish_factor: i32 = 8; 363 | result = @divTrunc(result, drawish_factor); 364 | } 365 | 366 | // Scaling idea from Clover 367 | result = @divTrunc(result * (700 + @divTrunc(pos.phase_material(), 32) - @intCast(i32, pos.history[pos.game_ply].fifty) * 5), 1024); 368 | 369 | return result; 370 | } 371 | 372 | pub inline fn evaluate_nnue(pos: *position.Position) i32 { 373 | return pos.evaluator.nnue_evaluator.evaluate(pos.turn); 374 | } 375 | 376 | pub inline fn evaluate_nnue_comptime(pos: *position.Position, comptime color: types.Color) i32 { 377 | return pos.evaluator.nnue_evaluator.evaluate_comptime(color, pos); 378 | } 379 | 380 | pub inline fn is_material_draw(pos: *position.Position) bool { 381 | var all = pos.all_pieces(types.Color.White) | pos.all_pieces(types.Color.Black); 382 | var kings = pos.piece_bitboards[types.Piece.WHITE_KING.index()] | pos.piece_bitboards[types.Piece.BLACK_KING.index()]; 383 | 384 | if (kings == all) { 385 | return true; 386 | } 387 | 388 | var wb = pos.piece_bitboards[types.Piece.WHITE_BISHOP.index()]; 389 | var bb = pos.piece_bitboards[types.Piece.BLACK_BISHOP.index()]; 390 | var wn = pos.piece_bitboards[types.Piece.WHITE_KNIGHT.index()]; 391 | var bn = pos.piece_bitboards[types.Piece.BLACK_KNIGHT.index()]; 392 | 393 | var wbc = types.popcount(wb); 394 | var bbc = types.popcount(bb); 395 | var wnc = types.popcount(wn); 396 | var bnc = types.popcount(bn); 397 | 398 | // KB vs K 399 | if (wbc == 1 and wb | kings == all) { 400 | return true; 401 | } 402 | 403 | if (bbc == 1 and bb | kings == all) { 404 | return true; 405 | } 406 | 407 | // KN vs K 408 | if (wnc == 1 and wn | kings == all) { 409 | return true; 410 | } 411 | 412 | if (bnc == 1 and bn | kings == all) { 413 | return true; 414 | } 415 | 416 | return false; 417 | } 418 | 419 | pub inline fn is_material_drawish(pos: *position.Position) bool { 420 | var all = pos.all_pieces(types.Color.White) | pos.all_pieces(types.Color.Black); 421 | var kings = pos.piece_bitboards[types.Piece.WHITE_KING.index()] | pos.piece_bitboards[types.Piece.BLACK_KING.index()]; 422 | 423 | if (kings == all) { 424 | return true; 425 | } 426 | 427 | var wb = pos.piece_bitboards[types.Piece.WHITE_BISHOP.index()]; 428 | var bb = pos.piece_bitboards[types.Piece.BLACK_BISHOP.index()]; 429 | var wn = pos.piece_bitboards[types.Piece.WHITE_KNIGHT.index()]; 430 | var bn = pos.piece_bitboards[types.Piece.BLACK_KNIGHT.index()]; 431 | 432 | var wbc = types.popcount(wb); 433 | var bbc = types.popcount(bb); 434 | var wnc = types.popcount(wn); 435 | var bnc = types.popcount(bn); 436 | 437 | // KN vs K or KNN vs K 438 | if (wnc <= 2 and wn | kings == all) { 439 | return true; 440 | } 441 | 442 | if (bnc <= 2 and bn | kings == all) { 443 | return true; 444 | } 445 | 446 | // KN vs KN 447 | if (wnc == 1 and bnc == 1 and wn | bn | kings == all) { 448 | return true; 449 | } 450 | 451 | // KB vs KB 452 | if (wbc == 1 and bbc == 1 and wb | bb | kings == all) { 453 | return true; 454 | } 455 | 456 | // KB vs KN 457 | if (wbc == 1 and bnc == 1 and wb | bn | kings == all) { 458 | return true; 459 | } 460 | 461 | if (bbc == 1 and wnc == 1 and bb | wn | kings == all) { 462 | return true; 463 | } 464 | 465 | // KNN vs KB 466 | if (wnc == 2 and bbc == 1 and wn | bb | kings == all) { 467 | return true; 468 | } 469 | 470 | if (bnc == 2 and wbc == 1 and bn | wb | kings == all) { 471 | return true; 472 | } 473 | 474 | // KBN vs KB 475 | if (wbc == 1 and wnc == 1 and bbc == 1 and wb | wn | bb | kings == all) { 476 | return true; 477 | } 478 | 479 | if (bbc == 1 and bnc == 1 and wbc == 1 and bb | bn | wb | kings == all) { 480 | return true; 481 | } 482 | 483 | return false; 484 | } 485 | 486 | pub const MaxMate: i32 = 256; 487 | 488 | pub inline fn is_near_mate(score: i32) bool { 489 | return score >= MateScore - MaxMate or score <= -MateScore + MaxMate; 490 | } 491 | -------------------------------------------------------------------------------- /src/chess/tables.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const types = @import("types.zig"); 3 | 4 | pub const KingAttacks = [64]types.Bitboard{ 5 | 0x302, 0x705, 0xe0a, 0x1c14, 6 | 0x3828, 0x7050, 0xe0a0, 0xc040, 7 | 0x30203, 0x70507, 0xe0a0e, 0x1c141c, 8 | 0x382838, 0x705070, 0xe0a0e0, 0xc040c0, 9 | 0x3020300, 0x7050700, 0xe0a0e00, 0x1c141c00, 10 | 0x38283800, 0x70507000, 0xe0a0e000, 0xc040c000, 11 | 0x302030000, 0x705070000, 0xe0a0e0000, 0x1c141c0000, 12 | 0x3828380000, 0x7050700000, 0xe0a0e00000, 0xc040c00000, 13 | 0x30203000000, 0x70507000000, 0xe0a0e000000, 0x1c141c000000, 14 | 0x382838000000, 0x705070000000, 0xe0a0e0000000, 0xc040c0000000, 15 | 0x3020300000000, 0x7050700000000, 0xe0a0e00000000, 0x1c141c00000000, 16 | 0x38283800000000, 0x70507000000000, 0xe0a0e000000000, 0xc040c000000000, 17 | 0x302030000000000, 0x705070000000000, 0xe0a0e0000000000, 0x1c141c0000000000, 18 | 0x3828380000000000, 0x7050700000000000, 0xe0a0e00000000000, 0xc040c00000000000, 19 | 0x203000000000000, 0x507000000000000, 0xa0e000000000000, 0x141c000000000000, 20 | 0x2838000000000000, 0x5070000000000000, 0xa0e0000000000000, 0x40c0000000000000, 21 | }; 22 | 23 | pub const KnightAttacks = [64]types.Bitboard{ 24 | 0x20400, 0x50800, 0xa1100, 0x142200, 25 | 0x284400, 0x508800, 0xa01000, 0x402000, 26 | 0x2040004, 0x5080008, 0xa110011, 0x14220022, 27 | 0x28440044, 0x50880088, 0xa0100010, 0x40200020, 28 | 0x204000402, 0x508000805, 0xa1100110a, 0x1422002214, 29 | 0x2844004428, 0x5088008850, 0xa0100010a0, 0x4020002040, 30 | 0x20400040200, 0x50800080500, 0xa1100110a00, 0x142200221400, 31 | 0x284400442800, 0x508800885000, 0xa0100010a000, 0x402000204000, 32 | 0x2040004020000, 0x5080008050000, 0xa1100110a0000, 0x14220022140000, 33 | 0x28440044280000, 0x50880088500000, 0xa0100010a00000, 0x40200020400000, 34 | 0x204000402000000, 0x508000805000000, 0xa1100110a000000, 0x1422002214000000, 35 | 0x2844004428000000, 0x5088008850000000, 0xa0100010a0000000, 0x4020002040000000, 36 | 0x400040200000000, 0x800080500000000, 0x1100110a00000000, 0x2200221400000000, 37 | 0x4400442800000000, 0x8800885000000000, 0x100010a000000000, 0x2000204000000000, 38 | 0x4020000000000, 0x8050000000000, 0x110a0000000000, 0x22140000000000, 39 | 0x44280000000000, 0x0088500000000000, 0x0010a00000000000, 0x20400000000000, 40 | }; 41 | 42 | pub const WhitePawnAttacks = [64]types.Bitboard{ 43 | 0x200, 0x500, 0xa00, 0x1400, 44 | 0x2800, 0x5000, 0xa000, 0x4000, 45 | 0x20000, 0x50000, 0xa0000, 0x140000, 46 | 0x280000, 0x500000, 0xa00000, 0x400000, 47 | 0x2000000, 0x5000000, 0xa000000, 0x14000000, 48 | 0x28000000, 0x50000000, 0xa0000000, 0x40000000, 49 | 0x200000000, 0x500000000, 0xa00000000, 0x1400000000, 50 | 0x2800000000, 0x5000000000, 0xa000000000, 0x4000000000, 51 | 0x20000000000, 0x50000000000, 0xa0000000000, 0x140000000000, 52 | 0x280000000000, 0x500000000000, 0xa00000000000, 0x400000000000, 53 | 0x2000000000000, 0x5000000000000, 0xa000000000000, 0x14000000000000, 54 | 0x28000000000000, 0x50000000000000, 0xa0000000000000, 0x40000000000000, 55 | 0x200000000000000, 0x500000000000000, 0xa00000000000000, 0x1400000000000000, 56 | 0x2800000000000000, 0x5000000000000000, 0xa000000000000000, 0x4000000000000000, 57 | 0x0, 0x0, 0x0, 0x0, 58 | 0x0, 0x0, 0x0, 0x0, 59 | }; 60 | 61 | pub const BlackPawnAttacks = [64]types.Bitboard{ 62 | 0x0, 0x0, 0x0, 0x0, 63 | 0x0, 0x0, 0x0, 0x0, 64 | 0x2, 0x5, 0xa, 0x14, 65 | 0x28, 0x50, 0xa0, 0x40, 66 | 0x200, 0x500, 0xa00, 0x1400, 67 | 0x2800, 0x5000, 0xa000, 0x4000, 68 | 0x20000, 0x50000, 0xa0000, 0x140000, 69 | 0x280000, 0x500000, 0xa00000, 0x400000, 70 | 0x2000000, 0x5000000, 0xa000000, 0x14000000, 71 | 0x28000000, 0x50000000, 0xa0000000, 0x40000000, 72 | 0x200000000, 0x500000000, 0xa00000000, 0x1400000000, 73 | 0x2800000000, 0x5000000000, 0xa000000000, 0x4000000000, 74 | 0x20000000000, 0x50000000000, 0xa0000000000, 0x140000000000, 75 | 0x280000000000, 0x500000000000, 0xa00000000000, 0x400000000000, 76 | 0x2000000000000, 0x5000000000000, 0xa000000000000, 0x14000000000000, 77 | 0x28000000000000, 0x50000000000000, 0xa0000000000000, 0x40000000000000, 78 | }; 79 | 80 | pub inline fn reverse_bitboard(b_: types.Bitboard) types.Bitboard { 81 | var b = b_; 82 | b = (b & 0x5555555555555555) << 1 | ((b >> 1) & 0x5555555555555555); 83 | b = (b & 0x3333333333333333) << 2 | ((b >> 2) & 0x3333333333333333); 84 | b = (b & 0x0f0f0f0f0f0f0f0f) << 4 | ((b >> 4) & 0x0f0f0f0f0f0f0f0f); 85 | b = (b & 0x00ff00ff00ff00ff) << 8 | ((b >> 8) & 0x00ff00ff00ff00ff); 86 | 87 | return (b << 48) | ((b & 0xffff0000) << 16) | 88 | ((b >> 16) & 0xffff0000) | (b >> 48); 89 | } 90 | 91 | // Hyperbola Quintessence Algorithm 92 | pub inline fn sliding_attack(square_: types.Square, occ: types.Bitboard, mask: types.Bitboard) types.Bitboard { 93 | var square = square_.index(); 94 | return (((mask & occ) -% types.SquareIndexBB[square] *% 2) ^ 95 | reverse_bitboard(reverse_bitboard(mask & occ) -% reverse_bitboard(types.SquareIndexBB[square]) *% 2)) & mask; 96 | } 97 | 98 | // ROOK MAGIC BITBOARDS 99 | 100 | inline fn get_rook_attacks_for_init(square: types.Square, occ: types.Bitboard) types.Bitboard { 101 | return sliding_attack(square, occ, types.MaskFile[@enumToInt(square.file())]) | sliding_attack(square, occ, types.MaskRank[@enumToInt(square.rank())]); 102 | } 103 | 104 | var RookAttackMasks: [64]types.Bitboard = std.mem.zeroes([64]types.Bitboard); 105 | var RookAttackShifts: [64]i32 = std.mem.zeroes([64]i32); 106 | var RookAttacks: [64][4096]types.Bitboard = std.mem.zeroes([64][4096]types.Bitboard); 107 | 108 | const RookMagics = [64]types.Bitboard{ 109 | 0x0080001020400080, 0x0040001000200040, 0x0080081000200080, 0x0080040800100080, 110 | 0x0080020400080080, 0x0080010200040080, 0x0080008001000200, 0x0080002040800100, 111 | 0x0000800020400080, 0x0000400020005000, 0x0000801000200080, 0x0000800800100080, 112 | 0x0000800400080080, 0x0000800200040080, 0x0000800100020080, 0x0000800040800100, 113 | 0x0000208000400080, 0x0000404000201000, 0x0000808010002000, 0x0000808008001000, 114 | 0x0000808004000800, 0x0000808002000400, 0x0000010100020004, 0x0000020000408104, 115 | 0x0000208080004000, 0x0000200040005000, 0x0000100080200080, 0x0000080080100080, 116 | 0x0000040080080080, 0x0000020080040080, 0x0000010080800200, 0x0000800080004100, 117 | 0x0000204000800080, 0x0000200040401000, 0x0000100080802000, 0x0000080080801000, 118 | 0x0000040080800800, 0x0000020080800400, 0x0000020001010004, 0x0000800040800100, 119 | 0x0000204000808000, 0x0000200040008080, 0x0000100020008080, 0x0000080010008080, 120 | 0x0000040008008080, 0x0000020004008080, 0x0000010002008080, 0x0000004081020004, 121 | 0x0000204000800080, 0x0000200040008080, 0x0000100020008080, 0x0000080010008080, 122 | 0x0000040008008080, 0x0000020004008080, 0x0000800100020080, 0x0000800041000080, 123 | 0x00FFFCDDFCED714A, 0x007FFCDDFCED714A, 0x003FFFCDFFD88096, 0x0000040810002101, 124 | 0x0001000204080011, 0x0001000204000801, 0x0001000082000401, 0x0001FFFAABFAD1A2, 125 | }; 126 | 127 | pub fn init_rook_attacks() void { 128 | var sq: usize = @enumToInt(types.Square.a1); 129 | 130 | while (sq <= @enumToInt(types.Square.h8)) : (sq += 1) { 131 | var edges = ((types.MaskRank[types.File.AFILE.index()] | types.MaskRank[types.File.HFILE.index()]) & ~types.MaskRank[types.rank_plain(sq)]) | 132 | ((types.MaskFile[types.File.AFILE.index()] | types.MaskFile[types.File.HFILE.index()]) & ~types.MaskFile[types.file_plain(sq)]); 133 | 134 | RookAttackMasks[sq] = (types.MaskRank[types.rank_plain(sq)] ^ types.MaskFile[types.file_plain(sq)]) & ~edges; 135 | RookAttackShifts[sq] = 64 - types.popcount(RookAttackMasks[sq]); 136 | 137 | var subset: types.Bitboard = 0; 138 | var index: types.Bitboard = 0; 139 | 140 | index = index *% RookMagics[sq]; 141 | index = index >> @intCast(u6, RookAttackShifts[sq]); 142 | RookAttacks[sq][index] = get_rook_attacks_for_init(@intToEnum(types.Square, sq), subset); 143 | subset = (subset -% RookAttackMasks[sq]) & RookAttackMasks[sq]; 144 | 145 | while (subset != 0) { 146 | index = subset; 147 | index = index *% RookMagics[sq]; 148 | index = index >> @intCast(u6, RookAttackShifts[sq]); 149 | RookAttacks[sq][index] = get_rook_attacks_for_init(@intToEnum(types.Square, sq), subset); 150 | subset = (subset -% RookAttackMasks[sq]) & RookAttackMasks[sq]; 151 | } 152 | } 153 | } 154 | 155 | // Returns the bitboard for rook attacks 156 | pub inline fn get_rook_attacks(square: types.Square, occ: types.Bitboard) types.Bitboard { 157 | return RookAttacks[square.index()][((occ & RookAttackMasks[square.index()]) *% RookMagics[square.index()]) >> @intCast(u6, RookAttackShifts[square.index()])]; 158 | } 159 | 160 | // Returns x-ray attacks, which is the attack when the first-layer blockers are removed. 161 | pub inline fn get_xray_rook_attacks(square: types.Square, occ: types.Bitboard, blockers: types.Bitboard) types.Bitboard { 162 | var attacks = get_rook_attacks(square, occ); 163 | return attacks ^ get_rook_attacks(square, occ ^ (blockers & attacks)); 164 | } 165 | 166 | // BISHOP MAGIC BITBOARDS 167 | 168 | inline fn get_bishop_attacks_for_init(square: types.Square, occ: types.Bitboard) types.Bitboard { 169 | return sliding_attack(square, occ, types.MaskDiagonal[@intCast(usize, square.diagonal())]) | sliding_attack(square, occ, types.MaskAntiDiagonal[@intCast(usize, square.anti_diagonal())]); 170 | } 171 | 172 | var BishopAttackMasks: [64]types.Bitboard = std.mem.zeroes([64]types.Bitboard); 173 | var BishopAttackShifts: [64]i32 = std.mem.zeroes([64]i32); 174 | var BishopAttacks: [64][512]types.Bitboard = std.mem.zeroes([64][512]types.Bitboard); 175 | 176 | const BishopMagics = [64]types.Bitboard{ 177 | 0x0002020202020200, 0x0002020202020000, 0x0004010202000000, 0x0004040080000000, 178 | 0x0001104000000000, 0x0000821040000000, 0x0000410410400000, 0x0000104104104000, 179 | 0x0000040404040400, 0x0000020202020200, 0x0000040102020000, 0x0000040400800000, 180 | 0x0000011040000000, 0x0000008210400000, 0x0000004104104000, 0x0000002082082000, 181 | 0x0004000808080800, 0x0002000404040400, 0x0001000202020200, 0x0000800802004000, 182 | 0x0000800400A00000, 0x0000200100884000, 0x0000400082082000, 0x0000200041041000, 183 | 0x0002080010101000, 0x0001040008080800, 0x0000208004010400, 0x0000404004010200, 184 | 0x0000840000802000, 0x0000404002011000, 0x0000808001041000, 0x0000404000820800, 185 | 0x0001041000202000, 0x0000820800101000, 0x0000104400080800, 0x0000020080080080, 186 | 0x0000404040040100, 0x0000808100020100, 0x0001010100020800, 0x0000808080010400, 187 | 0x0000820820004000, 0x0000410410002000, 0x0000082088001000, 0x0000002011000800, 188 | 0x0000080100400400, 0x0001010101000200, 0x0002020202000400, 0x0001010101000200, 189 | 0x0000410410400000, 0x0000208208200000, 0x0000002084100000, 0x0000000020880000, 190 | 0x0000001002020000, 0x0000040408020000, 0x0004040404040000, 0x0002020202020000, 191 | 0x0000104104104000, 0x0000002082082000, 0x0000000020841000, 0x0000000000208800, 192 | 0x0000000010020200, 0x0000000404080200, 0x0000040404040400, 0x0002020202020200, 193 | }; 194 | 195 | pub fn init_bishop_attacks() void { 196 | var sq: usize = @enumToInt(types.Square.a1); 197 | 198 | while (sq <= @enumToInt(types.Square.h8)) : (sq += 1) { 199 | var edges = ((types.MaskRank[types.File.AFILE.index()] | types.MaskRank[types.File.HFILE.index()]) & ~types.MaskRank[types.rank_plain(sq)]) | 200 | ((types.MaskFile[types.File.AFILE.index()] | types.MaskFile[types.File.HFILE.index()]) & ~types.MaskFile[types.file_plain(sq)]); 201 | 202 | BishopAttackMasks[sq] = (types.MaskDiagonal[types.diagonal_plain(sq)] ^ types.MaskAntiDiagonal[types.anti_diagonal_plain(sq)]) & ~edges; 203 | BishopAttackShifts[sq] = 64 - types.popcount(BishopAttackMasks[sq]); 204 | 205 | var subset: types.Bitboard = 0; 206 | var index: types.Bitboard = 0; 207 | 208 | index = index *% BishopMagics[sq]; 209 | index = index >> @intCast(u6, BishopAttackShifts[sq]); 210 | BishopAttacks[sq][index] = get_bishop_attacks_for_init(@intToEnum(types.Square, sq), subset); 211 | subset = (subset -% BishopAttackMasks[sq]) & BishopAttackMasks[sq]; 212 | 213 | while (subset != 0) { 214 | index = subset; 215 | index = index *% BishopMagics[sq]; 216 | index = index >> @intCast(u6, BishopAttackShifts[sq]); 217 | BishopAttacks[sq][index] = get_bishop_attacks_for_init(@intToEnum(types.Square, sq), subset); 218 | subset = (subset -% BishopAttackMasks[sq]) & BishopAttackMasks[sq]; 219 | } 220 | } 221 | } 222 | 223 | // Returns the bitboard for bishop attacks 224 | pub inline fn get_bishop_attacks(square: types.Square, occ: types.Bitboard) types.Bitboard { 225 | return BishopAttacks[square.index()][((occ & BishopAttackMasks[square.index()]) *% BishopMagics[square.index()]) >> @intCast(u6, BishopAttackShifts[square.index()])]; 226 | } 227 | 228 | // Returns x-ray attacks, which is the attack when the first-layer blockers are removed. 229 | pub inline fn get_xray_bishop_attacks(square: types.Square, occ: types.Bitboard, blockers: types.Bitboard) types.Bitboard { 230 | var attacks = get_bishop_attacks(square, occ); 231 | return attacks ^ get_bishop_attacks(square, occ ^ (blockers & attacks)); 232 | } 233 | 234 | // Squares between squares 235 | 236 | // Bitboard for the squares between two squares, 0 if they are not aligned 237 | pub var SquaresBetween: [64][64]types.Bitboard = std.mem.zeroes([64][64]types.Bitboard); 238 | 239 | pub fn init_squares_between() void { 240 | var sq1: usize = @enumToInt(types.Square.a1); 241 | 242 | while (sq1 <= @enumToInt(types.Square.h8)) : (sq1 += 1) { 243 | var sq2: usize = @enumToInt(types.Square.a1); 244 | 245 | while (sq2 <= @enumToInt(types.Square.h8)) : (sq2 += 1) { 246 | var sqs = types.SquareIndexBB[sq1] | types.SquareIndexBB[sq2]; 247 | if (types.file_plain(sq1) == types.file_plain(sq2) or types.rank_plain(sq1) == types.rank_plain(sq2)) { 248 | SquaresBetween[sq1][sq2] = get_rook_attacks_for_init(@intToEnum(types.Square, sq1), sqs) & get_rook_attacks_for_init(@intToEnum(types.Square, sq2), sqs); 249 | } else if (types.diagonal_plain(sq1) == types.diagonal_plain(sq2) or types.anti_diagonal_plain(sq1) == types.anti_diagonal_plain(sq2)) { 250 | SquaresBetween[sq1][sq2] = get_bishop_attacks_for_init(@intToEnum(types.Square, sq1), sqs) & get_bishop_attacks_for_init(@intToEnum(types.Square, sq2), sqs); 251 | } else { 252 | SquaresBetween[sq1][sq2] = 0; 253 | } 254 | } 255 | } 256 | } 257 | 258 | // Line between squares 259 | 260 | // Bitboard for line of two squares, 0 if they are not aligned 261 | pub var LineOf: [64][64]types.Bitboard = std.mem.zeroes([64][64]types.Bitboard); 262 | 263 | pub fn init_line_between() void { 264 | var sq1: usize = @enumToInt(types.Square.a1); 265 | 266 | while (sq1 <= @enumToInt(types.Square.h8)) : (sq1 += 1) { 267 | var sq2: usize = @enumToInt(types.Square.a1); 268 | 269 | while (sq2 <= @enumToInt(types.Square.h8)) : (sq2 += 1) { 270 | if (types.file_plain(sq1) == types.file_plain(sq2) or types.rank_plain(sq1) == types.rank_plain(sq2)) { 271 | LineOf[sq1][sq2] = get_rook_attacks_for_init(@intToEnum(types.Square, sq1), 0) & get_rook_attacks_for_init(@intToEnum(types.Square, sq2), 0) | types.SquareIndexBB[sq1] | types.SquareIndexBB[sq2]; 272 | } else if (types.diagonal_plain(sq1) == types.diagonal_plain(sq2) or types.anti_diagonal_plain(sq1) == types.anti_diagonal_plain(sq2)) { 273 | LineOf[sq1][sq2] = get_bishop_attacks_for_init(@intToEnum(types.Square, sq1), 0) & get_bishop_attacks_for_init(@intToEnum(types.Square, sq2), 0) | types.SquareIndexBB[sq1] | types.SquareIndexBB[sq2]; 274 | } else { 275 | LineOf[sq1][sq2] = 0; 276 | } 277 | } 278 | } 279 | } 280 | 281 | // Pseudo-legal attacks array 282 | 283 | pub var PseudoLegalAttacks: [types.N_PT][64]types.Bitboard = std.mem.zeroes([types.N_PT][64]types.Bitboard); 284 | pub var PawnAttacks: [types.N_COLORS][64]types.Bitboard = std.mem.zeroes([types.N_COLORS][64]types.Bitboard); 285 | 286 | pub fn init_pseudo_legal() void { 287 | std.mem.copy(types.Bitboard, PawnAttacks[0][0..64], WhitePawnAttacks[0..64]); 288 | std.mem.copy(types.Bitboard, PawnAttacks[1][0..64], BlackPawnAttacks[0..64]); 289 | std.mem.copy(types.Bitboard, PseudoLegalAttacks[@enumToInt(types.PieceType.Knight)][0..64], KnightAttacks[0..64]); 290 | std.mem.copy(types.Bitboard, PseudoLegalAttacks[@enumToInt(types.PieceType.King)][0..64], KingAttacks[0..64]); 291 | var sq: usize = @enumToInt(types.Square.a1); 292 | 293 | while (sq <= @enumToInt(types.Square.h8)) : (sq += 1) { 294 | PseudoLegalAttacks[@enumToInt(types.PieceType.Bishop)][sq] = get_bishop_attacks_for_init(@intToEnum(types.Square, sq), 0); 295 | PseudoLegalAttacks[@enumToInt(types.PieceType.Rook)][sq] = get_rook_attacks_for_init(@intToEnum(types.Square, sq), 0); 296 | PseudoLegalAttacks[@enumToInt(types.PieceType.Queen)][sq] = PseudoLegalAttacks[@enumToInt(types.PieceType.Bishop)][sq] | PseudoLegalAttacks[@enumToInt(types.PieceType.Rook)][sq]; 297 | } 298 | } 299 | 300 | pub fn init_all() void { 301 | init_bishop_attacks(); 302 | init_rook_attacks(); 303 | init_squares_between(); 304 | init_line_between(); 305 | init_pseudo_legal(); 306 | } 307 | 308 | pub inline fn get_attacks(pt: types.PieceType, sq: types.Square, occ: types.Bitboard) types.Bitboard { 309 | return switch (pt) { 310 | types.PieceType.Rook => get_rook_attacks(sq, occ), 311 | types.PieceType.Bishop => get_bishop_attacks(sq, occ), 312 | types.PieceType.Queen => get_rook_attacks(sq, occ) | get_bishop_attacks(sq, occ), 313 | else => PseudoLegalAttacks[@enumToInt(pt)][sq.index()], 314 | }; 315 | } 316 | 317 | // Get Pawn attacks of a given color and square 318 | pub inline fn get_pawn_attacks(comptime color: types.Color, sq: types.Square) types.Bitboard { 319 | return PawnAttacks[@enumToInt(color)][sq.index()]; 320 | } 321 | 322 | // Get Pawn attacks of every pawn on bitboard 323 | pub inline fn get_pawn_attacks_bb(comptime color: types.Color, bb: types.Bitboard) types.Bitboard { 324 | return if (color == types.Color.White) 325 | types.shift_bitboard(bb, types.Direction.NorthWest) | types.shift_bitboard(bb, types.Direction.NorthEast) 326 | else 327 | types.shift_bitboard(bb, types.Direction.SouthWest) | types.shift_bitboard(bb, types.Direction.SouthEast); 328 | } 329 | -------------------------------------------------------------------------------- /src/engine/interface.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const types = @import("../chess/types.zig"); 3 | const tables = @import("../chess/tables.zig"); 4 | const position = @import("../chess/position.zig"); 5 | const perft = @import("../chess/perft.zig"); 6 | const hce = @import("hce.zig"); 7 | const nnue = @import("nnue.zig"); 8 | const tt = @import("tt.zig"); 9 | const search = @import("search.zig"); 10 | const parameters = @import("parameters.zig"); 11 | const build_options = @import("build_options"); 12 | 13 | pub const UciInterface = struct { 14 | position: position.Position, 15 | search_thread: ?std.Thread, 16 | searcher: search.Searcher, 17 | 18 | pub fn new() UciInterface { 19 | var p = position.Position.new(); 20 | p.set_fen(types.DEFAULT_FEN[0..]); 21 | return UciInterface{ 22 | .position = p, 23 | .search_thread = null, 24 | .searcher = search.Searcher.new(), 25 | }; 26 | } 27 | 28 | pub fn main_loop(self: *UciInterface) !void { 29 | var stdin = std.io.getStdIn().reader(); 30 | var stdout = std.io.getStdOut().writer(); 31 | var command_arena = std.heap.ArenaAllocator.init(std.heap.c_allocator); 32 | defer command_arena.deinit(); 33 | 34 | self.searcher.deinit(); 35 | self.searcher = search.Searcher.new(); 36 | 37 | self.position.set_fen(types.DEFAULT_FEN[0..]); 38 | 39 | try stdout.print("Avalanche {s} by Yinuo Huang (SnowballSH)\n", .{build_options.version}); 40 | 41 | out: while (true) { 42 | // The command will probably be less than 16384 characters 43 | var line = try stdin.readUntilDelimiterOrEofAlloc(command_arena.allocator(), '\n', 16384); 44 | 45 | if (line == null) { 46 | break; 47 | } 48 | 49 | const tline = std.mem.trim(u8, line.?, "\r"); 50 | 51 | var tokens = std.mem.split(u8, tline, " "); 52 | var token = tokens.next(); 53 | if (token == null) { 54 | break; 55 | } 56 | 57 | if (std.mem.eql(u8, token.?, "stop")) { 58 | self.searcher.stop = true; 59 | self.searcher.is_searching = false; 60 | continue; 61 | } else if (std.mem.eql(u8, token.?, "isready")) { 62 | _ = try stdout.writeAll("readyok\n"); 63 | continue; 64 | } 65 | 66 | if (self.searcher.is_searching) { 67 | continue; 68 | } 69 | 70 | if (std.mem.eql(u8, token.?, "quit")) { 71 | break :out; 72 | } else if (std.mem.eql(u8, token.?, "uci")) { 73 | _ = try stdout.write("id name Avalanche "); 74 | _ = try stdout.write(build_options.version); 75 | _ = try stdout.writeByte('\n'); 76 | _ = try stdout.write("id author Yinuo Huang\n\n"); 77 | _ = try stdout.write("option name Hash type spin default 16 min 1 max 131072\n"); 78 | _ = try stdout.write("option name Threads type spin default 1 min 1 max 2048\n"); 79 | for (parameters.TunableParams) |tunable| { 80 | _ = try stdout.print("option name {s} type spin default {s} min {s} max {s}\n", .{ tunable.name, tunable.value, tunable.min_value, tunable.max_value }); 81 | } 82 | _ = try stdout.writeAll("uciok\n"); 83 | } else if (std.mem.eql(u8, token.?, "setoption")) { 84 | while (true) { 85 | token = tokens.next(); 86 | if (token == null or !std.mem.eql(u8, token.?, "name")) { 87 | break; 88 | } 89 | 90 | token = tokens.next(); 91 | if (token == null) { 92 | break; 93 | } 94 | if (std.mem.eql(u8, token.?, "Hash")) { 95 | token = tokens.next(); 96 | if (token == null or !std.mem.eql(u8, token.?, "value")) { 97 | break; 98 | } 99 | 100 | token = tokens.next(); 101 | if (token == null) { 102 | break; 103 | } 104 | 105 | const value = std.fmt.parseUnsigned(usize, token.?, 10) catch 16; 106 | tt.GlobalTT.reset(value); 107 | } else if (std.mem.eql(u8, token.?, "Threads")) { 108 | token = tokens.next(); 109 | if (token == null or !std.mem.eql(u8, token.?, "value")) { 110 | break; 111 | } 112 | 113 | token = tokens.next(); 114 | if (token == null) { 115 | break; 116 | } 117 | 118 | const value = std.fmt.parseUnsigned(usize, token.?, 10) catch 1; 119 | search.NUM_THREADS = value - 1; 120 | } else { 121 | for (parameters.TunableParams) |tunable| { 122 | if (std.mem.eql(u8, token.?, tunable.name)) { 123 | token = tokens.next(); 124 | if (token == null or !std.mem.eql(u8, token.?, "value")) { 125 | break; 126 | } 127 | 128 | token = tokens.next(); 129 | if (token == null) { 130 | break; 131 | } 132 | 133 | const value = std.fmt.parseUnsigned(usize, token.?, 10) catch 16; 134 | switch (tunable.id) { 135 | 0 => { 136 | parameters.LMRWeight = @intToFloat(f64, value) / 1000.0; 137 | search.init_lmr(); 138 | }, 139 | 1 => { 140 | parameters.LMRBias = @intToFloat(f64, value) / 1000.0; 141 | search.init_lmr(); 142 | }, 143 | 2 => { 144 | parameters.RFPDepth = @intCast(i32, value); 145 | }, 146 | 3 => { 147 | parameters.RFPMultiplier = @intCast(i32, value); 148 | }, 149 | 4 => { 150 | parameters.RFPImprovingDeduction = @intCast(i32, value); 151 | }, 152 | 5 => { 153 | parameters.NMPImprovingMargin = @intCast(i32, value); 154 | }, 155 | 6 => { 156 | parameters.NMPBase = @intCast(usize, value); 157 | }, 158 | 7 => { 159 | parameters.NMPDepthDivisor = @intCast(usize, value); 160 | }, 161 | 8 => { 162 | parameters.NMPBetaDivisor = @intCast(i32, value); 163 | }, 164 | 9 => { 165 | parameters.RazoringBase = @intCast(i32, value); 166 | }, 167 | 10 => { 168 | parameters.RazoringMargin = @intCast(i32, value); 169 | }, 170 | 11 => { 171 | parameters.AspirationWindow = @intCast(i32, value); 172 | }, 173 | else => unreachable, 174 | } 175 | // std.debug.print("info string {s} set to {d}\n", .{ tunable.name, value }); 176 | break; 177 | } 178 | } 179 | } 180 | 181 | break; 182 | } 183 | } else if (std.mem.eql(u8, token.?, "ucinewgame")) { 184 | self.searcher.deinit(); 185 | self.searcher = search.Searcher.new(); 186 | tt.GlobalTT.clear(); 187 | self.position.set_fen(types.DEFAULT_FEN[0..]); 188 | } else if (std.mem.eql(u8, token.?, "d")) { 189 | self.position.debug_print(); 190 | } else if (std.mem.eql(u8, token.?, "perft")) { 191 | var depth: u32 = 1; 192 | token = tokens.next(); 193 | if (token != null) { 194 | depth = std.fmt.parseUnsigned(u32, token.?, 10) catch 1; 195 | } 196 | 197 | depth = std.math.max(depth, 1); 198 | 199 | _ = perft.perft_test(&self.position, depth); 200 | } else if (std.mem.eql(u8, token.?, "perftdiv")) { 201 | var depth: u32 = 1; 202 | token = tokens.next(); 203 | if (token != null) { 204 | depth = std.fmt.parseUnsigned(u32, token.?, 10) catch 1; 205 | } 206 | 207 | depth = std.math.max(depth, 1); 208 | 209 | if (self.position.turn == types.Color.White) { 210 | perft.perft_div(types.Color.White, &self.position, depth); 211 | } else { 212 | perft.perft_div(types.Color.Black, &self.position, depth); 213 | } 214 | } else if (std.mem.eql(u8, token.?, "go")) { 215 | var movetime: ?u64 = null; 216 | var max_depth: ?u8 = null; 217 | var mytime: ?u64 = null; 218 | var myinc: ?u64 = null; 219 | var movestogo: ?u64 = null; 220 | self.searcher.force_thinking = true; 221 | self.searcher.max_nodes = null; 222 | self.searcher.soft_max_nodes = null; 223 | while (true) { 224 | token = tokens.next(); 225 | if (token == null) { 226 | break; 227 | } 228 | if (std.mem.eql(u8, token.?, "infinite")) { 229 | movetime = 1 << 63; 230 | movetime.? /= std.time.ns_per_ms; 231 | self.searcher.force_thinking = true; 232 | break; 233 | } 234 | if (std.mem.eql(u8, token.?, "depth")) { 235 | token = tokens.next(); 236 | if (token == null) { 237 | break; 238 | } 239 | max_depth = std.fmt.parseUnsigned(u8, token.?, 10) catch null; 240 | movetime = 1 << 60; 241 | self.searcher.ideal_time = movetime.?; 242 | self.searcher.force_thinking = true; 243 | break; 244 | } 245 | if (std.mem.eql(u8, token.?, "movetime")) { 246 | token = tokens.next(); 247 | if (token == null) { 248 | break; 249 | } 250 | 251 | movetime = std.fmt.parseUnsigned(u64, token.?, 10) catch 10 * std.time.ms_per_s; 252 | self.searcher.ideal_time = 1 << 60; 253 | self.searcher.force_thinking = false; 254 | 255 | break; 256 | } 257 | if (std.mem.eql(u8, token.?, "nodes")) { 258 | token = tokens.next(); 259 | if (token == null) { 260 | break; 261 | } 262 | 263 | self.searcher.max_nodes = std.fmt.parseUnsigned(u64, token.?, 10) catch null; 264 | self.searcher.soft_max_nodes = self.searcher.max_nodes; 265 | 266 | break; 267 | } 268 | if (std.mem.eql(u8, token.?, "wtime")) { 269 | self.searcher.force_thinking = false; 270 | token = tokens.next(); 271 | if (token == null) { 272 | break; 273 | } 274 | 275 | if (self.position.turn == types.Color.White) { 276 | if (movetime == null) { 277 | movetime = 0; 278 | } 279 | 280 | var mt = std.fmt.parseInt(i64, token.?, 10) catch 0; 281 | if (mt <= 0) { 282 | mt = 1; 283 | } 284 | var t = @intCast(u64, mt); 285 | 286 | mytime = t; 287 | } 288 | } else if (std.mem.eql(u8, token.?, "btime")) { 289 | self.searcher.force_thinking = false; 290 | token = tokens.next(); 291 | if (token == null) { 292 | break; 293 | } 294 | 295 | if (self.position.turn == types.Color.Black) { 296 | if (movetime == null) { 297 | movetime = 0; 298 | } 299 | 300 | var mt = std.fmt.parseInt(i64, token.?, 10) catch 0; 301 | if (mt <= 0) { 302 | mt = 1; 303 | } 304 | var t = @intCast(u64, mt); 305 | 306 | mytime = t; 307 | } 308 | } else if (std.mem.eql(u8, token.?, "winc")) { 309 | self.searcher.force_thinking = false; 310 | token = tokens.next(); 311 | if (token == null) { 312 | break; 313 | } 314 | 315 | if (self.position.turn == types.Color.White) { 316 | if (movetime == null) { 317 | movetime = 0; 318 | } 319 | myinc = std.fmt.parseUnsigned(u64, token.?, 10) catch 0; 320 | } 321 | } else if (std.mem.eql(u8, token.?, "binc")) { 322 | self.searcher.force_thinking = false; 323 | token = tokens.next(); 324 | if (token == null) { 325 | break; 326 | } 327 | 328 | if (self.position.turn == types.Color.Black) { 329 | if (movetime == null) { 330 | movetime = 0; 331 | } 332 | myinc = std.fmt.parseUnsigned(u64, token.?, 10) catch 0; 333 | } 334 | } else if (std.mem.eql(u8, token.?, "movestogo")) { 335 | self.searcher.force_thinking = false; 336 | token = tokens.next(); 337 | if (token == null) { 338 | break; 339 | } 340 | movestogo = std.fmt.parseUnsigned(u64, token.?, 10) catch 0; 341 | if (movestogo != null and movestogo.? == 0) { 342 | movestogo = null; 343 | } 344 | } 345 | } 346 | 347 | if (movetime != null) { 348 | const overhead = 25; 349 | if (mytime != null) { 350 | var inc: u64 = 0; 351 | if (myinc != null) { 352 | inc = myinc.?; 353 | } 354 | 355 | if (mytime.? <= overhead) { 356 | self.searcher.ideal_time = overhead - 5; 357 | movetime = overhead - 5; 358 | } else { 359 | if (movestogo == null) { 360 | self.searcher.ideal_time = inc + (mytime.? - overhead) / 28; 361 | movetime = 2 * inc + (mytime.? - overhead) / 16; 362 | } else { 363 | self.searcher.ideal_time = inc + (2 * (mytime.? - overhead)) / (2 * movestogo.? + 1); 364 | movetime = 2 * self.searcher.ideal_time; 365 | movetime = @min(movetime.?, mytime.? - @min(mytime.? - overhead, overhead * @min(movestogo.?, 5))); 366 | } 367 | self.searcher.ideal_time = @min(self.searcher.ideal_time, mytime.? - overhead); 368 | movetime = @min(movetime.?, mytime.? - overhead); 369 | } 370 | } 371 | } else { 372 | movetime = 1000000; 373 | } 374 | 375 | self.searcher.stop = false; 376 | 377 | self.search_thread = std.Thread.spawn( 378 | .{ .stack_size = 64 * 1024 * 1024 }, 379 | startSearch, 380 | .{ &self.searcher, &self.position, movetime.?, max_depth }, 381 | ) catch |e| { 382 | std.debug.panic("Could not spawn main thread!\n{}", .{e}); 383 | unreachable; 384 | }; 385 | self.search_thread.?.detach(); 386 | } else if (std.mem.eql(u8, token.?, "position")) { 387 | token = tokens.next(); 388 | if (token != null) { 389 | if (std.mem.eql(u8, token.?, "startpos")) { 390 | self.position.set_fen(types.DEFAULT_FEN[0..]); 391 | self.searcher.hash_history.clearRetainingCapacity(); 392 | self.searcher.hash_history.append(self.position.hash) catch {}; 393 | 394 | token = tokens.next(); 395 | if (token != null) { 396 | if (std.mem.eql(u8, token.?, "moves")) { 397 | while (true) { 398 | token = tokens.next(); 399 | if (token == null) { 400 | break; 401 | } 402 | 403 | var move = types.Move.new_from_string(&self.position, token.?); 404 | 405 | if (self.position.turn == types.Color.White) { 406 | self.position.play_move(types.Color.White, move); 407 | } else { 408 | self.position.play_move(types.Color.Black, move); 409 | } 410 | 411 | self.searcher.hash_history.append(self.position.hash) catch {}; 412 | } 413 | } 414 | } 415 | } else if (std.mem.eql(u8, token.?, "fen")) { 416 | tokens = std.mem.split(u8, tokens.rest(), " moves "); 417 | var fen = tokens.next(); 418 | if (fen != null) { 419 | self.position.set_fen(fen.?); 420 | self.searcher.hash_history.clearRetainingCapacity(); 421 | self.searcher.hash_history.append(self.position.hash) catch {}; 422 | 423 | var afterfen = tokens.next(); 424 | if (afterfen != null) { 425 | tokens = std.mem.split(u8, afterfen.?, " "); 426 | while (true) { 427 | token = tokens.next(); 428 | if (token == null) { 429 | break; 430 | } 431 | 432 | var move = types.Move.new_from_string(&self.position, token.?); 433 | 434 | if (self.position.turn == types.Color.White) { 435 | self.position.play_move(types.Color.White, move); 436 | } else { 437 | self.position.play_move(types.Color.Black, move); 438 | } 439 | 440 | self.searcher.hash_history.append(self.position.hash) catch {}; 441 | } 442 | } 443 | } 444 | } 445 | } 446 | } 447 | 448 | command_arena.allocator().free(line.?); 449 | } 450 | 451 | self.searcher.deinit(); 452 | search.helper_searchers.deinit(); 453 | search.threads.deinit(); 454 | } 455 | }; 456 | 457 | fn startSearch(searcher: *search.Searcher, pos: *position.Position, movetime: usize, max_depth: ?u8) void { 458 | searcher.max_millis = movetime; 459 | var depth = max_depth; 460 | 461 | var movelist = std.ArrayList(types.Move).initCapacity(std.heap.c_allocator, 32) catch unreachable; 462 | if (pos.turn == types.Color.White) { 463 | pos.generate_legal_moves(types.Color.White, &movelist); 464 | } else { 465 | pos.generate_legal_moves(types.Color.Black, &movelist); 466 | } 467 | var move_size = movelist.items.len; 468 | if (move_size == 1) { 469 | depth = 1; 470 | } 471 | movelist.deinit(); 472 | 473 | if (pos.turn == types.Color.White) { 474 | _ = searcher.iterative_deepening(pos, types.Color.White, depth); 475 | } else { 476 | _ = searcher.iterative_deepening(pos, types.Color.Black, depth); 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /nets.txt: -------------------------------------------------------------------------------- 1 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 2 | | Name | Train Info | Result | Comments | 3 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 4 | | 1.5.0 | Ancient Arch | base | Network for v1.5.0, before the new Arch | 5 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 6 | | net003 | 768->512x2->SCReLU->1 | -82.2 vs base | New arch | 7 | | | 100 epochs | | Ciekce says it's "giga-overfitted" | 8 | | | wdl=0.12 | | | 9 | | | lr=0.01 | | | 10 | | | lr_drop at 30 | | | 11 | | | 35M 5k-6k nodes self-play data | | | 12 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 13 | | net004 | Same as net003, but 75 epochs & wdl=0.15 & uses CReLU | -74.4 vs base | There's hope if we have more data | 14 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 15 | | net005 | Same as net004, but 200 epochs & 256 hidden neurons | STC vs base | Ciekce was probably correct | 16 | | | | 74 - 105 - 121 [0.448] 300 | Probably more data will help | 17 | | | | -36.0 +/- 30.5, LOS: 1.0 % | | 18 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 19 | | net006 | 768->384x2->CReLU->1 | STC vs base | First improvement over master. | 20 | | | 60 epochs | 171 - 125 - 304 [0.538] 600 | Although performs relatively worse as tc increases. | 21 | | | wdl=0.15 | 26.7 +/- 19.5, LOS: 99.6 % | | 22 | | | lr=0.01 | | | 23 | | | lr_drop at 30 | | | 24 | | | 150M 5.5k nodes self-play data | | | 25 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 26 | | net007a | 768->384x2->CReLU->1 | STC vs base | Trying some new params | 27 | | | 50 epochs | 77 - 35 - 89 [0.604] 201 | Much stronger than previous arch | 28 | | | wdl=0.25 | 73.7 +/- 36.1, LOS: 100.0 % | | 29 | | | lr=0.002 | | | 30 | | | lr *= 0.10 every 30 epochs | LTC vs base | | 31 | | | data reshuffled from net006 | 38 - 19 - 58 [0.583] 115 | | 32 | | | | 57.9 +/- 44.9, LOS: 99.4 % | | 33 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 34 | | net007b | 768->256x2->CReLU->1 | STC vs base | Trying a smaller network to see if 384 was necessary | 35 | | | 70 epochs | 42 - 25 - 26 [0.591] 93 | Seems like not enough data for 384 yet | 36 | | | wdl=0.35 | 64.2 +/- 61.2, LOS: 98.1 % | | 37 | | | lr=0.004 | | | 38 | | | lr *= 0.10 every 30 epochs | STC vs 007a | | 39 | | | data from net007a | 234 - 201 - 565 [0.516] 1000 | | 40 | | | | 11.5 +/- 14.2, LOS: 94.3 % | | 41 | | | | | | 42 | | | | LTC vs 007a | | 43 | | | | 67 - 52 - 181 [0.525] 300 | | 44 | | | | 17.4 +/- 24.8, LOS: 91.5 % | | 45 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 46 | | net008a | 768->384x2->CReLU->1 | STC vs 007b | Attempting lower lr and wdl. | 47 | | | 40 epochs | 89 - 106 - 231 [0.480] 426 | | 48 | | | wdl=0.15 | -13.9 +/- 22.3, LOS: 11.2 % | | 49 | | | lr=0.001 | | | 50 | | | lr *= 0.35 every 15 epochs | | | 51 | | | data from net007a | | | 52 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 53 | | net008b | 768->256x2->CReLU->1 | STC vs 007b | 008a but smaller | 54 | | | 60 epochs | 284 - 174 - 542 [0.555] 1000 | | 55 | | | wdl=0.15 | 38.4 +/- 14.5, LOS: 100.0 % | | 56 | | | lr=0.002 | | | 57 | | | lr *= 0.35 every 15 epochs | | | 58 | | | data from net007a | | | 59 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 60 | | net009a | 768->384x2->CReLU->1 | STC vs 008b | Probably something went wrong in training | 61 | | | 35 epochs | 21 - 48 - 74 [0.406] 143 | | 62 | | | wdl=0.15 | -66.4 +/- 39.6, LOS: 0.1 % | | 63 | | | lr=0.001 | | | 64 | | | lr *= 0.3 every 15 epochs | | | 65 | | | 177M 6k nodes self-play data from 008b | | | 66 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 67 | | net009b | 768->256x2->CReLU->1 | STC vs 008b | | 68 | | | 50 epochs | 12 - 26 - 46 [0.417] 84 | | 69 | | | wdl=0.15 | -58.5 +/- 50.1, LOS: 1.2 % | | 70 | | | lr=0.001 | | | 71 | | | lr *= 0.4 every 15 epochs | | | 72 | | | data from net009a | | | 73 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 74 | | net010a | net009a retrained, shuffled, wdl=0.05 | STC vs 008b | Bad data most likely | 75 | | | | 78 - 112 - 234 [0.460] 424 | | 76 | | | | -27.9 +/- 22.1, LOS: 0.7 % | | 77 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 78 | | net010b | 010a with 256 neurons | STC vs 008b | | 79 | | | | 7 - 19 - 43 [0.413] 69 | | 80 | | | | -61.0 +/- 50.1, LOS: 0.9 % | | 81 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 82 | | net011 | 768->384x2->CReLU->1 | STC vs 008b | | 83 | | | 39 epochs | 54 - 109 - 113 [0.400] 276 | | 84 | | | wdl=0.20 | -70.2 +/- 31.7, LOS: 0.0 % | | 85 | | | lr=0.001 | | | 86 | | | lr *= 0.3 every 15 epochs | | | 87 | | | new 180M 6k node self-play data | | | 88 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 89 | | net013 | 768->256x2->CReLU->1 | STC vs 008b | Likely overfitting | 90 | | | 48 epochs | 214 - 299 - 409 [0.454] 922 | | 91 | | | wdl=0.25 | -32.1 +/- 16.7, LOS: 0.0 % | | 92 | | | lr=0.001 | | | 93 | | | lr *= 0.1 every 30 epochs | | | 94 | | | new 294M 5k node self-play data from 008b | | | 95 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 96 | | net014 | 768->384x2->CReLU->1 | Epoch 60: | | 97 | | | wdl=0.30 | STC vs 008b | | 98 | | | lr=0.001 | 86 - 99 - 145 [0.480] 330 | | 99 | | | lr *= 0.1 every 30 epochs | -13.7 +/- 28.1, LOS: 17.0 % | | 100 | | | shuffled from net013 | | | 101 | | | | Epoch 65: | | 102 | | | | STC vs 008b | | 103 | | | | 33 - 49 - 58 [0.443] 140 | | 104 | | | | -39.9 +/- 44.3, LOS: 3.9 % | | 105 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 106 | | net015 | 768->384x2->CReLU->1 | STC vs 008b | | 107 | | | 50 epochs | 207 - 225 - 348 [0.488] 780 | | 108 | | | wdl=0.15 | -8.0 +/- 18.1, LOS: 19.3 % | | 109 | | | lr=0.0011 | | | 110 | | | lr *= 0.24 every 20 epochs | | | 111 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 112 | | net016 | 768->384x2->CReLU->1 | STC vs 008b | | 113 | | | 35 epochs | 328 - 338 - 554 [0.496] 1220 | | 114 | | | wdl=0.25 | -2.8 +/- 14.4, LOS: 34.9 % | | 115 | | | lr=0.001 | | | 116 | | | lr *= 0.1 every 15 epochs | | | 117 | | | added another 175M depth=8 data | | | 118 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 119 | | net017 | 768->384x2->CReLU->1 | STC vs 008b | | 120 | | | 40 epochs | 230 - 248 - 342 [0.489] 820 | | 121 | | | wdl=0.30 | -7.6 +/- 18.1, LOS: 20.5 % | | 122 | | | lr=0.001 | | | 123 | | | lr *= 0.3 every 16 epochs | | | 124 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 125 | | net028 (xuebeng) | 768->512x2->SCReLU->1 | LTC vs 008b | Finally! | 126 | | | 50 epochs | 616 - 497 - 1088 [0.527] 2201 | | 127 | | | wdl=0.35 | 18.8 +/- 10.3, LOS: 100.0 %, DrawRatio: 49.4 % | | 128 | | | lr=0.001 | | | 129 | | | lr *= 0.1 every 20 epochs | | | 130 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 131 | | v2_03 | 768->512x2->CReLU->1 | STC vs xuebeng | idk what went wrong | 132 | | | 45 epochs | 130 - 213 - 250 [0.430] 593 | | 133 | | | wdl=0.40 | -49.0 +/- 21.3, LOS: 0.0 %, DrawRatio: 42.2 % | | 134 | | | lr=0.001 | | | 135 | | | lr *= 0.3 every 15 epochs | | | 136 | | | New 500M depth=8 data from net028 | | | 137 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ 138 | | v2_10 | 768->768x2->CReLU->1 | LTC vs xuebeng | | 139 | | | 50 epochs | +1.6 elo | | 140 | | | wdl=0.25 | | | 141 | | | 1.3B combined data | | | 142 | +------------------+-------------------------------------------------------+------------------------------------------------+------------------------------------------------------+ -------------------------------------------------------------------------------- /src/engine/search.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const types = @import("../chess/types.zig"); 4 | const tables = @import("../chess/tables.zig"); 5 | const position = @import("../chess/position.zig"); 6 | const hce = @import("hce.zig"); 7 | const tt = @import("tt.zig"); 8 | const movepick = @import("movepick.zig"); 9 | const see = @import("see.zig"); 10 | 11 | const parameters = @import("parameters.zig"); 12 | 13 | const DATAGEN = false; 14 | 15 | pub var QuietLMR: [64][64]i32 = undefined; 16 | 17 | pub fn init_lmr() void { 18 | var depth: usize = 1; 19 | while (depth < 64) : (depth += 1) { 20 | var moves: usize = 1; 21 | while (moves < 64) : (moves += 1) { 22 | const a = parameters.LMRWeight * std.math.ln(@intToFloat(f32, depth)) * std.math.ln(@intToFloat(f32, moves)) + parameters.LMRBias; 23 | QuietLMR[depth][moves] = @floatToInt(i32, @floor(a)); 24 | } 25 | } 26 | } 27 | 28 | pub const MAX_PLY = 128; 29 | pub const MAX_GAMEPLY = 1024; 30 | 31 | pub const NodeType = enum { 32 | Root, 33 | PV, 34 | NonPV, 35 | }; 36 | 37 | pub const MAX_THREADS = 512; 38 | pub var NUM_THREADS: usize = 0; 39 | 40 | pub const STABILITY_MULTIPLIER = [5]f32{ 2.50, 1.20, 0.90, 0.80, 0.75 }; 41 | 42 | pub var helper_searchers: std.ArrayList(Searcher) = std.ArrayList(Searcher).init(std.heap.c_allocator); 43 | pub var threads: std.ArrayList(?std.Thread) = std.ArrayList(?std.Thread).init(std.heap.c_allocator); 44 | 45 | pub const Searcher = struct { 46 | min_depth: usize = 1, 47 | max_millis: u64 = 0, 48 | ideal_time: u64 = 0, 49 | force_thinking: bool = false, 50 | iterative_deepening_depth: usize = 0, 51 | timer: std.time.Timer = undefined, 52 | 53 | soft_max_nodes: ?u64 = null, 54 | max_nodes: ?u64 = null, 55 | 56 | time_stop: bool = false, 57 | 58 | nodes: u64 = 0, 59 | ply: u32 = 0, 60 | seldepth: u32 = 0, 61 | stop: bool = false, 62 | is_searching: bool = false, 63 | 64 | exclude_move: [MAX_PLY]types.Move = undefined, 65 | nmp_min_ply: u32 = 0, 66 | 67 | hash_history: std.ArrayList(u64) = undefined, 68 | eval_history: [MAX_PLY]i32 = undefined, 69 | move_history: [MAX_PLY]types.Move = undefined, 70 | moved_piece_history: [MAX_PLY]types.Piece = undefined, 71 | 72 | best_move: types.Move = undefined, 73 | pv: [MAX_PLY][MAX_PLY]types.Move = undefined, 74 | pv_size: [MAX_PLY]usize = undefined, 75 | 76 | killer: [MAX_PLY][2]types.Move = undefined, 77 | history: [2][64][64]i32 = undefined, 78 | 79 | counter_moves: [2][64][64]types.Move = undefined, 80 | continuation: *[12][64][64][64]i32, 81 | 82 | root_board: position.Position = undefined, 83 | thread_id: usize = 0, 84 | silent_output: bool = false, 85 | 86 | pub fn new() Searcher { 87 | var s = Searcher{ 88 | .continuation = std.heap.c_allocator.create([12][64][64][64]i32) catch unreachable, 89 | }; 90 | 91 | s.hash_history = std.ArrayList(u64).initCapacity(std.heap.c_allocator, MAX_GAMEPLY) catch unreachable; 92 | s.reset_heuristics(true); 93 | 94 | return s; 95 | } 96 | 97 | pub fn deinit(self: *Searcher) void { 98 | self.hash_history.deinit(); 99 | std.heap.c_allocator.destroy(self.continuation); 100 | } 101 | 102 | pub fn reset_heuristics(self: *Searcher, comptime total_reset: bool) void { 103 | self.nmp_min_ply = 0; 104 | 105 | { 106 | var i: usize = 0; 107 | while (i < MAX_PLY) : (i += 1) { 108 | self.killer[i][0] = types.Move.empty(); 109 | self.killer[i][1] = types.Move.empty(); 110 | 111 | self.exclude_move[i] = types.Move.empty(); 112 | } 113 | } 114 | 115 | { 116 | var j: usize = 0; 117 | while (j < 64) : (j += 1) { 118 | var k: usize = 0; 119 | while (k < 64) : (k += 1) { 120 | var i: usize = 0; 121 | while (i < 2) : (i += 1) { 122 | if (total_reset) { 123 | self.history[i][j][k] = 0; 124 | } else { 125 | self.history[i][j][k] = @divTrunc(self.history[i][j][k], 2); 126 | } 127 | self.counter_moves[i][j][k] = types.Move.empty(); 128 | } 129 | if (j < 12) { 130 | i = 0; 131 | while (i < 64) : (i += 1) { 132 | var o: usize = 0; 133 | while (o < 64) : (o += 1) { 134 | self.continuation[j][k][i][o] = 0; 135 | } 136 | } 137 | } 138 | } 139 | } 140 | } 141 | 142 | { 143 | var j: usize = 0; 144 | while (j < MAX_PLY) : (j += 1) { 145 | var k: usize = 0; 146 | while (k < MAX_PLY) : (k += 1) { 147 | self.pv[j][k] = types.Move.empty(); 148 | } 149 | self.pv_size[j] = 0; 150 | self.eval_history[j] = 0; 151 | self.move_history[j] = types.Move.empty(); 152 | self.moved_piece_history[j] = types.Piece.NO_PIECE; 153 | } 154 | } 155 | } 156 | 157 | pub inline fn should_stop(self: *Searcher) bool { 158 | return self.stop or (self.thread_id == 0 and self.iterative_deepening_depth > self.min_depth and ((self.max_nodes != null and self.nodes >= self.max_nodes.?) or (!self.force_thinking and self.timer.read() / std.time.ns_per_ms >= self.max_millis))); 159 | } 160 | 161 | pub inline fn should_not_continue(self: *Searcher, factor: f32) bool { 162 | return self.stop or (self.thread_id == 0 and self.iterative_deepening_depth > self.min_depth and ((self.soft_max_nodes != null and self.nodes >= self.soft_max_nodes.?) or (!self.force_thinking and self.timer.read() / std.time.ns_per_ms >= @min(self.max_millis, @floatToInt(u64, @floor(@intToFloat(f32, self.ideal_time) * factor)))))); 163 | } 164 | 165 | pub fn iterative_deepening(self: *Searcher, pos: *position.Position, comptime color: types.Color, max_depth: ?u8) i32 { 166 | var out = std.io.bufferedWriter(std.io.getStdOut().writer()); 167 | var outW = out.writer(); 168 | self.stop = false; 169 | self.is_searching = true; 170 | self.time_stop = false; 171 | self.reset_heuristics(false); 172 | self.nodes = 0; 173 | self.best_move = types.Move.empty(); 174 | 175 | self.timer = std.time.Timer.start() catch unreachable; 176 | 177 | var prev_score = -hce.MateScore; 178 | var score = -hce.MateScore; 179 | var bm = types.Move.empty(); 180 | 181 | var stability: usize = 0; 182 | 183 | const extra = NUM_THREADS - helper_searchers.items.len; 184 | helper_searchers.ensureTotalCapacity(NUM_THREADS) catch unreachable; 185 | helper_searchers.appendNTimesAssumeCapacity(undefined, extra); 186 | threads.ensureTotalCapacity(NUM_THREADS) catch unreachable; 187 | threads.appendNTimesAssumeCapacity(null, extra); 188 | var ti: usize = NUM_THREADS - extra; 189 | while (ti < NUM_THREADS) : (ti += 1) { 190 | helper_searchers.items[ti] = Searcher.new(); 191 | } 192 | 193 | ti = 0; 194 | while (ti < NUM_THREADS) : (ti += 1) { 195 | helper_searchers.items[ti].nodes = 0; 196 | } 197 | 198 | var tdepth: usize = 1; 199 | var bound: usize = if (max_depth == null) MAX_PLY - 2 else max_depth.?; 200 | outer: while (tdepth <= bound) { 201 | self.ply = 0; 202 | self.seldepth = 0; 203 | 204 | var alpha = -hce.MateScore; 205 | var beta = hce.MateScore; 206 | var delta = hce.MateScore; 207 | 208 | var depth = tdepth; 209 | 210 | if (depth >= 6) { 211 | alpha = @max(score - parameters.AspirationWindow, -hce.MateScore); 212 | beta = @min(score + parameters.AspirationWindow, hce.MateScore); 213 | delta = parameters.AspirationWindow; 214 | } 215 | 216 | while (true) { 217 | self.iterative_deepening_depth = @max(self.iterative_deepening_depth, depth); 218 | if (depth > 1) { 219 | self.helpers(pos, color, depth, alpha, beta); 220 | } 221 | 222 | self.nmp_min_ply = 0; 223 | 224 | var val = self.negamax(pos, color, depth, alpha, beta, false, NodeType.Root, false); 225 | 226 | if (depth > 1) { 227 | self.stop_helpers(); 228 | } 229 | 230 | if (self.time_stop or self.should_stop()) { 231 | break :outer; 232 | } 233 | 234 | score = val; 235 | 236 | if (score <= alpha) { 237 | beta = @divTrunc(alpha + beta, 2); 238 | alpha = @max(alpha - delta, -hce.MateScore); 239 | } else if (score >= beta) { 240 | beta = @min(beta + delta, hce.MateScore); 241 | if (depth > 1 and (tdepth < 4 or depth > tdepth - 4)) { 242 | depth -= 1; 243 | } 244 | } else { 245 | break; 246 | } 247 | 248 | delta += @divTrunc(delta, 4); 249 | } 250 | 251 | if (self.best_move.to_u16() != bm.to_u16()) { 252 | stability = 0; 253 | } else { 254 | stability += 1; 255 | } 256 | 257 | bm = self.best_move; 258 | 259 | var total_nodes: usize = self.nodes; 260 | 261 | if (depth > 1) { 262 | outW.print("info string thread 0 nodes {}\n", .{ 263 | self.nodes, 264 | }) catch {}; 265 | var thread_index: usize = 0; 266 | while (thread_index < NUM_THREADS) : (thread_index += 1) { 267 | outW.print("info string thread {} nodes {}\n", .{ 268 | thread_index + 1, helper_searchers.items[thread_index].nodes, 269 | }) catch {}; 270 | total_nodes += helper_searchers.items[thread_index].nodes; 271 | } 272 | } 273 | 274 | if (!self.silent_output) { 275 | outW.print("info depth {} seldepth {} nodes {} time {} score ", .{ 276 | tdepth, 277 | self.seldepth, 278 | total_nodes, 279 | self.timer.read() / std.time.ns_per_ms, 280 | }) catch {}; 281 | 282 | if ((std.math.absInt(score) catch 0) >= (hce.MateScore - hce.MaxMate)) { 283 | outW.print("mate {} pv", .{ 284 | (@divTrunc(hce.MateScore - (std.math.absInt(score) catch 0), 2) + 1) * @as(i32, if (score > 0) 1 else -1), 285 | }) catch {}; 286 | if (bound == MAX_PLY - 1) { 287 | bound = depth + 2; 288 | } 289 | } else { 290 | outW.print("cp {} pv", .{ 291 | score, 292 | }) catch {}; 293 | } 294 | 295 | if (self.pv_size[0] > 0) { 296 | var i: usize = 0; 297 | while (i < self.pv_size[0]) : (i += 1) { 298 | outW.writeByte(' ') catch {}; 299 | self.pv[0][i].uci_print(outW); 300 | } 301 | } else { 302 | outW.writeByte(' ') catch {}; 303 | bm.uci_print(outW); 304 | } 305 | 306 | outW.writeByte('\n') catch {}; 307 | out.flush() catch {}; 308 | } 309 | 310 | var factor: f32 = @max(0.5, 1.1 - 0.03 * @intToFloat(f32, stability)); 311 | 312 | if (score - prev_score > parameters.AspirationWindow) { 313 | factor *= 1.1; 314 | } 315 | 316 | prev_score = score; 317 | 318 | if (self.should_not_continue(factor)) { 319 | break; 320 | } 321 | 322 | tdepth += 1; 323 | } 324 | 325 | self.best_move = bm; 326 | 327 | if (!self.silent_output) { 328 | outW.writeAll("bestmove ") catch {}; 329 | bm.uci_print(outW); 330 | outW.writeByte('\n') catch {}; 331 | out.flush() catch {}; 332 | } 333 | 334 | self.is_searching = false; 335 | 336 | tt.GlobalTT.do_age(); 337 | 338 | return score; 339 | } 340 | 341 | pub fn is_draw(self: *Searcher, pos: *position.Position, threefold: bool) bool { 342 | if (pos.history[pos.game_ply].fifty >= 100) { 343 | return true; 344 | } 345 | 346 | if (hce.is_material_draw(pos)) { 347 | return true; 348 | } 349 | 350 | if (self.hash_history.items.len > 1) { 351 | var index: i16 = @intCast(i16, self.hash_history.items.len) - 3; 352 | const limit: i16 = index - @intCast(i16, pos.history[pos.game_ply].fifty) - 1; 353 | var count: u8 = 0; 354 | const threshold: u8 = if (threefold) 2 else 1; 355 | while (index >= limit and index >= 0) { 356 | if (self.hash_history.items[@intCast(usize, index)] == pos.hash) { 357 | count += 1; 358 | if (count >= threshold) { 359 | return true; 360 | } 361 | } 362 | index -= 2; 363 | } 364 | } 365 | 366 | return false; 367 | } 368 | 369 | pub fn helpers(self: *Searcher, pos: *position.Position, comptime color: types.Color, depth_: usize, alpha_: i32, beta_: i32) void { 370 | var i: usize = 0; 371 | while (i < NUM_THREADS) : (i += 1) { 372 | var id: usize = i + 1; 373 | if (threads.items[i] != null) { 374 | threads.items[i].?.join(); 375 | } 376 | var depth: usize = depth_; 377 | if (id % 2 == 1) { 378 | depth += 1; 379 | } 380 | helper_searchers.items[i].max_millis = self.max_millis; 381 | helper_searchers.items[i].thread_id = id; 382 | helper_searchers.items[i].root_board = pos.*; 383 | threads.items[i] = std.Thread.spawn( 384 | .{ .stack_size = 64 * 1024 * 1024 }, 385 | Searcher.start_helper, 386 | .{ &helper_searchers.items[i], color, depth, alpha_, beta_ }, 387 | ) catch |e| { 388 | std.debug.panic("Could not spawn helper thread {}!\n{}", .{ i, e }); 389 | unreachable; 390 | }; 391 | } 392 | } 393 | 394 | pub fn start_helper(self: *Searcher, color: types.Color, depth_: usize, alpha_: i32, beta_: i32) void { 395 | self.stop = false; 396 | self.is_searching = true; 397 | self.time_stop = false; 398 | self.best_move = types.Move.empty(); 399 | self.timer = std.time.Timer.start() catch unreachable; 400 | self.force_thinking = true; 401 | self.ply = 0; 402 | self.seldepth = 0; 403 | if (color == types.Color.White) { 404 | _ = self.negamax(&self.root_board, types.Color.White, depth_, alpha_, beta_, false, NodeType.Root, false); 405 | } else { 406 | _ = self.negamax(&self.root_board, types.Color.Black, depth_, alpha_, beta_, false, NodeType.Root, false); 407 | } 408 | } 409 | 410 | pub fn stop_helpers(self: *Searcher) void { 411 | _ = self; 412 | var i: usize = 0; 413 | while (i < NUM_THREADS) : (i += 1) { 414 | helper_searchers.items[i].stop = true; 415 | } 416 | while (i < NUM_THREADS) : (i += 1) { 417 | threads.items[i].?.join(); 418 | } 419 | } 420 | 421 | pub fn negamax(self: *Searcher, pos: *position.Position, comptime color: types.Color, depth_: usize, alpha_: i32, beta_: i32, comptime is_null: bool, comptime node: NodeType, comptime cutnode: bool) i32 { 422 | var alpha = alpha_; 423 | var beta = beta_; 424 | var depth = depth_; 425 | comptime var opp_color = if (color == types.Color.White) types.Color.Black else types.Color.White; 426 | 427 | self.pv_size[self.ply] = 0; 428 | 429 | // >> Step 1: Preparations 430 | 431 | // Step 1.1: Stop if time is up 432 | if (self.nodes & 2047 == 0 and self.should_stop()) { 433 | self.time_stop = true; 434 | return 0; 435 | } 436 | 437 | self.seldepth = @max(self.seldepth, self.ply); 438 | 439 | var is_root = node == NodeType.Root; 440 | var on_pv: bool = node != NodeType.NonPV; 441 | 442 | // Step 1.3: Ply Overflow Check 443 | if (self.ply == MAX_PLY) { 444 | return hce.evaluate_comptime(pos, color); 445 | } 446 | 447 | var in_check = pos.in_check(color); 448 | 449 | // Step 4.1: Check Extension (moved up) 450 | if (in_check) { 451 | depth += 1; 452 | } 453 | 454 | // Step 1.5: Go to Quiescence Search at Horizon 455 | if (depth == 0) { 456 | return self.quiescence_search(pos, color, alpha, beta); 457 | } 458 | 459 | // Step 1.4: Mate-distance pruning 460 | if (!is_root) { 461 | var r_alpha = @max(-hce.MateScore + @intCast(i32, self.ply), alpha); 462 | var r_beta = @min(hce.MateScore - @intCast(i32, self.ply) - 1, beta); 463 | 464 | if (r_alpha >= r_beta) { 465 | return r_alpha; 466 | } 467 | } 468 | 469 | self.nodes += 1; 470 | 471 | // Step 1.6: Draw check 472 | if (!is_root and self.is_draw(pos, on_pv)) { 473 | return 0; 474 | } 475 | 476 | // >> Step 2: TT Probe 477 | var hashmove = types.Move.empty(); 478 | var tthit = false; 479 | var tt_eval: i32 = 0; 480 | var entry = tt.GlobalTT.get(pos.hash); 481 | 482 | if (entry != null) { 483 | tthit = true; 484 | tt_eval = entry.?.eval; 485 | if (tt_eval > hce.MateScore - hce.MaxMate and tt_eval <= hce.MateScore) { 486 | tt_eval -= @intCast(i32, self.ply); 487 | } else if (tt_eval < -hce.MateScore + hce.MaxMate and tt_eval >= -hce.MateScore) { 488 | tt_eval += @intCast(i32, self.ply); 489 | } 490 | hashmove = entry.?.bestmove; 491 | if (is_root) { 492 | self.best_move = hashmove; 493 | } 494 | 495 | if (!is_null and !on_pv and !is_root and entry.?.depth >= depth) { 496 | if (pos.history[pos.game_ply].fifty < 90 and (depth == 0 or !on_pv)) { 497 | switch (entry.?.flag) { 498 | .Exact => { 499 | return tt_eval; 500 | }, 501 | .Lower => { 502 | alpha = @max(alpha, tt_eval); 503 | }, 504 | .Upper => { 505 | beta = @min(beta, tt_eval); 506 | }, 507 | else => {}, 508 | } 509 | if (alpha >= beta) { 510 | return tt_eval; 511 | } 512 | } 513 | } 514 | } 515 | 516 | var static_eval: i32 = if (in_check) -hce.MateScore + @intCast(i32, self.ply) else if (tthit) entry.?.eval else if (is_null) -self.eval_history[self.ply - 1] else if (self.exclude_move[self.ply].to_u16() != 0) self.eval_history[self.ply] else hce.evaluate_comptime(pos, color); 517 | var best_score: i32 = static_eval; 518 | 519 | var low_estimate: i32 = -hce.MateScore - 1; 520 | 521 | self.eval_history[self.ply] = static_eval; 522 | 523 | var improving = !in_check and self.ply >= 2 and static_eval > self.eval_history[self.ply - 2]; 524 | 525 | var has_non_pawns = pos.has_non_pawns(); 526 | 527 | var last_move = if (self.ply > 0) self.move_history[self.ply - 1] else types.Move.empty(); 528 | var last_last_last_move = if (self.ply > 2) self.move_history[self.ply - 3] else types.Move.empty(); 529 | 530 | // >> Step 3: Extensions/Reductions 531 | // Step 3.1: IIR 532 | // http://talkchess.com/forum3/viewtopic.php?f=7&t=74769&sid=85d340ce4f4af0ed413fba3188189cd1 533 | if (depth >= 3 and !in_check and !tthit and self.exclude_move[self.ply].to_u16() == 0 and (on_pv or cutnode)) { 534 | depth -= 1; 535 | } 536 | 537 | // >> Step 4: Prunings 538 | if (!in_check and !on_pv and self.exclude_move[self.ply].to_u16() == 0) { 539 | low_estimate = if (!tthit or entry.?.flag == tt.Bound.Lower) static_eval else entry.?.eval; 540 | 541 | // Step 4.1: Reverse Futility Pruning 542 | if (std.math.absInt(beta) catch 0 < hce.MateScore - hce.MaxMate and depth <= parameters.RFPDepth) { 543 | var n = @intCast(i32, depth) * parameters.RFPMultiplier; 544 | if (improving) { 545 | n -= parameters.RFPImprovingDeduction; 546 | } 547 | if (static_eval - n >= beta) { 548 | return beta; 549 | } 550 | } 551 | 552 | var nmp_static_eval = static_eval; 553 | if (improving) { 554 | nmp_static_eval += parameters.NMPImprovingMargin; 555 | } 556 | 557 | // Step 4.2: Null move pruning 558 | if (!is_null and depth >= 3 and self.ply >= self.nmp_min_ply and nmp_static_eval >= beta and has_non_pawns) { 559 | var r = parameters.NMPBase + depth / parameters.NMPDepthDivisor; 560 | r += @intCast(usize, @min(4, @divTrunc((static_eval - beta), parameters.NMPBetaDivisor))); 561 | r = @min(r, depth); 562 | 563 | self.ply += 1; 564 | pos.play_null_move(); 565 | var null_score = -self.negamax(pos, opp_color, depth - r, -beta, -beta + 1, true, NodeType.NonPV, !cutnode); 566 | self.ply -= 1; 567 | pos.undo_null_move(); 568 | 569 | if (self.time_stop) { 570 | return 0; 571 | } 572 | 573 | if (null_score >= beta) { 574 | if (null_score >= hce.MateScore - hce.MaxMate) { 575 | null_score = beta; 576 | } 577 | 578 | if (depth < 12 or self.nmp_min_ply > 0) { 579 | return null_score; 580 | } 581 | 582 | self.nmp_min_ply = self.ply + @intCast(u32, (depth - r) * 3 / 4); 583 | 584 | var verif_score = self.negamax(pos, color, depth - r, beta - 1, beta, false, NodeType.NonPV, false); 585 | 586 | self.nmp_min_ply = 0; 587 | 588 | if (self.time_stop) { 589 | return 0; 590 | } 591 | 592 | if (verif_score >= beta) { 593 | return verif_score; 594 | } 595 | } 596 | } 597 | 598 | // Step 4.3: Razoring 599 | if (depth <= 3 and static_eval - parameters.RazoringBase + parameters.RazoringMargin * @intCast(i32, depth) < alpha) { 600 | return self.quiescence_search(pos, color, alpha, beta); 601 | } 602 | } 603 | 604 | // >> Step 5: Search 605 | 606 | // Step 5.1: Move Generation 607 | var movelist = std.ArrayList(types.Move).initCapacity(std.heap.c_allocator, 64) catch unreachable; 608 | defer movelist.deinit(); 609 | pos.generate_legal_moves(color, &movelist); 610 | var move_size = movelist.items.len; 611 | 612 | var quiet_moves = std.ArrayList(types.Move).initCapacity(std.heap.c_allocator, 32) catch unreachable; 613 | defer quiet_moves.deinit(); 614 | 615 | self.killer[self.ply + 1][0] = types.Move.empty(); 616 | self.killer[self.ply + 1][1] = types.Move.empty(); 617 | 618 | if (move_size == 0) { 619 | if (in_check) { 620 | // Checkmate 621 | return -hce.MateScore + @intCast(i32, self.ply); 622 | } else { 623 | // Stalemate 624 | return 0; 625 | } 626 | } 627 | 628 | // Step 5.2: Move Ordering 629 | var evallist = movepick.scoreMoves(self, pos, &movelist, hashmove, is_null); 630 | defer evallist.deinit(); 631 | 632 | // Step 5.3: Move Iteration 633 | var best_move = types.Move.empty(); 634 | best_score = -hce.MateScore + @intCast(i32, self.ply); 635 | 636 | var skip_quiet = false; 637 | 638 | var quiet_count: usize = 0; 639 | var legals: usize = 0; 640 | 641 | var index: usize = 0; 642 | while (index < move_size) : (index += 1) { 643 | var move = movepick.getNextBest(&movelist, &evallist, index); 644 | if (move.to_u16() == self.exclude_move[self.ply].to_u16()) { 645 | continue; 646 | } 647 | 648 | var is_capture = move.is_capture(); 649 | var is_killer = move.to_u16() == self.killer[self.ply][0].to_u16() or move.to_u16() == self.killer[self.ply][1].to_u16(); 650 | 651 | if (!is_capture) { 652 | quiet_moves.append(move) catch unreachable; 653 | quiet_count += 1; 654 | } 655 | 656 | var is_important = is_killer or move.is_promotion(); 657 | 658 | if (skip_quiet and !is_capture and !is_important) { 659 | continue; 660 | } 661 | 662 | if (!DATAGEN and !is_root and index > 1 and !in_check and !on_pv and has_non_pawns) { 663 | if (!is_important and !is_capture and depth <= 5) { 664 | // Step 5.4a: Late Move Pruning 665 | var late = 4 + depth * depth; 666 | if (improving) { 667 | late += 1 + depth / 2; 668 | } 669 | 670 | if (quiet_count > late) { 671 | skip_quiet = true; 672 | } 673 | 674 | // Step 5.4b: Futility Pruning 675 | //if (static_eval + 135 * @intCast(i32, depth) <= alpha and std.math.absInt(alpha) catch 0 < hce.MateScore - hce.MaxMate) { 676 | // skip_quiet = true; 677 | // continue; 678 | //} 679 | } 680 | } 681 | 682 | legals += 1; 683 | 684 | var extension: i32 = 0; 685 | 686 | // Step 5.5: Singular extension 687 | // zig fmt: off 688 | if (self.ply > 0 689 | and !is_root 690 | and self.ply < depth * 2 691 | and depth >= 7 692 | and tthit 693 | and entry.?.flag != tt.Bound.Upper 694 | and !hce.is_near_mate(entry.?.eval) 695 | and hashmove.to_u16() == move.to_u16() 696 | and entry.?.depth >= depth - 3 697 | ) { 698 | // zig fmt: on 699 | var margin = @intCast(i32, depth); 700 | var singular_beta = @max(tt_eval - margin, -hce.MateScore + hce.MaxMate); 701 | 702 | self.exclude_move[self.ply] = hashmove; 703 | var singular_score = self.negamax(pos, color, (depth - 1) / 2, singular_beta - 1, singular_beta, true, NodeType.NonPV, cutnode); 704 | self.exclude_move[self.ply] = types.Move.empty(); 705 | if (singular_score < singular_beta) { 706 | extension = 1; 707 | } else if (singular_beta >= beta) { 708 | return singular_beta; 709 | } else if (tt_eval >= beta) { 710 | extension = -2; 711 | } else if (cutnode) { 712 | extension = -1; 713 | } 714 | } else if (on_pv and !is_root and self.ply < depth * 2) { 715 | // Recapture Extension 716 | if (is_capture and ((last_move.is_capture() and move.to == last_move.to) or (last_last_last_move.is_capture() and move.to == last_last_last_move.to))) { 717 | extension = 1; 718 | } 719 | } 720 | 721 | var new_depth = @intCast(usize, @intCast(i32, depth) + extension - 1); 722 | 723 | self.move_history[self.ply] = move; 724 | self.moved_piece_history[self.ply] = pos.mailbox[move.from]; 725 | self.ply += 1; 726 | pos.play_move(color, move); 727 | self.hash_history.append(pos.hash) catch {}; 728 | 729 | tt.GlobalTT.prefetch(pos.hash); 730 | 731 | var score: i32 = 0; 732 | var min_lmr_move: usize = if (on_pv) 5 else 3; 733 | const is_winning_capture = is_capture and evallist.items[index] >= movepick.SortWinningCapture - 200; 734 | var do_full_search = false; 735 | if (on_pv and legals == 1) { 736 | score = -self.negamax(pos, opp_color, new_depth, -beta, -alpha, false, NodeType.PV, false); 737 | } else { 738 | if (!in_check and depth >= 3 and index >= min_lmr_move and (!is_capture or !is_winning_capture)) { 739 | // Step 5.6: Late-Move Reduction 740 | var reduction: i32 = QuietLMR[@min(depth, 63)][@min(index, 63)]; 741 | 742 | if (self.thread_id % 2 == 1) { 743 | reduction -= 1; 744 | } 745 | 746 | if (improving) { 747 | reduction -= 1; 748 | } 749 | 750 | if (!on_pv) { 751 | reduction += 1; 752 | } 753 | 754 | reduction -= @divTrunc(self.history[@enumToInt(color)][move.from][move.to], 6144); 755 | 756 | var rd: usize = @intCast(usize, std.math.clamp(@intCast(i32, new_depth) - reduction, 1, new_depth + 1)); 757 | 758 | // Step 5.7: Principal-Variation-Search (PVS) 759 | score = -self.negamax(pos, opp_color, rd, -alpha - 1, -alpha, false, NodeType.NonPV, true); 760 | 761 | do_full_search = score > alpha and rd < new_depth; 762 | } else { 763 | do_full_search = !on_pv or index > 0; 764 | } 765 | 766 | if (do_full_search) { 767 | score = -self.negamax(pos, opp_color, new_depth, -alpha - 1, -alpha, false, NodeType.NonPV, !cutnode); 768 | } 769 | 770 | if (on_pv and ((score > alpha and score < beta) or index == 0)) { 771 | score = -self.negamax(pos, opp_color, new_depth, -beta, -alpha, false, NodeType.PV, false); 772 | } 773 | } 774 | 775 | self.ply -= 1; 776 | pos.undo_move(color, move); 777 | _ = self.hash_history.pop(); 778 | 779 | if (self.time_stop) { 780 | return 0; 781 | } 782 | 783 | // Step 5.8: Alpha-Beta Pruning 784 | if (score > best_score) { 785 | best_score = score; 786 | best_move = move; 787 | 788 | if (is_root) { 789 | self.best_move = move; 790 | } 791 | 792 | if (!is_null) { 793 | self.pv[self.ply][0] = move; 794 | std.mem.copy(types.Move, self.pv[self.ply][1..(self.pv_size[self.ply + 1] + 1)], self.pv[self.ply + 1][0..(self.pv_size[self.ply + 1])]); 795 | self.pv_size[self.ply] = self.pv_size[self.ply + 1] + 1; 796 | } 797 | 798 | if (score > alpha) { 799 | alpha = score; 800 | 801 | if (alpha >= beta) { 802 | break; 803 | } 804 | } 805 | } 806 | } 807 | 808 | if (alpha >= beta and !best_move.is_capture() and !best_move.is_promotion()) { 809 | var temp = self.killer[self.ply][0]; 810 | if (temp.to_u16() != best_move.to_u16()) { 811 | self.killer[self.ply][0] = best_move; 812 | self.killer[self.ply][1] = temp; 813 | } 814 | 815 | const adj = @min(1536, @intCast(i32, if (static_eval <= alpha) depth + 1 else depth) * 384 - 384); 816 | 817 | if (!is_null and self.ply >= 1) { 818 | var last = self.move_history[self.ply - 1]; 819 | self.counter_moves[@enumToInt(color)][last.from][last.to] = best_move; 820 | } 821 | 822 | const b = best_move.to_u16(); 823 | const max_history: i32 = 16384; 824 | for (quiet_moves.items) |m| { 825 | const is_best = m.to_u16() == b; 826 | const hist = self.history[@enumToInt(color)][m.from][m.to] * adj; 827 | if (is_best) { 828 | self.history[@enumToInt(color)][m.from][m.to] += adj - @divTrunc(hist, max_history); 829 | } else { 830 | self.history[@enumToInt(color)][m.from][m.to] += -adj - @divTrunc(hist, max_history); 831 | } 832 | 833 | // Continuation History 834 | if (!is_null and self.ply >= 1) { 835 | const plies: [3]usize = .{ 0, 1, 3 }; 836 | for (plies) |plies_ago| { 837 | if (self.ply >= plies_ago + 1) { 838 | const prev = self.move_history[self.ply - plies_ago - 1]; 839 | if (prev.to_u16() == 0) continue; 840 | 841 | const cont_hist = self.continuation[self.moved_piece_history[self.ply - plies_ago - 1].pure_index()][prev.to][m.from][m.to] * adj; 842 | if (is_best) { 843 | self.continuation[self.moved_piece_history[self.ply - plies_ago - 1].pure_index()][prev.to][m.from][m.to] += adj - @divTrunc(cont_hist, max_history); 844 | } else { 845 | self.continuation[self.moved_piece_history[self.ply - plies_ago - 1].pure_index()][prev.to][m.from][m.to] += -adj - @divTrunc(cont_hist, max_history); 846 | } 847 | } 848 | } 849 | } 850 | } 851 | } 852 | 853 | // >> Step 7: Transposition Table Update 854 | if (!skip_quiet and self.exclude_move[self.ply].to_u16() == 0) { 855 | var tt_flag = if (best_score >= beta) tt.Bound.Lower else if (alpha != alpha_) tt.Bound.Exact else tt.Bound.Upper; 856 | 857 | tt.GlobalTT.set(tt.Item{ 858 | .eval = best_score, 859 | .bestmove = best_move, 860 | .flag = tt_flag, 861 | .depth = @intCast(u8, depth), 862 | .hash = pos.hash, 863 | .age = tt.GlobalTT.age, 864 | }); 865 | } 866 | 867 | return best_score; 868 | } 869 | 870 | pub fn quiescence_search(self: *Searcher, pos: *position.Position, comptime color: types.Color, alpha_: i32, beta_: i32) i32 { 871 | var alpha = alpha_; 872 | var beta = beta_; 873 | comptime var opp_color = if (color == types.Color.White) types.Color.Black else types.Color.White; 874 | 875 | // >> Step 1: Preparation 876 | 877 | // Step 1.1: Stop if time is up 878 | if (self.nodes & 2047 == 0 and self.should_stop()) { 879 | self.time_stop = true; 880 | return 0; 881 | } 882 | 883 | self.pv_size[self.ply] = 0; 884 | 885 | // Step 1.2: Material Draw Check 886 | if (hce.is_material_draw(pos)) { 887 | return 0; 888 | } 889 | 890 | // Step 1.4: Ply Overflow Check 891 | if (self.ply == MAX_PLY) { 892 | return hce.evaluate_comptime(pos, color); 893 | } 894 | 895 | self.nodes += 1; 896 | 897 | var in_check = pos.in_check(color); 898 | 899 | // >> Step 2: Prunings 900 | 901 | var best_score = -hce.MateScore + @intCast(i32, self.ply); 902 | var static_eval = best_score; 903 | if (!in_check) { 904 | static_eval = hce.evaluate_comptime(pos, color); 905 | best_score = static_eval; 906 | 907 | // Step 2.1: Stand Pat pruning 908 | if (best_score >= beta) { 909 | return beta; 910 | } 911 | if (best_score > alpha) { 912 | alpha = best_score; 913 | } 914 | } 915 | 916 | // >> Step 3: TT Probe 917 | var hashmove = types.Move.empty(); 918 | var entry = tt.GlobalTT.get(pos.hash); 919 | 920 | if (entry != null) { 921 | hashmove = entry.?.bestmove; 922 | if (entry.?.flag == tt.Bound.Exact) { 923 | return entry.?.eval; 924 | } else if (entry.?.flag == tt.Bound.Lower and entry.?.eval >= beta) { 925 | return entry.?.eval; 926 | } else if (entry.?.flag == tt.Bound.Upper and entry.?.eval <= alpha) { 927 | return entry.?.eval; 928 | } 929 | } 930 | 931 | // >> Step 4: QSearch 932 | 933 | // Step 4.1: Q Move Generation 934 | var movelist = std.ArrayList(types.Move).initCapacity(std.heap.c_allocator, 32) catch unreachable; 935 | defer movelist.deinit(); 936 | if (in_check) { 937 | pos.generate_legal_moves(color, &movelist); 938 | if (movelist.items.len == 0) { 939 | // Checkmated 940 | return -hce.MateScore + @intCast(i32, self.ply); 941 | } 942 | } else { 943 | pos.generate_q_moves(color, &movelist); 944 | } 945 | var move_size = movelist.items.len; 946 | 947 | // Step 4.2: Q Move Ordering 948 | var evallist = movepick.scoreMoves(self, pos, &movelist, hashmove, false); 949 | defer evallist.deinit(); 950 | 951 | // Step 4.3: Q Move Iteration 952 | var index: usize = 0; 953 | 954 | while (index < move_size) : (index += 1) { 955 | var move = movepick.getNextBest(&movelist, &evallist, index); 956 | var is_capture = move.is_capture(); 957 | 958 | // Step 4.4: SEE Pruning 959 | if (is_capture and index > 0) { 960 | var see_score = evallist.items[index]; 961 | 962 | if (see_score < movepick.SortWinningCapture - 2048) { 963 | continue; 964 | } 965 | } 966 | 967 | self.move_history[self.ply] = move; 968 | self.moved_piece_history[self.ply] = pos.mailbox[move.from]; 969 | self.ply += 1; 970 | pos.play_move(color, move); 971 | tt.GlobalTT.prefetch(pos.hash); 972 | var score = -self.quiescence_search(pos, opp_color, -beta, -alpha); 973 | self.ply -= 1; 974 | pos.undo_move(color, move); 975 | 976 | if (self.time_stop) { 977 | return 0; 978 | } 979 | 980 | // Step 4.5: Alpha-Beta Pruning 981 | if (score > best_score) { 982 | best_score = score; 983 | if (score > alpha) { 984 | if (score >= beta) { 985 | return beta; 986 | } 987 | 988 | alpha = score; 989 | } 990 | } 991 | } 992 | 993 | return best_score; 994 | } 995 | }; 996 | --------------------------------------------------------------------------------