def dfs(self, board, stats, alpha=None, deep=0): ''' alpha - to reduce brute force if alpha is None dfs will return always list not 0-length ''' stats['nodes'] += 1 if deep == self.max_deep: return self.board_evaluation(board) move_color = board.move_color result = [] lines = self.lines if deep == 0 else 1 for move in board.get_board_moves(): revert_info = board.make_move(move) if revert_info is None: continue cand = self.dfs(board, stats, alpha=result[-1]['evaluation'] if len(result) == lines else None, deep=deep + 1) board.revert_move(revert_info) if cand is None: # Not found better move continue result.append(cand[0]) result[-1]['moves'].append(move) result.sort(key=lambda x: x['evaluation'], reverse=(move_color == WHITE)) result = result[:lines] # TODO: (kosteev) cut two deep recursion if alpha is not None: if (move_color == BLACK and result[0]['evaluation'] <= alpha or move_color == WHITE and result[0]['evaluation'] >= alpha): return None sign = color_sign(move_color) if not result: if board.is_check(opposite=True): # Checkmate result = [{ 'evaluation': -sign * (Board.MAX_EVALUATION - deep), 'moves': [] }] else: # Draw result = [{'evaluation': 0, 'moves': []}] return result
def dfs(self, board, stats, deep=0): stats['nodes'] += 1 if deep == self.max_deep: return self.board_evaluation(board) move_color = board.move_color result = [] lines = self.lines if deep == 0 else 1 for move in board.get_board_moves(): revert_info = board.make_move(move) if revert_info is None: continue cand = self.dfs(board, stats, deep=deep + 1) board.revert_move(revert_info) result.append(cand[0]) result[-1]['moves'].append(move) result.sort(key=lambda x: x['evaluation'], reverse=(move_color == WHITE)) result = result[:lines] sign = color_sign(move_color) if not result: if board.is_check(opposite=True): # Checkmate result = [{ 'evaluation': -sign * (Board.MAX_EVALUATION - deep), 'moves': [] }] else: # Draw result = [{'evaluation': 0, 'moves': []}] return result
def evaluation_params(self): ''' - material - development - center - activeness - space - king safety ''' material = [0, 0] development = [0, 0] center = [0, 0] activeness = [0, 0] space = [0, 0] king_safety = [0, 0] stage = 0 if len(self.pieces) <= 16: stage = 2 elif len(self.pieces) <= 24: stage = 1 for position, (piece, color) in self.pieces.items(): ind = 0 if color == WHITE else 1 material[ind] += PIECES[piece]['value'] activeness[ind] += PIECE_CELL_ACTIVENESS[piece][position] if piece == 'pawn': if 3 <= position[0] <= 4: if ind == 0: if position[1] == 4: center[ind] += 7 elif position[1] == 3: center[ind] += 15 elif position[1] == 2: center[ind] += 7 else: if position[1] == 3: center[ind] += 7 if position[1] == 4: center[ind] += 15 elif position[1] == 5: center[ind] += 7 # elif piece == 'king': # king_safety[ind] = (7 - position[1]) if ind == 0 else position[1] # king_safety[ind] *= 10 engine_eval = 0 if self.move_up_color: engine_eval += 2 * color_sign(self.move_up_color) * len(self.pieces) if stage <= 1: evaluation = material[0] - material[1] + \ (engine_eval + \ activeness[0] - activeness[1] + \ center[0] - center[1]) / 1000.0 else: evaluation = material[0] - material[1] + \ (engine_eval + \ activeness[0] - activeness[1]) / 1000.0 return { 'material': material, 'development': development, 'center': center, 'activeness': activeness, 'space': space, 'king_safety': king_safety, 'engine_eval': engine_eval, 'evaluation': evaluation }
def get_piece_probable_moves(self, position): ''' Returns probable piece moves 1. By rules ( + on board) - PROBABLE_MOVES 2. Do not pass someone on the way, except finish with opposite color 3. Do not make own king under check ??? 1. on passan 2. 0-0 | 0-0-0 ''' piece, move_color = self.pieces[position] opp_move_color = get_opp_color(move_color) sign = color_sign(move_color) probable_moves = [] if piece != 'pawn': probable_moves = PROBABLE_MOVES[piece][position] else: promote_pieces = [] if position[1] + sign in [0, 7]: # Last rank for piece in PIECES: if piece not in ['king', 'pawn']: promote_pieces.append(piece) else: promote_pieces.append(None) for promote_piece in promote_pieces: # Firstly, on side for x in [-1, 1]: probable_move = { 'new_position': (position[0] + x, position[1] + sign), 'new_piece': promote_piece } if (probable_move['new_position'] in self.pieces and self.pieces[probable_move['new_position']][1] == opp_move_color): probable_moves.append([probable_move]) if probable_move['new_position'] == self.en_passant: probable_move['captured_position'] = (position[0] + x, position[1]) probable_moves.append([probable_move]) # Secondly, move forward # Check if position is empty, to avoid treating as take probable_move = { 'new_position': (position[0], position[1] + sign), 'new_piece': promote_piece } if probable_move['new_position'] not in self.pieces: forward_moves = [probable_move] probable_move = { 'new_position': (position[0], position[1] + 2 * sign), 'new_piece': promote_piece } if (probable_move['new_position'] not in self.pieces and position[1] - sign in [0, 7]): # Move on two steps forward_moves.append(probable_move) probable_moves.append(forward_moves) # Extra logic for castles if piece == 'king': # Make a copy probable_moves = list(probable_moves) # If (king, rook) haven't moved + # if not under check + king don't passing beaten cell + # if no piece is on the way kc = self.castles[get_castle_id(move_color, 'k')] qc = self.castles[get_castle_id(move_color, 'q')] if (kc or qc): r = 0 if move_color == WHITE else 7 if kc: assert(position == (4, r)) assert(self.pieces[(7, r)] == ('rook', move_color)) if qc: assert(position == (4, r)) assert(self.pieces[(0, r)] == ('rook', move_color)) is_under_check = self.beaten_cell(position, opp_move_color) if (kc and not is_under_check and not self.beaten_cell((position[0] + 1, r), opp_move_color)): if not any((x, r) in self.pieces for x in [5, 6]): probable_moves.append([{ 'new_position': (6, r) }]) if (qc and not is_under_check and not self.beaten_cell((position[0] - 1, r), opp_move_color)): if not any((x, r) in self.pieces for x in [1, 2, 3]): probable_moves.append([{ 'new_position': (2, r) }]) return probable_moves
def get_board_simple_moves(self): move_color = self.move_color opp_move_color = get_opp_color(move_color) sign = color_sign(move_color) simple_moves = [] for position, (piece, color) in self.pieces.items(): if color != move_color: continue new_positions = [] if piece == 'knight': for variant in PROBABLE_MOVES['knight'][position]: for move in variant: new_position = move['new_position'] captured_piece, _ = self.pieces.get(new_position, (None, None)) if not captured_piece: new_positions.append(new_position) elif piece == 'pawn': new_position = (position[0], position[1] + sign) if (new_position not in self.pieces and new_position[1] not in [0, 7]): # Do not allow promotions new_positions.append(new_position) if position[1] - sign in [0, 7]: new_position = (position[0], position[1] + 2 * sign) if new_position not in self.pieces: new_positions.append(new_position) else: for line_type in LINE_TYPES: if piece not in LINES_INFO[line_type]['pieces']: continue line_id, _ = CELL_TO_LINE_ID[line_type][position] mask = self.masks[line_type][line_id] new_positions.extend(FREE_MOVES[position][line_type][mask][piece]) if piece == 'king': # Castles kc = self.castles[get_castle_id(move_color, 'k')] qc = self.castles[get_castle_id(move_color, 'q')] if (kc or qc): r = 0 if move_color == WHITE else 7 if kc: assert(position == (4, r)) assert(self.pieces[(7, r)] == ('rook', move_color)) if qc: assert(position == (4, r)) assert(self.pieces[(0, r)] == ('rook', move_color)) is_under_check = self.beaten_cell(position, opp_move_color) if (kc and not is_under_check and not self.beaten_cell((position[0] + 1, r), opp_move_color)): if not any((x, r) in self.pieces for x in [5, 6]): new_positions.append((6, r)) if (qc and not is_under_check and not self.beaten_cell((position[0] - 1, r), opp_move_color)): if not any((x, r) in self.pieces for x in [1, 2, 3]): new_positions.append((2, r)) for new_position in new_positions: move = { 'position': position, 'new_position': new_position, 'piece': piece, 'new_piece': piece, 'captured_position': new_position, 'captured_piece': None } simple_moves.append(move) # simple_moves.sort(key=lambda x: (x['position'], x['new_position'], x['new_piece'])) random.shuffle(simple_moves) return simple_moves
def get_board_captures(self, capture_sort_key=None): ''' Captures + promotions without capture ''' move_color = self.move_color opp_move_color = get_opp_color(move_color) sign = color_sign(move_color) capture_moves = [] for position, (piece, color) in self.pieces.items(): if color != move_color: continue if piece == 'knight': for variant in PROBABLE_MOVES['knight'][position]: for move in variant: new_position = move['new_position'] new_piece = piece captured_position = new_position captured_piece, captured_color = self.pieces.get(captured_position, (None, None)) if captured_color != opp_move_color: break move = { 'position': position, 'new_position': new_position, 'piece': piece, 'new_piece': new_piece, 'captured_position': captured_position, 'captured_piece': captured_piece } capture_moves.append(move) else: promotion_pieces = [piece] if (piece == 'pawn' and position[1] + sign in [0, 7]): # Last rank promotion_pieces = PROMOTION_PIECES # Add promotions without capture new_position = (position[0], position[1] + sign) if new_position not in self.pieces: for promote_piece in promotion_pieces: move = { 'position': position, 'new_position': new_position, 'piece': piece, 'new_piece': promote_piece, 'captured_position': new_position, 'captured_piece': None } capture_moves.append(move) for line_type in LINE_TYPES: if piece not in LINES_INFO[line_type]['pieces']: continue line_id, _ = CELL_TO_LINE_ID[line_type][position] mask = self.masks[line_type][line_id] next_cells = NEXT_CELL[position][line_type][mask] en_passant_cells = () if (piece == 'pawn' and self.en_passant): # Extra logic for pawn x = -1 if line_type == LINE_TYPE_LT else 1 new_position = (position[0] + x, position[1] + sign) if new_position == self.en_passant: en_passant_cells = (new_position, ) for new_position in next_cells + en_passant_cells: if new_position is None: continue if piece == 'king': # Extra logic for king diff = abs(new_position[0] - position[0]) if diff == 0: diff = abs(new_position[1] - position[1]) if diff > 1: continue elif piece == 'pawn': # Extra logic for pawn diff = new_position[1] - position[1] if diff != sign: continue for new_piece in promotion_pieces: captured_position = new_position captured_piece, captured_color = self.pieces.get(captured_position, (None, None)) if not captured_piece: # En passant captured_position = (self.en_passant[0], self.en_passant[1] - sign) captured_piece, captured_color = self.pieces[captured_position] if captured_color == move_color: break move = { 'position': position, 'new_position': new_position, 'piece': piece, 'new_piece': new_piece, 'captured_position': captured_position, 'captured_piece': captured_piece } capture_moves.append(move) # capture_moves.sort(key=lambda x: (x['position'], x['new_position'], x['new_piece'])) random.shuffle(capture_moves) # Sort captured moves if capture_sort_key is None: capture_sort_key = self.sort_take_by_value capture_moves.sort(key=capture_sort_key) return capture_moves
def dfs_parallel(self, board, alpha, beta, analyze_launch_time, moves_to_consider=None): ''' !!!! It always returns result of non-zero length `moves_to_consider` - moves to consider, if None than all valid moves are considered ''' move_color = board.move_color result = [] is_any_move = False pool_args = [] moves = [] board_moves = board.get_board_moves( ) if moves_to_consider is None else moves_to_consider if move_color == WHITE: parent_alpha_beta = self.manager().Value('i', alpha) else: parent_alpha_beta = self.manager().Value('i', beta) for move in board_moves: revert_info = board.make_move(move) if revert_info is None: continue is_any_move = True args = (board.copy(), alpha, beta, analyze_launch_time) kwargs = { 'deep': 1, 'parent_alpha_beta': parent_alpha_beta, 'parent_ind': len(moves) } pool_args.append((self, args, kwargs)) moves.append(move) board.revert_move(revert_info) for cand, ind in self.pool().imap_unordered(pool_dfs_wrapper, pool_args): result.append(cand[0]) result[-1]['moves'].append(moves[ind]) # XXX: sorting is stable, it will never get newer variant # (which can be with wrong evaluation). result.sort(key=lambda x: x['evaluation'], reverse=(move_color == WHITE)) result = result[:self.lines] if len(result) == self.lines: if move_color == WHITE: parent_alpha_beta.value = max(parent_alpha_beta.value, result[-1]['evaluation']) else: parent_alpha_beta.value = min(parent_alpha_beta.value, result[-1]['evaluation']) # Here is the first time it could happen if move_color == WHITE: if parent_alpha_beta.value >= beta: break else: if alpha >= parent_alpha_beta.value: break if not is_any_move: sign = color_sign(move_color) if board.is_check(opposite=True): # Checkmate result = [{ 'evaluation': -sign * Board.MAX_EVALUATION, 'moves': [] }] else: # Draw result = [{'evaluation': 0, 'moves': []}] return result, None
def dfs(self, board, alpha, beta, analyze_launch_time, moves_to_consider=None, deep=0, max_material_diff=999, parent_alpha_beta=None, parent_ind=None): ''' !!!! This function should be multi-thread safe. !!!! It always returns result of non-zero length `moves_to_consider` - moves to consider, if None than all valid moves are considered only if deep < max_deep deep < max_deep - all moves max_deep <= deep < max_deep + max_deep_captures - captures (if check then all moves) max_deep + max_deep_captures <= deep < max_deep + max_deep_captures + max_deep_one_capture - one capture ''' if time.time() - analyze_launch_time > self.max_time: # If alpha or beta is determined here, than there is calculated one variant already if alpha >= -Board.MAX_EVALUATION: return [{ 'evaluation': alpha, # Return evaluation that will not affect result 'moves': [] }], parent_ind if beta <= Board.MAX_EVALUATION: return [{ 'evaluation': beta, # Return evaluation that will not affect result 'moves': [] }], parent_ind result = [] move_color = board.move_color sign = color_sign(move_color) lines = self.lines if deep == 0 else 1 is_any_move = False if deep >= self.max_deep: if (deep < self.max_deep + self.max_deep_captures and board.is_check(opposite=True)): moves = board.get_board_moves( capture_sort_key=Board.sort_take_by_value) else: result = self.board_evaluation(board) is_any_move = True if len(result) == lines: if move_color == WHITE: alpha = max(alpha, result[-1]['evaluation']) else: beta = min(beta, result[-1]['evaluation']) if (alpha >= beta or deep == self.max_deep + self.max_deep_captures + self.max_deep_one_capture): # No moves should be considered moves = [] else: moves = board.get_board_captures( capture_sort_key=Board.sort_take_by_value) else: moves = board.get_board_moves( capture_sort_key=Board.sort_take_by_value ) if moves_to_consider is None else moves_to_consider for move in moves: revert_info = board.make_move(move) if revert_info is None: continue is_any_move = True kwargs = {'deep': deep + 1} if deep >= self.max_deep + self.max_deep_captures: material_diff = 0 if move['captured_piece']: material_diff += PIECES[move['captured_piece']]['value'] if move['new_piece'] != move['piece']: material_diff += PIECES[move['new_piece']]['value'] if material_diff > max_material_diff: board.revert_move(revert_info) # Break recursion, do not consider line if opponent takes more valuable piece or # promote something valuable or neither both of this # Return something that will not affect result result = [{ 'evaluation': sign * (Board.MAX_EVALUATION + 1), 'moves': [] }] break kwargs['max_material_diff'] = material_diff cand, _ = self.dfs(board, alpha, beta, analyze_launch_time, **kwargs) board.revert_move(revert_info) result.append(cand[0]) result[-1]['moves'].append(move) # XXX: sorting is stable, it will never get newer variant # (which can be with wrong evaluation). result.sort(key=lambda x: x['evaluation'], reverse=(move_color == WHITE)) result = result[:lines] if len(result) == lines: if move_color == WHITE: alpha = max(alpha, result[-1]['evaluation']) else: beta = min(beta, result[-1]['evaluation']) # Refresh alpha/beta according to parent if parent_alpha_beta is not None: if move_color == WHITE: beta = parent_alpha_beta.value else: alpha = parent_alpha_beta.value if alpha >= beta: break if deep >= self.max_deep + self.max_deep_captures: # Consider only one take/promotion break if not is_any_move: if board.is_check(opposite=True): # Checkmate result = [{ 'evaluation': -sign * (Board.MAX_EVALUATION - deep), 'moves': [] }] else: # Draw result = [{'evaluation': 0, 'moves': []}] return result, parent_ind
print 'Time: {:.3f}'.format(e - s) print 'Iteration: {}'.format(iteration) analyze_max_deep = max_deep # TODO: use len(get_board_moves) or time if len(board.pieces) <= 12: print 'Max deep increased by 1' analyze_max_deep += 1 print 'Max deep: {}'.format(analyze_max_deep) print 'Lines: {}'.format(lines) print 'Play: {}'.format(play) print print_board(board) move_up_color = board.move_up_color move_color = board.move_color sign = color_sign(move_color) print print '{} goes up'.format(move_up_color.upper()) print '{} to move'.format(move_color.upper()) print 'Evaluation: {}'.format(board.evaluation) if play: print 'Total sleep: {:.3f}'.format(total_sleep) print if (play and move_color != move_up_color): print 'Waiting for opponent move' continue prev_first_line = first_line start_time = time.time()
def run_advicer(mode, max_deep, lines, board, board_hashes): print 'Run advicer...' max_deep_captures = 3 # a = time.time() # pre_analysis = run_analyzer( # max_deep=1, max_deep_captures=1, lines=999, board=board) # pre_moves = [ # line['moves'][-1] # for line in pre_analysis['result'] # if line['moves'] # ] # print '{:.3f}'.format(time.time() - a) # # pre_analysis = run_analyzer( # max_deep=max_deep, max_deep_captures=max_deep_captures, lines=lines, # board=board, moves_to_consider=pre_moves) # print '{:.3f}'.format(time.time() - a) # print # print analysis = run_analyzer(max_deep=max_deep, max_deep_captures=max_deep_captures, lines=lines, board=board) first_line = analysis['result'][0] opening_info = get_opening_info(board) if opening_info is not None: opening_analysis = run_analyzer( max_deep=max_deep, max_deep_captures=max_deep_captures, lines=1, board=board, moves_to_consider=[opening_info['move']]) # TODO: check moves == [] opening_first_line = opening_analysis['result'][0] if (opening_first_line['moves'] and abs(first_line['evaluation'] - opening_first_line['evaluation']) < 0.5): # If line is exist (moves != []) and evaluation is pretty close to best print 'Opening `{}` line selected: {}'.format( opening_info['name'], format_move(board, opening_info['move'])) first_line = opening_first_line # Consider repetition first_line_moves = first_line['moves'] sign = color_sign(board.move_color) if first_line_moves: revert_info = board.make_move(first_line_moves[-1]) board_hash = board.hash board.revert_move(revert_info) if board_hashes.get(board_hash, 0) >= 1: print print 'First line leads to two times repetition' proper_evaluation = -2.5 if sign * first_line['evaluation'] > proper_evaluation: # If position is not so bad, prevent three times repetition # Try to find another line analysis = run_analyzer(max_deep=max_deep, max_deep_captures=max_deep_captures, lines=3, board=board) result = analysis['result'] candidates = [] for line in result: # It will always has moves, because at least one line has moves (first_line) revert_info = board.make_move(line['moves'][-1]) board_hash = board.hash board.revert_move(revert_info) # Collect all appropriate lines if (sign * line['evaluation'] > proper_evaluation and board_hashes.get(board_hash, 0) <= 1): candidates.append((board_hashes.get(board_hash, 0), line)) # Use stability of sort # Select the rarest proper line (with better evaluation) candidates.sort(key=lambda x: x[0]) if candidates: first_line = candidates[0][1] else: print 'Not found any other good line' print 'Selected line: ({}) {}'.format( first_line['evaluation'], moves_stringify(board, first_line['moves'])) else: print 'Position is not so good to prevent repetitions' return first_line
def get_syzygy_best_move(board): ''' WDL - 6 pieces DTM - 5 pieces ''' if len(board.pieces) > 6: return None fen = get_fen_from_board(board) try: response = urllib2.urlopen( "https://syzygy-tables.info/api/v2?fen={}".format( urllib.quote(fen))).read() parsed = json.loads(response) except Exception: return None parsed_moves = [{ 'key': key, 'wdl': value['wdl'], 'dtm': value['dtm'] } for key, value in parsed['moves'].items()] if not parsed_moves: # Is it a draw? return None random.shuffle(parsed_moves) if len(board.pieces) == 6: parsed_moves.sort(key=lambda x: x['wdl']) else: parsed_moves.sort(key=lambda x: (x['wdl'], -x['dtm'])) parsed_move = parsed_moves[0] # TODO: promotions sign = color_sign(board.move_color) if parsed_move['wdl'] == 0: evaluation = 0 else: if parsed_move['dtm'] is None: evaluation = Board.MAX_EVALUATION / 2 else: evaluation = Board.MAX_EVALUATION / 2 - abs(parsed_move['dtm']) - 1 if parsed_move['wdl'] > 0: evaluation *= -1 evaluation *= sign position = name_to_cell(parsed_move['key'][:2]) new_position = name_to_cell(parsed_move['key'][2:4]) piece = board.pieces[position][0] new_piece = piece if len(parsed_move['key']) == 5: new_piece = get_piece_by_title(parsed_move['key'][4]) captured_piece, _ = board.pieces.get(new_position, (None, None)) move = { 'position': position, 'new_position': new_position, 'piece': piece, 'new_piece': new_piece, 'captured_piece': captured_piece } return {'evaluation': evaluation, 'moves': [move]}