From 579a0d1b1cb2c29bab086c9c8610c0878245c902 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 17 Jan 2020 15:31:37 +0000 Subject: [PATCH] Transposition table hash memory now adjustable (and not infinite), with UCI option support Add cutnodes and internal iterative deepening to search Additional evaluation terms Stop init MoveStack and PieceStack with undefs as was leading to out of bounds errors in cmtable --- .gitignore | 1 + Project.toml | 2 +- src/Astellarn.jl | 2 +- src/AstellarnEngine.jl | 5 +- src/bitboard.jl | 13 +++ src/board.jl | 40 +++++++-- src/evaluate.jl | 190 ++++++++++++++++++++++++----------------- src/move.jl | 3 +- src/movegen.jl | 10 ++- src/moveorder.jl | 4 +- src/parameters.jl | 16 ++-- src/pieces.jl | 2 +- src/search.jl | 72 ++++++++++------ src/transposition.jl | 60 +++++++++++-- src/uci.jl | 19 +++-- 15 files changed, 297 insertions(+), 142 deletions(-) diff --git a/.gitignore b/.gitignore index b82d597..499350c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ src/transposition2.jl deps/tbprobe.so deps/config.jl deps/build.log +builddir/ diff --git a/Project.toml b/Project.toml index 201e23e..80ada1e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Astellarn" uuid = "f591c62c-242e-4069-a3ea-9f76b695a940" authors = ["Jeremy Revell"] -version = "0.2.1" +version = "0.2.2" [deps] Crayons = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" diff --git a/src/Astellarn.jl b/src/Astellarn.jl index 0d33087..92ff7de 100755 --- a/src/Astellarn.jl +++ b/src/Astellarn.jl @@ -1,5 +1,5 @@ module Astellarn - const ASTELLARN_VERSION = "v0.2.1" + const ASTELLARN_VERSION = "v0.2.2" using Crayons using StaticArrays diff --git a/src/AstellarnEngine.jl b/src/AstellarnEngine.jl index 861953a..6e10459 100755 --- a/src/AstellarnEngine.jl +++ b/src/AstellarnEngine.jl @@ -6,4 +6,7 @@ using Astellarn # start the uci loop -uci_main() +Base.@ccallable function julia_main()::Cint + uci_main() + return 0 +end diff --git a/src/bitboard.jl b/src/bitboard.jl index bd0b1d2..80605df 100644 --- a/src/bitboard.jl +++ b/src/bitboard.jl @@ -456,6 +456,14 @@ A bitboard representing the central files of a board. const CENTERFILES = FILE_C | FILE_D | FILE_E | FILE_F +""" + CENTRAL_SQUARES + +A bitboard representing the central 4 squares of a board. +""" +const CENTRAL_SQUARES = (RANK_4 | RANK_5) & (FILE_D | FILE_E) + + """ KINGFLANK @@ -465,6 +473,11 @@ const KINGFLANK = @SVector [QUEENSIDE ⊻ FILE_D, QUEENSIDE, QUEENSIDE, CENTERFILES, CENTERFILES, KINGSIDE, KINGSIDE, KINGSIDE ⊻ FILE_E] +distance(sqr1::Int, sqr2::Int) = max(abs(fileof(sqr1) - fileof(sqr2)), abs(rankof(sqr1) - rankof(sqr2))) + +const DISTANCE_BETWEEN = [distance(sqr1, sqr2) for sqr1 in 1:64, sqr2 in 1:64] + + # Custom show for bitboard types function Base.show(io::IO, bb::Bitboard) println(io, "Bitboard:") diff --git a/src/board.jl b/src/board.jl index 148386b..8b09b58 100644 --- a/src/board.jl +++ b/src/board.jl @@ -180,7 +180,7 @@ friendly(board::Board) = @inbounds board[board.turn] Return the positions, as a `Bitboard`, of all the occupied squares on the board. """ -occupied(board::Board) = @inbounds board[WHITE] | board[BLACK] +occupied(board::Board) = white(board) | black(board) """ @@ -225,12 +225,27 @@ function Base.show(io::IO, board::Board) end +""" + white(board::Board) + +Get the location of all the white pieces on the board, as a `Bitboard`. +""" +white(board::Board) = @inbounds board.colors[1] + + +""" + black(board::Board) + +Get the location of all the black pieces on the board, as a `Bitboard`. +""" +black(board::Board) = @inbounds board.colors[2] + """ pawns(board::Board) Get the location of all the pawns on the `board`, as a `Bitboard`. """ -pawns(board::Board) = @inbounds board[PAWN] +pawns(board::Board) = @inbounds board.pieces[1] """ @@ -238,7 +253,7 @@ pawns(board::Board) = @inbounds board[PAWN] Get the location of all the knights on the `board`, as a `Bitboard`. """ -knights(board::Board) = @inbounds board[KNIGHT] +knights(board::Board) = @inbounds board.pieces[2] """ @@ -246,7 +261,7 @@ knights(board::Board) = @inbounds board[KNIGHT] Get the location of all the bishops on the `board`, as a `Bitboard`. """ -bishops(board::Board) = @inbounds board[BISHOP] +bishops(board::Board) = @inbounds board.pieces[3] """ @@ -254,7 +269,7 @@ bishops(board::Board) = @inbounds board[BISHOP] Get the location of all the rooks on the `board`, as a `Bitboard`. """ -rooks(board::Board) = @inbounds board[ROOK] +rooks(board::Board) = @inbounds board.pieces[4] """ @@ -262,7 +277,7 @@ rooks(board::Board) = @inbounds board[ROOK] Get the location of all the queens on the `board`, as a `Bitboard`. """ -queens(board::Board) = @inbounds board[QUEEN] +queens(board::Board) = @inbounds board.pieces[5] """ @@ -270,7 +285,7 @@ queens(board::Board) = @inbounds board[QUEEN] Get the location of all the kings on the `board`, as a `Bitboard`. """ -kings(board::Board) = @inbounds board[KING] +kings(board::Board) = @inbounds board.pieces[6] """ @@ -338,7 +353,14 @@ cancastlequeenside(board::Board, color::Color) = isone((board.castling >> (color cancastlequeenside(board::Board) = cancastlequeenside(board, board.turn) -# is the position legal +""" + has_non_pawn_material(board::Board) + +Check if a position has non-pawn material left. +""" +has_non_pawn_material(board::Board) = !isempty(knights(board)) || !isempty(bishops(board)) || !isempty(rooks(board)) || !isempty(queens(board)) + + """ islegal(board::Board) @@ -413,7 +435,7 @@ end Detects draws by insufficient material. Returns `true` if the position is drawn. """ function isdrawbymaterial(board::Board) - piece_count = count(board[WHITE]) + count(board[BLACK]) + piece_count = count(white(board)) + count(black(board)) if piece_count == 2 return true elseif piece_count == 3 diff --git a/src/evaluate.jl b/src/evaluate.jl index 102782b..4e33387 100644 --- a/src/evaluate.jl +++ b/src/evaluate.jl @@ -36,9 +36,8 @@ end function initEvalInfo(board::Board) # extract pawn positions - wpawns = board[WHITE] & board[PAWN] - bpawns = board[BLACK] & board[PAWN] - pawns = [wpawns, bpawns] + wpawns = white(board) & pawns(board) + bpawns = black(board) & pawns(board) # check for rammed pawns wrammedpawns = pawnAdvance(bpawns, wpawns, BLACK) @@ -104,9 +103,9 @@ function scale_factor(board::Board, eval::Int) return SCALE_OCB_ONE_ROOK end end - if (eval > 0) && (count(board[WHITE]) == 2) && ismany(board[WHITEKNIGHT] | board[WHITEBISHOP]) + if (eval > 0) && (count(white(board)) == 2) && ismany(board[WHITEKNIGHT] | board[WHITEBISHOP]) return SCALE_DRAW - elseif (eval < 0) && (count(board[BLACK]) == 2) && ismany(board[BLACKKNIGHT] | board[BLACKBISHOP]) + elseif (eval < 0) && (count(black(board)) == 2) && ismany(board[BLACKKNIGHT] | board[BLACKBISHOP]) return SCALE_DRAW end return SCALE_NORMAL @@ -122,7 +121,7 @@ end Naive evaluation function to get the code development going. """ function evaluate(board::Board, ptable::PawnTable) - ei, ea = initEvalInfo(board::Board) + ei, ea = initEvalInfo(board) score = 0 score += board.psqteval @@ -131,14 +130,14 @@ function evaluate(board::Board, ptable::PawnTable) score += pt_entry.score end - v = fld(scoreEG(score) + scoreMG(score), 2) - if abs(v) > LAZY_THRESH - if board.turn == WHITE - return v - else - return -v - end - end + # v = fld(scoreEG(score) + scoreMG(score), 2) + # if abs(v) > LAZY_THRESH + # if board.turn == WHITE + # return v + # else + # return -v + # end + # end if pt_entry == false score += evaluate_pawns(board, ei, ea, ptable) @@ -171,12 +170,8 @@ end function evaluate_pawns(board::Board, ei::EvalInfo, ea::EvalAttackInfo, ptable::PawnTable) - if (pt_entry = get(ptable, board.phash, false)) !== false - return pt_entry.score - end - - w_pawns = board[WHITE] & board[PAWN] - b_pawns = board[BLACK] & board[PAWN] + w_pawns = white(board) & pawns(board) + b_pawns = black(board) & pawns(board) position_eval = 0 score = 0 @@ -200,7 +195,7 @@ function evaluate_pawns(board::Board, ei::EvalInfo, ea::EvalAttackInfo, ptable:: file = fileof(pawn) # Passed pawns if isempty(w_pawns & PASSED_PAWN_MASKS[2][pawn]) - score -= PASS_PAWN_THREAT[8 - rankof(pawn)] + score -= PASS_PAWN_THREAT[9 - rankof(pawn)] end # Isolated pawns if isempty(NEIGHBOUR_FILE_MASKS[file] & b_pawns & ~FILE[file]) @@ -222,25 +217,38 @@ end function evaluate_knights(board::Board, ei::EvalInfo, ea::EvalAttackInfo) - w_knights = (board[WHITE] & board[KNIGHT]) - b_knights = (board[BLACK] & board[KNIGHT]) + w_knights = (white(board) & knights(board)) + b_knights = (black(board) & knights(board)) score = 0 position_eval = 0 + w_outposts = (RANK_4 | RANK_5 | RANK_6) & ea.wpawnattacks & ~ea.bpawnattacks @inbounds for knight in w_knights attacks = knightMoves(knight) ea.wknightattacks |= attacks score += KNIGHT_MOBILITY[count(attacks & ei.wmobility) + 1] end - score += count((w_knights << 8) & board[WHITE] & board[PAWN]) * PAWN_SHIELD_BONUS + # Score knight outposts + score += count(w_outposts & w_knights) * KNIGHT_OUTPOST_BONUS + # Score reachable knight outposts + score += count(w_outposts & ea.wknightattacks & ~white(board)) * KNIGHT_POTENTIAL_OUTPOST_BONUS + # Score knights behind pawns + score += count((w_knights << 8) & white(board) & pawns(board)) * PAWN_SHIELD_BONUS + + b_outposts = (RANK_3 | RANK_4 | RANK_5) & ~ea.wpawnattacks & ea.bpawnattacks @inbounds for knight in b_knights attacks = knightMoves(knight) ea.bknightattacks |= attacks score -= KNIGHT_MOBILITY[count(attacks & ei.bmobility) + 1] end - score -= count((b_knights >> 8) & board[BLACK] & board[PAWN]) * PAWN_SHIELD_BONUS + # Score knight outposts + score -= count(b_outposts & b_knights) * KNIGHT_OUTPOST_BONUS + # Score reachable knight outposts + score -= count(b_outposts & ea.bknightattacks & ~black(board)) * KNIGHT_POTENTIAL_OUTPOST_BONUS + # Score knights behind pawns + score -= count((b_knights >> 8) & black(board) & pawns(board)) * PAWN_SHIELD_BONUS # bonus for knights in rammed positions num_rammed = count(ei.wrammedpawns) @@ -249,12 +257,12 @@ function evaluate_knights(board::Board, ei::EvalInfo, ea::EvalAttackInfo) # Evaluate trapped knights. for trap in KNIGHT_TRAP_PATTERNS[1] - if ((board[BLACK] & board[PAWN] & trap.pawnmask) == trap.pawnmask) && isone(w_knights & trap.minormask) + if ((black(board) & pawns(board) & trap.pawnmask) == trap.pawnmask) && isone(w_knights & trap.minormask) position_eval -= KNIGHT_TRAP_PENALTY end end for trap in KNIGHT_TRAP_PATTERNS[2] - if ((board[WHITE] & board[PAWN] & trap.pawnmask) == trap.pawnmask) && isone(b_knights & trap.minormask) + if ((white(board) & pawns(board) & trap.pawnmask) == trap.pawnmask) && isone(b_knights & trap.minormask) position_eval += KNIGHT_TRAP_PENALTY end end @@ -265,8 +273,8 @@ end function evaluate_bishops(board::Board, ei::EvalInfo, ea::EvalAttackInfo) - w_bishops = (board[WHITE] & board[BISHOP]) - b_bishops = (board[BLACK] & board[BISHOP]) + w_bishops = (white(board) & bishops(board)) + b_bishops = (black(board) & bishops(board)) position_eval = 0 score = 0 @@ -274,29 +282,41 @@ function evaluate_bishops(board::Board, ei::EvalInfo, ea::EvalAttackInfo) attacks = EMPTY occ = occupied(board) + w_outposts = (RANK_4 | RANK_5 | RANK_6) & ea.wpawnattacks & ~ea.bpawnattacks @inbounds for bishop in w_bishops attacks = bishopMoves(bishop, occ) ea.wbishopattacks |= attacks score += BISHOP_MOBILITY[count(attacks & ei.wmobility) + 1] + if ismany(attacks & CENTRAL_SQUARES) + score += BISHOP_CENTRAL_CONTROL + end end + # Outpost bonus + score += count(w_outposts & w_bishops) * BISHOP_OUTPOST_BONUS # Add a bonus for being behind a pawn. - score += count((w_bishops << 8) & board[WHITE] & board[PAWN]) * PAWN_SHIELD_BONUS + score += count((w_bishops << 8) & white(board) & pawns(board)) * PAWN_SHIELD_BONUS + + b_outposts = (RANK_3 | RANK_4 | RANK_5) & ~ea.wpawnattacks & ea.bpawnattacks @inbounds for bishop in b_bishops attacks = bishopMoves(bishop, occ) ea.bbishopattacks |= attacks score -= BISHOP_MOBILITY[count(attacks & ei.bmobility) + 1] + if ismany(attacks & CENTRAL_SQUARES) + score -= BISHOP_CENTRAL_CONTROL + end end + score -= count(b_outposts & b_bishops) * BISHOP_OUTPOST_BONUS # Add a bonus for being behind a pawn - score -= count((b_bishops >> 8) & board[BLACK] & board[PAWN]) * PAWN_SHIELD_BONUS + score -= count((b_bishops >> 8) & black(board) & pawns(board)) * PAWN_SHIELD_BONUS for trap in BISHOP_TRAP_PATTERNS[1] - if ((board[BLACK] & board[PAWN] & trap.pawnmask) == trap.pawnmask) && !isempty(w_bishops & trap.minormask) + if ((black(board) & pawns(board) & trap.pawnmask) == trap.pawnmask) && !isempty(w_bishops & trap.minormask) position_eval -= BISHOP_TRAP_PENALTY end end for trap in BISHOP_TRAP_PATTERNS[2] - if ((board[WHITE] & board[PAWN] & trap.pawnmask) == trap.pawnmask) && !isempty(b_bishops & trap.minormask) + if ((white(board) & pawns(board) & trap.pawnmask) == trap.pawnmask) && !isempty(b_bishops & trap.minormask) position_eval += BISHOP_TRAP_PENALTY end end @@ -311,19 +331,19 @@ function evaluate_bishops(board::Board, ei::EvalInfo, ea::EvalAttackInfo) # penalty for bishops on colour of own pawns if !isempty(w_bishops & LIGHT) - score -= BISHOP_COLOR_PENALTY * count(board[WHITE] & board[PAWN] & LIGHT) + score -= BISHOP_COLOR_PENALTY * count(white(board) & pawns(board) & LIGHT) position_eval -= BISHOP_RAMMED_COLOR_PENALTY * count(ei.wrammedpawns & LIGHT) end if !isempty(w_bishops & DARK) - score -= BISHOP_COLOR_PENALTY * count(board[WHITE] & board[PAWN] & DARK) + score -= BISHOP_COLOR_PENALTY * count(white(board) & pawns(board) & DARK) position_eval -= BISHOP_RAMMED_COLOR_PENALTY * count(ei.wrammedpawns & DARK) end if !isempty(b_bishops & LIGHT) - score += BISHOP_COLOR_PENALTY * count(board[BLACK] & board[PAWN] & LIGHT) + score += BISHOP_COLOR_PENALTY * count(black(board) & pawns(board) & LIGHT) position_eval += BISHOP_RAMMED_COLOR_PENALTY * count(ei.brammedpawns & LIGHT) end if !isempty(b_bishops & DARK) - score += BISHOP_COLOR_PENALTY * count(board[BLACK] & board[PAWN] & DARK) + score += BISHOP_COLOR_PENALTY * count(black(board) & pawns(board) & DARK) position_eval += BISHOP_RAMMED_COLOR_PENALTY * count(ei.brammedpawns & DARK) end @@ -333,8 +353,8 @@ end function evaluate_rooks(board::Board, ei::EvalInfo, ea::EvalAttackInfo) - w_rooks = (board[WHITE] & board[ROOK]) - b_rooks = (board[BLACK] & board[ROOK]) + w_rooks = (white(board) & rooks(board)) + b_rooks = (black(board) & rooks(board)) position_eval = 0 score = 0 @@ -344,9 +364,12 @@ function evaluate_rooks(board::Board, ei::EvalInfo, ea::EvalAttackInfo) rfile = file(rook) if isempty(rfile & pawns(board)) score += ROOK_OPEN_FILE_BONUS - elseif isempty(rfile & board[WHITE] & board[PAWN]) + elseif isempty(rfile & white(board) & pawns(board)) score += ROOK_SEMIOPEN_FILE_BONUS end + if !isempty(rfile & queens(board)) + score += ROOK_ON_QUEEN_FILE + end if isone(rfile & board[BLACKKING]) position_eval += ROOK_KING_FILE_BONUS end @@ -359,9 +382,12 @@ function evaluate_rooks(board::Board, ei::EvalInfo, ea::EvalAttackInfo) rfile = file(rook) if isempty(rfile & pawns(board)) score -= ROOK_OPEN_FILE_BONUS - elseif isempty(rfile & board[BLACK] & board[PAWN]) + elseif isempty(rfile & black(board) & pawns(board)) score -= ROOK_SEMIOPEN_FILE_BONUS end + if !isempty(rfile & queens(board)) + score -= ROOK_ON_QUEEN_FILE + end if isone(rfile & board[WHITEKING]) position_eval -= ROOK_KING_FILE_BONUS end @@ -427,41 +453,49 @@ function evaluate_kings(board::Board, ei::EvalInfo, ea::EvalAttackInfo) end # Increase king safety if attacking pieces are non existent - if isempty(board[BLACKQUEEN]) - king_safety += 15 - end - if isempty(board[WHITEQUEEN]) - king_safety -= 15 - end - if isempty((board[BLACK] & board[ROOK])) - king_safety += 9 - end - if isempty((board[WHITE] & board[ROOK])) - king_safety -= 9 - end - if isempty((board[BLACK] & board[BISHOP])) - king_safety += 6 - end - if isempty((board[WHITE] & board[BISHOP])) - king_safety -= 6 - end - if isempty((board[BLACK] & board[KNIGHT])) - king_safety += 5 - end - if isempty((board[WHITE] & board[KNIGHT])) - king_safety -= 5 + if has_non_pawn_material(board) + if isempty(board[BLACKQUEEN]) + king_safety += 15 + end + if isempty(board[WHITEQUEEN]) + king_safety -= 15 + end + if isempty((black(board) & rooks(board))) + king_safety += 9 + end + if isempty((white(board) & rooks(board))) + king_safety -= 9 + end + if isempty((black(board) & bishops(board))) + king_safety += 6 + end + if isempty((white(board) & bishops(board))) + king_safety -= 6 + end + if isempty((black(board) & knights(board))) + king_safety += 5 + end + if isempty((white(board) & knights(board))) + king_safety -= 5 + end end # Increase king safety for each pawn surrounding him - king_safety += count(ea.wkingattacks & board[WHITE] & board[PAWN]) * KING_PAWN_SHIELD_BONUS - king_safety -= count(ea.bkingattacks & board[BLACK] & board[PAWN]) * KING_PAWN_SHIELD_BONUS + # king_safety += count(ea.wkingattacks & white(board) & pawns(board)) * KING_PAWN_SHIELD_BONUS + # king_safety -= count(ea.bkingattacks & black(board) & pawns(board)) * KING_PAWN_SHIELD_BONUS + if isempty(pawns(board) & KINGFLANK[fileof(w_king_sqr)]) + score -= PAWNLESS_FLANK + end + if isempty(pawns(board) & KINGFLANK[fileof(b_king_sqr)]) + score += PAWNLESS_FLANK + end # decrease king safety if on an open file, with enemy rooks or queens on the board. - if !isempty((board[BLACK] & board[ROOK]) | board[BLACKQUEEN]) && isempty(file(w_king_sqr) & pawns(board)) + if !isempty((black(board) & rooks(board)) | board[BLACKQUEEN]) && isempty(file(w_king_sqr) & pawns(board)) king_safety -= 15 end - if !isempty((board[WHITE] & board[ROOK]) | board[WHITEQUEEN]) && isempty(file(b_king_sqr) & pawns(board)) + if !isempty((white(board) & rooks(board)) | board[WHITEQUEEN]) && isempty(file(b_king_sqr) & pawns(board)) king_safety += 15 end @@ -567,12 +601,12 @@ function evaluate_threats(board::Board, ei::EvalInfo, ea::EvalAttackInfo) # strongly protected by the enemy. strongly_protected = ea.bpawnattacks | (b_double_attacks & ~w_double_attacks) # well defended by the enemy - defended = (board[BLACK] & ~pawns(board)) & strongly_protected + defended = (black(board) & ~pawns(board)) & strongly_protected # not well defended by the enemy - weak = board[BLACK] & ~strongly_protected & w_attacks + weak = black(board) & ~strongly_protected & w_attacks # Case where our opponent is hanging pieces - case = ~b_attacks | ((board[BLACK] & ~pawns(board)) & w_double_attacks) + case = ~b_attacks | ((black(board) & ~pawns(board)) & w_double_attacks) # Bonus if opponent is hanging pieces score += HANGING_BONUS * count(weak & case) @@ -607,8 +641,8 @@ function evaluate_threats(board::Board, ei::EvalInfo, ea::EvalAttackInfo) score += THREAT_BY_ROOK[5] * case_queens safe = ~b_attacks | w_attacks - case = pawns(board) & board[WHITE] & safe - case = pawnCapturesWhite(case, board[BLACK] & ~pawns(board)) + case = pawns(board) & white(board) & safe + case = pawnCapturesWhite(case, black(board) & ~pawns(board)) score += THREAT_BY_PAWN * count(case) @@ -617,12 +651,12 @@ function evaluate_threats(board::Board, ei::EvalInfo, ea::EvalAttackInfo) # strongly protected by the enemy. strongly_protected = ea.wpawnattacks | (w_double_attacks & ~b_double_attacks) # well defended by the enemy - defended = (board[WHITE] & ~pawns(board)) & strongly_protected + defended = (white(board) & ~pawns(board)) & strongly_protected # not well defended by the enemy - weak = board[WHITE] & ~strongly_protected & b_attacks + weak = white(board) & ~strongly_protected & b_attacks # Case where our opponent is hanging pieces - case = ~w_attacks | ((board[WHITE] & ~pawns(board)) & b_double_attacks) + case = ~w_attacks | ((white(board) & ~pawns(board)) & b_double_attacks) # Bonus if opponent is hanging pieces score -= HANGING_BONUS * count(weak & case) @@ -657,8 +691,8 @@ function evaluate_threats(board::Board, ei::EvalInfo, ea::EvalAttackInfo) score -= THREAT_BY_ROOK[5] * case_queens safe = ~w_attacks | b_attacks - case = pawns(board) & board[BLACK] & safe - case = pawnCapturesBlack(case, board[WHITE] & ~pawns(board)) + case = pawns(board) & black(board) & safe + case = pawnCapturesBlack(case, white(board) & ~pawns(board)) score -= THREAT_BY_PAWN * count(case) diff --git a/src/move.jl b/src/move.jl index 7b08d7d..0eaa362 100644 --- a/src/move.jl +++ b/src/move.jl @@ -99,7 +99,8 @@ end # Allows a preallocation for MoveStack -MoveStack(size::Int) = MoveStack(Vector{Move}(undef, size), 0) +# MoveStack(size::Int) = MoveStack(Vector{Move}(undef, size), 0) +MoveStack(size::Int) = MoveStack(repeat([MOVE_NONE], size), 0) # define useful array methods for MoveStack diff --git a/src/movegen.jl b/src/movegen.jl index 4d1673a..ae0ea86 100644 --- a/src/movegen.jl +++ b/src/movegen.jl @@ -209,13 +209,15 @@ end # internal functions to build bishop moves function build_bishop_moves!(movestack::MoveStack, board::Board, targets::Bitboard, common::MoveGenCommon) build_free_bishop_moves!(movestack, board, targets, common::MoveGenCommon) - build_pinned_bishop_moves!(movestack, board, targets, common::MoveGenCommon) + if !isempty(pinned(board)) + build_pinned_bishop_moves!(movestack, board, targets, common::MoveGenCommon) + end return end function build_free_bishop_moves!(movestack::MoveStack, board::Board, targets::Bitboard, common::MoveGenCommon) - occ = occupied(board) + occ = common.occ for bishop in (bishoplike(board) & common.friends & common.unpinned) push_normal!(movestack, bishop, bishopMoves(bishop, occ) & targets) end @@ -241,7 +243,9 @@ end # internal functions to build rook moves function build_rook_moves!(movestack::MoveStack, board::Board, targets::Bitboard, common::MoveGenCommon) build_free_rook_moves!(movestack, board, targets, common) - build_pinned_rook_moves!(movestack, board, targets, common) + if !isempty(pinned(board)) + build_pinned_rook_moves!(movestack, board, targets, common) + end return end diff --git a/src/moveorder.jl b/src/moveorder.jl index 97f1ffb..f8bf1f8 100644 --- a/src/moveorder.jl +++ b/src/moveorder.jl @@ -39,7 +39,7 @@ end function idx_bestmove(moveorder::MoveOrder, idx_start::Int, idx_end::Int) best = idx_start for i in (idx_start + 1):idx_end - if moveorder.values[i] > moveorder.values[best] + if moveorder.values[i] >= moveorder.values[best] best = i end end @@ -120,7 +120,7 @@ function init_normal_moveorder!(thread::Thread, tt_move::Move, ply::Int) if (previous_move === MOVE_NONE || previous_move === NULL_MOVE) moveorder.counter = MOVE_NONE else - @inbounds moveorder.counter = thread.cmtable[(!board.turn).val][previous_piece.val][previous_to] + @inbounds moveorder.counter = thread.cmtable[(!board.turn).val][previous_piece.val][previous_to] end @inbounds moveorder.killer1 = thread.killers[ply + 1][1] @inbounds moveorder.killer2 = thread.killers[ply + 1][2] diff --git a/src/parameters.jl b/src/parameters.jl index db0bb25..f91bfc0 100644 --- a/src/parameters.jl +++ b/src/parameters.jl @@ -1,6 +1,6 @@ #============================ Global Parameters ===============================# const MAX_MOVES = 256 -const MAX_PLY = 40 +const MAX_PLY = 128 const MATE = 32000 const MAX_QUIET_TRACK = 92 MOVE_OVERHEAD = 100 # But it's not constant, and we don't declare it as such otherwise the compiler may hard-code it. @@ -11,9 +11,9 @@ ABORT_SIGNAL = Base.Threads.Atomic{Bool}(false) const Q_FUTILITY_MARGIN = 175 const RAZOR_DEPTH = 1 -const RAZOR_MARGIN = 400 +const RAZOR_MARGIN = 500 const BETA_PRUNE_DEPTH = 8 -const BETA_PRUNE_MARGIN = 85 +const BETA_PRUNE_MARGIN = 185 const SEE_PRUNE_DEPTH = 8 const SEE_QUIET_MARGIN = -190 const SEE_NOISY_MARGIN = -25 @@ -121,6 +121,7 @@ const TEMPO_BONUS = 22 const ROOK_OPEN_FILE_BONUS = makescore(50, 25) const ROOK_SEMIOPEN_FILE_BONUS = makescore(20, 5) const ROOK_KING_FILE_BONUS = 10 +const ROOK_ON_QUEEN_FILE = makescore(8, 5) #============================= Pawn Evaluation ================================# @@ -139,15 +140,19 @@ const PASS_PAWN_THREAT = SVector{7}([makescore(0, 0), makescore(10, 30), makesco const KNIGHT_TRAP_PENALTY = 50 const KNIGHT_RAMMED_BONUS = 2 +const KNIGHT_OUTPOST_BONUS = makescore(60, 40) +const KNIGHT_POTENTIAL_OUTPOST_BONUS = makescore(30, 10) #=========================== Bishop Evaluation ================================# -const BISHOP_TRAP_PENALTY = 90 +const BISHOP_TRAP_PENALTY = 80 const BISHOP_COLOR_PENALTY = makescore(4, 8) const BISHOP_RAMMED_COLOR_PENALTY = 5 const BISHOP_PAIR_BONUS = 10 +const BISHOP_OUTPOST_BONUS = makescore(30, 20) +const BISHOP_CENTRAL_CONTROL = makescore(40, 0) #============================= King Evaluation ================================# @@ -156,6 +161,7 @@ const BISHOP_PAIR_BONUS = 10 const CASTLE_OPTION_BONUS = 8 const KING_PAWN_SHIELD_BONUS = 12 const KING_FLANK_ATTACK = makescore(10, 0) +const PAWNLESS_FLANK = makescore(20, 95) #============================ Queen Evaluation ================================# @@ -171,7 +177,7 @@ const THREAT_BY_KING = makescore(25, 90) const THREAT_BY_MINOR = @SVector [makescore(5, 30), makescore(60, 40), makescore(80, 55), makescore(90, 120), makescore(80, 160)] const THREAT_BY_ROOK = @SVector [makescore(5, 45), makescore(40, 70), makescore(40, 60), makescore(0, 40), makescore(50, 40)] const THREAT_BY_PAWN = makescore(170, 95) -const LAZY_THRESH = 1450 +const LAZY_THRESH = 1600 #========================= Mobility Evaluation ================================# diff --git a/src/pieces.jl b/src/pieces.jl index c4a9073..631641f 100644 --- a/src/pieces.jl +++ b/src/pieces.jl @@ -15,7 +15,7 @@ end # Allows a preallocation for PieceStack -PieceStack(size::Int) = PieceStack(Vector{PieceType}(undef, size), 0) +PieceStack(size::Int) = PieceStack(repeat([VOID], size), 0) # define useful array methods for PieceStack diff --git a/src/search.jl b/src/search.jl index ca2495b..abb3b62 100644 --- a/src/search.jl +++ b/src/search.jl @@ -89,14 +89,14 @@ end function aspiration_window_internal(thread::Thread, ttable::TT_Table, depth::Int, eval::Int, α::Int, β::Int, δ::Int)::Int eval = eval while !thread.stop - eval = absearch(thread, ttable, α, β, depth, 0) + eval = absearch(thread, ttable, α, β, depth, 0, false) # reporting thread.ss.depth = depth # window cond met if ((α < eval < β) || (elapsedtime(thread.timeman) > 2.5)) && !thread.stop - uci_report(thread, α, β, eval) + uci_report(thread, ttable, α, β, eval) return eval end @@ -154,7 +154,7 @@ function qsearch(thread::Thread, ttable::TT_Table, α::Int, β::Int, ply::Int):: end # probe the transposition table - tt_entry = get(ttable.table, board.hash, NO_ENTRY) + tt_entry = getTTentry(ttable, board.hash) if tt_entry !== NO_ENTRY tt_eval = Int(tt_entry.eval) tt_move = tt_entry.move @@ -253,7 +253,7 @@ end The main alpha-beta search function, containing all the pruning heuristics. """ -function absearch(thread::Thread, ttable::TT_Table, α::Int, β::Int, depth::Int, ply::Int)::Int +function absearch(thread::Thread, ttable::TT_Table, α::Int, β::Int, depth::Int, ply::Int, cutnode::Bool)::Int board = thread.board @inbounds pv_current = thread.pv[ply + 1] @inbounds pv_future = thread.pv[ply + 2] @@ -332,7 +332,7 @@ function absearch(thread::Thread, ttable::TT_Table, α::Int, β::Int, depth::Int end # Probe the transposition table. - tt_entry = get(ttable.table, board.hash, NO_ENTRY) + tt_entry = getTTentry(ttable, board.hash) if tt_entry !== NO_ENTRY tt_eval = Int(tt_entry.eval) tt_value = Int(ttvalue(tt_entry, ply)) @@ -370,11 +370,11 @@ function absearch(thread::Thread, ttable::TT_Table, α::Int, β::Int, depth::Int end # add to transposition table - tt_entry = TT_Entry(eval, MOVE_NONE, MAX_PLY - 1, tt_bound) - if (tt_entry.bound == BOUND_EXACT) || - ((tt_entry.bound == BOUND_LOWER) && (eval >= β)) || - ((tt_entry.bound == BOUND_UPPER) && (eval <= α)) - setTTentry!(ttable, board.hash, tt_entry) + if (tt_bound == BOUND_EXACT) || + ((tt_bound == BOUND_LOWER) && (eval >= β)) || + ((tt_bound == BOUND_UPPER) && (eval <= α)) + #tt_entry = TT_Entry(eval, MOVE_NONE, MAX_PLY - 1, tt_bound) + setTTentry!(ttable, board.hash, eval, MOVE_NONE, MAX_PLY - 1, tt_bound) return eval end end @@ -396,14 +396,14 @@ function absearch(thread::Thread, ttable::TT_Table, α::Int, β::Int, depth::Int # Razoring. # If the evaluation plus a small margin is still below alpha, we drop into the quiescence search. - if (pvnode === false) && (ischeck(board) === false) && (depth <= RAZOR_DEPTH) && (eval + RAZOR_MARGIN < α) + if (pvnode === false) && (ischeck(board) === false) && (depth <= RAZOR_DEPTH) && (eval + RAZOR_MARGIN <= α) q_eval = qsearch(thread, ttable, α, β, ply) return q_eval end # Beta pruning. # If the evaluation minus a margin is still better than beta, we can prune here. - if (pvnode === false) && (ischeck(board) === false) && (depth <= BETA_PRUNE_DEPTH) && (eval - BETA_PRUNE_MARGIN * depth > β) + if (pvnode === false) && (ischeck(board) === false) && (depth <= BETA_PRUNE_DEPTH) && (eval - BETA_PRUNE_MARGIN * depth >= β) return eval end @@ -414,16 +414,16 @@ function absearch(thread::Thread, ttable::TT_Table, α::Int, β::Int, depth::Int # Check we have non-pawn material left on the board. # Do not do more than 2 b2b null moves. if (depth >= 2) && (eval >= β) && !pvnode && !ischeck(board) && - !isempty(queens(board) | bishops(board) | knights(board) | rooks(board)) && (ply > 0 ? (thread.movestack[ply] !== NULL_MOVE) : true) && (ply > 1 ? (thread.movestack[ply - 1] !== NULL_MOVE) : true) && - ((tt_entry === NO_ENTRY) || (tt_eval >= β)) + ((tt_entry === NO_ENTRY) || (tt_eval >= β)) && + has_non_pawn_material(board) reduction = fld(depth, 5) + 3 + min(fld(eval - β, 150), 3) u = apply_null!(thread) - cand_eval = -absearch(thread, ttable, -β, -β + 1, depth - reduction, ply + 1) + cand_eval = -absearch(thread, ttable, -β, -β + 1, depth - reduction, ply + 1, !cutnode) undo_null!(thread, u) if (cand_eval >= β) return β @@ -444,6 +444,18 @@ function absearch(thread::Thread, ttable::TT_Table, α::Int, β::Int, depth::Int played = 0 num_quiets = 0 + # Internal iterative deepening step. + # Aim is to perform a quick search to fill the transposition table with potential moves. + if (depth > 6) && (tt_move === MOVE_NONE) + absearch(thread, ttable, α, β, depth - 7, ply + 1, cutnode) + tt_entry = getTTentry(ttable, board.hash) + if tt_entry !== NO_ENTRY + #tt_eval = Int(tt_entry.eval) + #tt_value = Int(ttvalue(tt_entry, ply)) + tt_move = tt_entry.move + end + end + # Init the move ordering init_normal_moveorder!(thread, tt_move, ply) @inbounds quiets_tried = thread.quietstack[ply + 1] @@ -452,12 +464,16 @@ function absearch(thread::Thread, ttable::TT_Table, α::Int, β::Int, depth::Int isquiet = !istactical(board, move) + # If we pick a quiet move, extract the history statistics. if isquiet hist, cmhist, fmhist = gethistory(thread, move, ply) num_quiets += 1 end - if isquiet && (best > -MATE + MAX_PLY) + # Quiet move pruning steps. + # We require that a non-mating line exists before pruning. + # Opt not to prune if there is only king + pawn material remaining. + if isquiet && (best > -MATE + MAX_PLY) && has_non_pawn_material(board) # Quiet move futility pruning step, using history. if (depth <= FUTILITY_PRUNE_DEPTH) && (eval + futility_margin <= α) && (hist + cmhist + fmhist < FUTILITY_LIMIT[improving]) @@ -487,8 +503,8 @@ function absearch(thread::Thread, ttable::TT_Table, α::Int, β::Int, depth::Int # Prune moves which fail the static exchange evaluator. # Only ran if our best evaluation is not a mating line. - if (static_exchange_evaluator(board, move, isquiet ? see_quiet_margin : see_noisy_margin) == false) && - (depth <= SEE_PRUNE_DEPTH) && (best > -MATE + MAX_PLY) && (moveorder.stage > STAGE_GOOD_NOISY) + if (moveorder.stage > STAGE_GOOD_NOISY) && (depth <= SEE_PRUNE_DEPTH) && (best > -MATE + MAX_PLY) && + (static_exchange_evaluator(board, move, isquiet ? see_quiet_margin : see_noisy_margin) == false) continue end @@ -509,6 +525,10 @@ function absearch(thread::Thread, ttable::TT_Table, α::Int, β::Int, depth::Int if isone(improving) reduction += 1 end + # Add reductions for cutnodes + if cutnode + reduction += 2 + end # Killer moves, and counter moves, are worth looking at more. if moveorder.stage < STAGE_INIT_QUIET reduction -= 1 @@ -533,13 +553,13 @@ function absearch(thread::Thread, ttable::TT_Table, α::Int, β::Int, depth::Int # perform search, taking into account LMR if reduction !== 1 - cand_eval = -absearch(thread, ttable, -α - 1, -α, newdepth - reduction, ply + 1) + cand_eval = -absearch(thread, ttable, -α - 1, -α, newdepth - reduction, ply + 1, true) end if ((reduction !== 1) && (cand_eval > α)) || (reduction == 1 && !(pvnode && played == 1)) - cand_eval = -absearch(thread, ttable, -α - 1, -α, newdepth - 1, ply + 1) + cand_eval = -absearch(thread, ttable, -α - 1, -α, newdepth - 1, ply + 1, !cutnode) end - if (pvnode && (played == 1 || cand_eval > α)) - cand_eval = -absearch(thread, ttable, -β, -α, newdepth - 1, ply + 1) + if (pvnode && (played == 1 || ((cand_eval > α) && (isroot || cand_eval < β)))) + cand_eval = -absearch(thread, ttable, -β, -α, newdepth - 1, ply + 1, false) end # Revert move. @@ -577,7 +597,7 @@ function absearch(thread::Thread, ttable::TT_Table, α::Int, β::Int, depth::Int # Update the history heuristics. # This is done when a quiet move fails high. - if (best >= β) && !istactical(board, best_move) && (best_move !== MOVE_NONE) + if (best >= β) && (best_move !== MOVE_NONE) && !istactical(board, best_move) updatehistory!(thread, quiets_tried, ply, depth^2) end @@ -588,8 +608,8 @@ function absearch(thread::Thread, ttable::TT_Table, α::Int, β::Int, depth::Int # We don't do this at the root node. if isroot == false tt_bound = best >= β ? BOUND_LOWER : (best > init_α ? BOUND_EXACT : BOUND_UPPER) - tt_entry = TT_Entry(eval, best_move, depth, tt_bound) - setTTentry!(ttable, board.hash, tt_entry) + #tt_entry = TT_Entry(eval, best_move, depth, tt_bound) + setTTentry!(ttable, board.hash, eval, best_move, depth, tt_bound) end return best end @@ -729,7 +749,7 @@ function optimistic_move_estimator(board::Board) end # promo checks - if isempty(board[PAWN] & board[board.turn] & (board.turn == WHITE ? RANK_7 : RANK_2)) == false + if isempty(pawns(board) & board[board.turn] & (board.turn == WHITE ? RANK_7 : RANK_2)) == false @inbounds value += PVALS_MG[5] - PVALS_MG[1] end diff --git a/src/transposition.jl b/src/transposition.jl index db1c370..70341e0 100644 --- a/src/transposition.jl +++ b/src/transposition.jl @@ -8,29 +8,60 @@ mutable struct TT_Entry move::Move depth::UInt8 bound::UInt8 + hash16::UInt16 end -const NO_ENTRY = TT_Entry(0, Move(), 0, 0) +const NO_ENTRY = TT_Entry(0, Move(), 0, 0, 0) mutable struct TT_Table - table::Dict{ZobristHash, TT_Entry} + table::Dict{UInt32, TT_Entry} + hashmask::UInt64 end -TT_Table() = TT_Table(Dict{ZobristHash, TT_Entry}()) +TT_Table() = TT_Table(16) +# Chess engines usually face memory restrictions, as we can't possibly hash and store all positions. +# The below function aims to cap the hash memory size, by restricting the keys in the dict. +function TT_Table(size_MB::Int) + bytes = size_MB << 20 -function getTTentry(tt::TT_Table, hash::ZobristHash) - tt.table[hash] + # work out size of slots in TT + #slotsize = Base.summarysize(TT_Entry()) + slotsize = 14 # At present... + maxslots = bytes / slotsize + + size = 1 + while size <= maxslots + size <<= 1 + end + # We shift right again as we "overshifted" by one in the while loop. + size >>= 1 + # + size -= 1 + + TT_Table(Dict{UInt32, TT_Entry}(), UInt64(size)) end -function hasTTentry(tt::TT_Table, hash::ZobristHash) - haskey(tt.table, hash) +function getTTentry(tt::TT_Table, hash::ZobristHash) + res = get(tt.table, UInt32(hash.hash & tt.hashmask), NO_ENTRY) + if (res !== NO_ENTRY) && (res.hash16 === UInt16(hash.hash >> 48)) + return res + else + return NO_ENTRY + end end -function setTTentry!(tt::TT_Table, hash::ZobristHash, entry::TT_Entry) - tt.table[hash] = entry +# function hasTTentry(tt::TT_Table, hash::ZobristHash) +# haskey(tt.table, UInt32(hash.hash & tt.hashmask)) +# end + + +function setTTentry!(tt::TT_Table, hash::ZobristHash, eval::Integer, move::Move, depth::Integer, bound::UInt8) + hash16 = UInt16(hash.hash >> 48) + entry = TT_Entry(eval, move, depth, bound, hash16) + tt.table[UInt32(hash.hash & tt.hashmask)] = entry end @@ -43,3 +74,14 @@ function ttvalue(tt_entry::TT_Entry, ply::Int) return tt_entry.eval end end + + +function hashfull(tt::TT_Table) + full = 0 + for i in 1:3000 + if haskey(tt.table, UInt32(i)) + full += 1 + end + end + fld(full,3) +end diff --git a/src/uci.jl b/src/uci.jl index 194b737..c41de26 100644 --- a/src/uci.jl +++ b/src/uci.jl @@ -69,7 +69,7 @@ function uci_main() uci_perft(threads, splitlines) elseif splitlines[1] == "setoption" - uci_setoptions(threads, splitlines) + uci_setoptions(threads, splitlines, ttable) end # additional options currently unsupported end @@ -194,11 +194,12 @@ function uci_go(threads::ThreadPool, ttable::TT_Table, splitlines::Vector{SubStr nodes = threads[1].ss.nodes elapsed = elapsedtime(timeman) nps = nodes*1000/elapsed + hf = hashfull(ttable) # Report back to the UCI ucistring = movetostring(move) if !threads[1].stop - @printf("info depth %d seldepth %d nodes %d nps %d tbhits %d score cp %d pv ", threads[1].ss.depth, threads[1].ss.seldepth, nodes, nps, threads[1].ss.tbhits, eval) + @printf("info depth %d seldepth %d nodes %d nps %d tbhits %d score cp %d hashfull %d pv ", threads[1].ss.depth, threads[1].ss.seldepth, nodes, nps, threads[1].ss.tbhits, eval, hf) print(join(movetostring.(threads[1].pv[1]), " ")) print("\n") end @@ -239,7 +240,14 @@ function uci_position!(threads::ThreadPool, splitlines::Vector{SubString{String} end -function uci_setoptions(threads::ThreadPool, splitlines::Vector{SubString{String}}) +function uci_setoptions(threads::ThreadPool, splitlines::Vector{SubString{String}}, ttable::TT_Table) + if splitlines[3] == "Hash" + newtable = TT_Table(parse(Int, splitlines[5])) + ttable.table = newtable.table + ttable.hashmask = newtable.hashmask + println("info string set Hash to ", splitlines[5]) + end + if splitlines[3] == "Threads" num_threads = parse(Int, splitlines[5]) createthreadpool(threads[1].board, num_threads) @@ -260,11 +268,12 @@ end # This function is called when the thread (or search) wishes to output stats mid-execution. -function uci_report(thread::Thread, α::Int, β::Int, value::Int) +function uci_report(thread::Thread, ttable::TT_Table, α::Int, β::Int, value::Int) score = max(α, min(β, value)) elapsed = elapsedtime(thread.timeman) nps = thread.ss.nodes*1000 / elapsed - @printf("info depth %d seldepth %d nodes %d nps %d tbhits %d score cp %d pv ", thread.ss.depth, thread.ss.seldepth, thread.ss.nodes, nps, thread.ss.tbhits, score) + hf = hashfull(ttable) + @printf("info depth %d seldepth %d nodes %d nps %d tbhits %d score cp %d hashfull %d pv ", thread.ss.depth, thread.ss.seldepth, thread.ss.nodes, nps, thread.ss.tbhits, score, hf) print(join(movetostring.(thread.pv[1]), " ")) print("\n") return