Skip to content

Commit

Permalink
Add killer move heuristic in search
Browse files Browse the repository at this point in the history
  • Loading branch information
ArcticXWolf committed Mar 24, 2021
1 parent 324d6ba commit 8a05541
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 9 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ lichess provides enough resources for an average search depth of 3-4 plys.
* Quiescence search
* Iterative deepening
* Move ordering
* Killer move heuristic
* Transposition tables as cache
* Opening books
* Endgame tables
Expand Down
15 changes: 13 additions & 2 deletions engines/axwchessbot/evaluation/evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def __init__(self):
self.pair_bonus = 0
self.rook_bonus = 0
self.tempo_bonus = 0
self.mobility_bonus = 0
self.blocked_pieces_score = 0
self.king_shield_bonus = 0
self.passed_pawn_bonus = 0
Expand Down Expand Up @@ -60,6 +61,7 @@ def evaluate(self) -> Evaluation:
self.evaluate_tempo(color)
self.evaluate_blocked_pieces(color)
self.evaluate_king_shield(color)
self.evaluate_mobility(color)
self.evaluate_passed_pawns()

self.combine_results()
Expand Down Expand Up @@ -99,6 +101,8 @@ def combine_results(self) -> None:
self.total_score -= self.eval_result[chess.BLACK].rook_bonus
self.total_score += self.eval_result[chess.WHITE].tempo_bonus
self.total_score -= self.eval_result[chess.BLACK].tempo_bonus
self.total_score += self.eval_result[chess.WHITE].mobility_bonus
self.total_score -= self.eval_result[chess.BLACK].mobility_bonus
self.total_score += self.eval_result[chess.WHITE].blocked_pieces_score
self.total_score -= self.eval_result[chess.BLACK].blocked_pieces_score
# kingshield bonus is included in material_score_midgame
Expand All @@ -110,10 +114,10 @@ def combine_results(self) -> None:
self.total_score_perspective = -self.total_score

def evaluate_gameover(self) -> float:
if not self.board.is_game_over():
if not self.board.is_game_over(claim_draw=True):
return None

result = self.board.result()
result = self.board.result(claim_draw=True)
if result == "0-1":
return float("-inf")
elif result == "1-0":
Expand Down Expand Up @@ -190,6 +194,13 @@ def evaluate_tempo(self, color: chess.Color) -> None:
"tempo"
]

def evaluate_mobility(self, color: chess.Color) -> None:
board = self.board.copy()
if board.turn != color:
board.push(chess.Move.null())

self.eval_result[color].mobility_bonus += len(list(board.legal_moves))

def evaluate_pair_bonus(self, color: chess.Color) -> None:
if len(self.board.pieces(chess.BISHOP, color)) > 1:
self.eval_result[color].pair_bonus += score_tables.additional_modifiers[
Expand Down
45 changes: 40 additions & 5 deletions engines/axwchessbot/search/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def __init__(
self.quiesce_depth = quiesce_depth
self.cache = TranspositionTable(1e7)
self.timeout = timeout
self.killer_moves = {}

if cache:
self.cache = cache
Expand Down Expand Up @@ -99,6 +100,7 @@ def alpha_beta_search(
beta: float = float("inf"),
move=None,
previous_moves=None,
ply=0,
) -> Tuple:
best_score = float("-inf")
best_move = None
Expand Down Expand Up @@ -143,22 +145,44 @@ def alpha_beta_search(

move_list_to_choose_from = evaluation.Evaluation(self.board).move_order()

if cached:
move_list_to_choose_from.insert(0, cached.move)
if ply in self.killer_moves:
if self.killer_moves[ply][1] is not None:
try:
move_list_to_choose_from.remove(self.killer_moves[ply][1])
move_list_to_choose_from.insert(0, self.killer_moves[ply][1])
except ValueError:
pass
if self.killer_moves[ply][0] is not None:
try:
move_list_to_choose_from.remove(self.killer_moves[ply][0])
move_list_to_choose_from.insert(0, self.killer_moves[ply][0])
except ValueError:
pass

if (
previous_moves
and len(previous_moves) > depth_left
and previous_moves[depth_left - 1] in move_list_to_choose_from
):
move_list_to_choose_from.insert(0, previous_moves[depth_left - 1])
try:
move_list_to_choose_from.remove(previous_moves[depth_left - 1])
move_list_to_choose_from.insert(0, previous_moves[depth_left - 1])
except ValueError:
pass

if cached:
try:
move_list_to_choose_from.remove(cached.move)
move_list_to_choose_from.insert(0, cached.move)
except ValueError:
pass

for m in move_list_to_choose_from:
san = self.board.san(m)
self.board.push(m)

new_moves, score, new_debug_info = self.alpha_beta_search(
depth_left - 1, -beta, -alpha, m, previous_moves
depth_left - 1, -beta, -alpha, m, previous_moves, ply + 1
)
score = -score
debug_info["moves_analysis"].append(
Expand All @@ -180,6 +204,7 @@ def alpha_beta_search(
if score > alpha:
alpha = score
if alpha >= beta:
self.update_killer_moves(ply, m)
break

if best_score <= alpha_orig:
Expand All @@ -205,7 +230,7 @@ def quiesce_search(self, alpha: float, beta: float, depth_left: int = 0):
if alpha < stand_pat:
alpha = stand_pat

if depth_left > 0 or not self.board.is_game_over():
if depth_left > 0 or not self.board.is_game_over(claim_draw=True):
for move in self.get_captures_by_value():
if self.board.is_capture(move):
self.board.push(move)
Expand All @@ -218,6 +243,16 @@ def quiesce_search(self, alpha: float, beta: float, depth_left: int = 0):
alpha = score
return alpha

def update_killer_moves(self, ply: int, new_move: chess.Move) -> None:
if not self.board.is_capture(new_move):
if ply not in self.killer_moves:
self.killer_moves[ply] = [new_move, None]
return

if new_move != self.killer_moves[ply][0]:
self.killer_moves[ply][1] = self.killer_moves[ply][0]
self.killer_moves[ply][0] = new_move

def next_move_by_opening_db(self):
try:
move = (
Expand Down
2 changes: 1 addition & 1 deletion engines/axwchessbot/tests/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def test_if_search_runs_without_error():

def test_if_engine_can_play_itself():
board = chess.Board()
while not board.is_game_over():
while not board.is_game_over(claim_draw=True):
search_obj = search.Search(board, 1, 0, 0)
result = search_obj.next_move_by_engine()
board.push(result[0])
13 changes: 12 additions & 1 deletion engines/axwchessbot/utils/uci.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ def __init__(self, line: str):
and self.inc[chess.BLACK] is not None
)

@staticmethod
def default_args():
return GoCommandArgs("go wtime 600000 btime 600000 winc 10000 binc 10000")


class Uci:
def __init__(self, abdepth=40, qdepth=6, timeout=180):
Expand Down Expand Up @@ -95,6 +99,8 @@ def command(self, msg):
go_args = GoCommandArgs(msg)
if go_args.has_args and go_args.has_timing_info:
self.set_depth_by_timing(go_args)
else:
self.set_depth_by_timing(GoCommandArgs.default_args())
start_search = timer()
search_obj = search.Search(
self.board, self.abdepth, self.qdepth, self.timeout, self.cache
Expand All @@ -113,7 +119,12 @@ def command(self, msg):
f"info score cp {int(score)} depth {info.get('depth_reached', 0)} nodes {info.get('positions_analyzed', 0)}"
)
self.output(f"bestmove {move.uci()}")
return
return move

if msg[0:1] == ".":
move = self.command("go")
self.board.push(move)
self.output(f"{str(self.board.unicode())}")

def output(self, msg):
print(msg)
Expand Down

0 comments on commit 8a05541

Please sign in to comment.