Exemple #1
0
    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
Exemple #2
0
    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
Exemple #3
0
    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
        }
Exemple #4
0
    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
Exemple #5
0
    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
Exemple #6
0
    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
Exemple #7
0
    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
Exemple #8
0
    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
Exemple #9
0
            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()
Exemple #10
0
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
Exemple #11
0
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]}