Загрузка данных


import copy
from typing import List, Tuple, Optional

BOARD_SIZE = 8
WIN_SCORE = 100000

def convert_site_board_to_engine(raw_board: List[List[int]]) -> List[List[Optional[dict]]]:
    """Преобразует матрицу цифр с сайта в формат для движка."""
    engine_board = []
    for r in range(BOARD_SIZE):
        row = []
        for c in range(BOARD_SIZE):
            cell = raw_board[r][c]
            if cell == 1:
                row.append({'player': 'black', 'type': 'man'})
            elif cell == 2:
                row.append({'player': 'black', 'type': 'king'})
            elif cell == 3:
                row.append({'player': 'white', 'type': 'man'})
            elif cell == 4:
                row.append({'player': 'white', 'type': 'king'})
            else:
                row.append(None)
        engine_board.append(row)
    return engine_board

def move_to_text(move: dict) -> str:
    """Переводит путь хода в формат 'A3 -> C5 -> E7'."""
    cols = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
    path_strs = [f"{cols[c]}{r + 1}" for r, c in move['path']]
    
    separator = " бьет " if move['is_capture'] else " -> "
    return separator.join(path_strs)

def evaluate_board(board: List[List[Optional[dict]]]) -> int:
    score = 0
    for r in range(BOARD_SIZE):
        for c in range(BOARD_SIZE):
            piece = board[r][c]
            if not piece:
                continue

            val = 300 if piece['type'] == 'king' else 100

            if piece['type'] == 'man':
                advancement = (7 - r) if piece['player'] == 'white' else r
                val += advancement * 5

            if 2 <= r <= 5 and 2 <= c <= 5:
                val += 10

            if piece['player'] == 'white':
                score += val
            else:
                score -= val

    return score

def get_valid_moves(board: List[List[Optional[dict]]], player: str) -> List[dict]:
    captures = []
    quiet_moves = []

    for r in range(BOARD_SIZE):
        for c in range(BOARD_SIZE):
            piece = board[r][c]
            if not piece or piece['player'] != player:
                continue

            piece_captures = _find_captures_for_piece(board, r, c, piece)
            captures.extend(piece_captures)

            if not captures:
                piece_quiets = _find_quiet_moves_for_piece(board, r, c, piece)
                quiet_moves.extend(piece_quiets)

    # Правило обязательного максимального взятия (Русские шашки)
    if captures:
        max_captured_count = max(len(m['captured']) for m in captures)
        return [m for m in captures if len(m['captured']) == max_captured_count]

    return quiet_moves

def _find_quiet_moves_for_piece(board, r: int, c: int, piece: dict) -> List[dict]:
    moves = []
    # Диагонали для шашек
    dirs = [(-1, -1), (-1, 1), (1, -1), (1, 1)] if piece['type'] == 'king' else (
        [(-1, -1), (-1, 1)] if piece['player'] == 'white' else [(1, -1), (1, 1)]
    )

    for dr, dc in dirs:
        if piece['type'] == 'king':
            nr, nc = r + dr, c + dc
            while 0 <= nr < BOARD_SIZE and 0 <= nc < BOARD_SIZE and board[nr][nc] is None:
                moves.append({
                    'from': (r, c),
                    'to': (nr, nc),
                    'path': [(r, c), (nr, nc)],
                    'captured': [],
                    'is_capture': False
                })
                nr += dr
                nc += dc
        else:
            nr, nc = r + dr, c + dc
            if 0 <= nr < BOARD_SIZE and 0 <= nc < BOARD_SIZE and board[nr][nc] is None:
                moves.append({
                    'from': (r, c),
                    'to': (nr, nc),
                    'path': [(r, c), (nr, nc)],
                    'captured': [],
                    'is_capture': False
                })

    return moves

def _find_captures_for_piece(board, r: int, c: int, piece: dict, visited_captured=None, current_path=None) -> List[dict]:
    if visited_captured is None:
        visited_captured = []
    if current_path is None:
        current_path = [(r, c)]

    captures = []
    opponent = 'black' if piece['player'] == 'white' else 'white'
    dirs = [(-1, -1), (-1, 1), (1, -1), (1, 1)]  # Бьют во все 4 стороны

    if piece['type'] == 'king':
        for dr, dc in dirs:
            nr, nc = r + dr, c + dc
            found_enemy = None
            while 0 <= nr < BOARD_SIZE and 0 <= nc < BOARD_SIZE:
                cell = board[nr][nc]
                if cell:
                    if cell['player'] == opponent and (nr, nc) not in visited_captured:
                        if found_enemy is None:
                            found_enemy = (nr, nc)
                        else:
                            break
                    else:
                        break
                else:
                    if found_enemy:
                        new_captured = visited_captured + [found_enemy]
                        new_path = current_path + [(nr, nc)]
                        temp_board = copy.deepcopy(board)
                        temp_board[nr][nc] = temp_board[r][c]
                        temp_board[r][c] = None

                        further = _find_captures_for_piece(temp_board, nr, nc, piece, new_captured, new_path)
                        if further:
                            captures.extend(further)
                        else:
                            captures.append({
                                'from': current_path[0],
                                'to': (nr, nc),
                                'path': new_path,
                                'captured': new_captured,
                                'is_capture': True
                            })
                nr += dr
                nc += dc
    else:
        for dr, dc in dirs:
            mid_r, mid_c = r + dr, c + dc
            land_r, land_c = r + 2 * dr, c + 2 * dc

            if (0 <= land_r < BOARD_SIZE and 0 <= land_c < BOARD_SIZE and
                board[land_r][land_c] is None and
                board[mid_r][mid_c] and board[mid_r][mid_c]['player'] == opponent and
                (mid_r, mid_c) not in visited_captured):

                new_captured = visited_captured + [(mid_r, mid_c)]
                new_path = current_path + [(land_r, land_c)]
                temp_board = copy.deepcopy(board)
                promoted = (land_r == 0 if piece['player'] == 'white' else land_r == 7)
                p_copy = copy.deepcopy(piece)
                if promoted:
                    p_copy['type'] = 'king'

                temp_board[land_r][land_c] = p_copy
                temp_board[r][c] = None

                further = _find_captures_for_piece(temp_board, land_r, land_c, p_copy, new_captured, new_path)
                if further:
                    captures.extend(further)
                else:
                    captures.append({
                        'from': current_path[0],
                        'to': (land_r, land_c),
                        'path': new_path,
                        'captured': new_captured,
                        'is_capture': True
                    })

    return captures

def make_move(board: List[List[Optional[dict]]], move: dict) -> List[List[Optional[dict]]]:
    new_board = copy.deepcopy(board)
    fr, fc = move['from']
    tr, tc = move['to']

    piece = new_board[fr][fc]
    new_board[fr][fc] = None
    new_board[tr][tc] = piece

    if piece['type'] == 'man':
        if (piece['player'] == 'white' and tr == 0) or (piece['player'] == 'black' and tr == 7):
            piece['type'] = 'king'

    for cr, cc in move['captured']:
        new_board[cr][cc] = None

    return new_board

def quiescence(board, player: str, alpha: int, beta: int) -> int:
    static_eval = evaluate_board(board)
    current_eval = static_eval if player == 'white' else -static_eval

    if current_eval >= beta:
        return beta
    if current_eval > alpha:
        alpha = current_eval

    moves = get_valid_moves(board, player)
    captures = [m for m in moves if m['is_capture']]

    if not captures:
        return current_eval

    for move in captures:
        next_board = make_move(board, move)
        next_player = 'black' if player == 'white' else 'white'
        val = -quiescence(next_board, next_player, -beta, -alpha)

        if val >= beta:
            return beta
        if val > alpha:
            alpha = val

    return alpha

def negamax(board, depth: int, alpha: int, beta: int, player: str) -> Tuple[int, Optional[dict]]:
    moves = get_valid_moves(board, player)

    if not moves:
        return -WIN_SCORE + (10 - depth), None

    if depth <= 0:
        return quiescence(board, player, alpha, beta), None

    best_move = moves[0]
    best_val = -float('inf')

    moves.sort(key=lambda m: len(m['captured']), reverse=True)

    for move in moves:
        next_board = make_move(board, move)
        next_player = 'black' if player == 'white' else 'white'

        val, _ = negamax(next_board, depth - 1, -beta, -alpha, next_player)
        val = -val

        if val > best_val:
            best_val = val
            best_move = move

        if val > alpha:
            alpha = val

        if alpha >= beta:
            break

    return best_val, best_move

def get_best_move(board: List[List[Optional[dict]]], player: str, depth: int = 5) -> Optional[dict]:
    _, move = negamax(board, depth, -float('inf'), float('inf'), player)
    return move