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


import os
import random
import time
import tkinter as tk
from tkinter import ttk

import chess
import chess.engine
import keyboard

from analysis import ChessAnalyzer
import crop
from move import ChessMover


class ChessLogic:
    def __init__(self, engine_path="stockfish.exe", mode="medium"):
        self.analyzer = ChessAnalyzer()
        self.mover = ChessMover()
        self.engine_path = engine_path

        # Режимы силы
        self.modes_list = ["easy", "medium", "hard", "max"]
        self.mode = mode.lower() if mode else "medium"
        self.skill_level = 10
        self.multipv_count = 2
        self.blunder_threshold = 200
        self.fixed_depth = 12

        # Права на рокировку
        self.castling_rights = {"K": True, "Q": True, "k": True, "q": True}

        # Состояние
        self.trigger_analysis = False
        self.trigger_toggle_color = False
        self.is_analyzed = False
        self.running = False

        self.board_state = []
        self.my_color = "w"
        self.active_color = "w"
        self.en_passant_target = None
        self.halfmove_clock = 0
        self.fullmove_number = 1

        # Восстановление после шумных кадров
        self.pending_board_state = None
        self.pending_counter = 0
        self.illegal_counter = 0
        self.max_pending_frames = 2
        self.max_illegal_frames = 3

        # Последние данные
        self.last_move_uci = "-"
        self.last_status = "Ожидание запуска"
        self.last_fen = ""
        self.last_detected_color = None

        # GUI
        self.root = None
        self.ui_ready = False
        self.ui_status_var = None
        self.ui_color_var = None
        self.ui_turn_var = None
        self.ui_mode_var = None
        self.ui_fen_var = None
        self.ui_move_var = None
        self.ui_legal_var = None

        self.engine = None
        self.set_difficulty_mode(self.mode)
        self._open_engine()

    # -----------------------------
    # БАЗОВЫЕ ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ
    # -----------------------------
    def _set_status(self, text):
        self.last_status = text
        if self.ui_ready and self.root is not None and self.ui_status_var is not None:
            try:
                self.root.after(0, lambda: self.ui_status_var.set(text))
            except Exception:
                pass
        else:
            print(text)

    def _set_ui(self, var_name, value):
        if not self.ui_ready or self.root is None:
            return
        var = getattr(self, var_name, None)
        if var is None:
            return
        try:
            self.root.after(0, lambda v=var, val=value: v.set(val))
        except Exception:
            pass

    def _square_to_coords(self, square_index):
        row = 7 - chess.square_rank(square_index)
        col = chess.square_file(square_index)
        return row, col

    def _count_differences(self, a, b):
        if not a or not b:
            return 64
        diff = 0
        for r in range(8):
            for c in range(8):
                if a[r][c] != b[r][c]:
                    diff += 1
        return diff

    def _build_fen_from_state(self, board_state, active_color=None, castling=None, ep="-", halfmove=0, fullmove=1):
        if not board_state:
            return ""

        fen_rows = []
        for row in board_state:
            empty_count = 0
            row_fen = ""
            for cell in row:
                if cell == "":
                    empty_count += 1
                else:
                    if empty_count > 0:
                        row_fen += str(empty_count)
                        empty_count = 0
                    row_fen += cell
            if empty_count > 0:
                row_fen += str(empty_count)
            fen_rows.append(row_fen)

        position_fen = "/".join(fen_rows)
        if active_color is None:
            active_color = self.active_color
        if castling is None:
            castling = self.get_castling_rights()
        return f"{position_fen} {active_color} {castling} {ep} {halfmove} {fullmove}"

    def _board_signature(self, board_state):
        if not board_state:
            return None
        return tuple(tuple(row) for row in board_state)

    def _normalize_image_input(self, image_input):
        if image_input is None:
            return None, {}
        if isinstance(image_input, dict):
            return image_input.get("image"), image_input
        if isinstance(image_input, str):
            if not os.path.exists(image_input):
                print(f"[!] Файл '{image_input}' не найден!")
                return None, {}
            return None if crop is None else __import__("cv2").imread(image_input), {}
        return image_input, {}

    def _get_board_image(self):
        if hasattr(crop, "get_board_image"):
            return crop.get_board_image()
        if hasattr(crop, "find_chessboard_strictly_1to1"):
            return crop.find_chessboard_strictly_1to1()[1]
        return None

    # -----------------------------
    # STOCKFISH
    # -----------------------------
    def _open_engine(self):
        try:
            if os.path.exists(self.engine_path):
                self.engine = chess.engine.SimpleEngine.popen_uci(self.engine_path)
                self.engine.configure({"Skill Level": self.skill_level})
                self._set_status(f"[ENGINE]: запущен {self.engine_path}")
            else:
                self.engine = None
                self._set_status(f"[ENGINE]: не найден {self.engine_path}")
        except Exception as e:
            self.engine = None
            self._set_status(f"[ENGINE]: ошибка запуска — {e}")

    def _close_engine(self):
        try:
            if self.engine is not None:
                self.engine.quit()
        except Exception:
            pass
        self.engine = None

    # -----------------------------
    # РЕЖИМЫ СЛОЖНОСТИ
    # -----------------------------
    def set_difficulty_mode(self, mode):
        self.mode = mode.lower() if mode else "medium"
        if self.mode == "easy":
            self.skill_level = 3
            self.multipv_count = 3
            self.blunder_threshold = 300
            self._set_status("[ENGINE CONFIG]: EASY")
        elif self.mode == "medium":
            self.skill_level = 10
            self.multipv_count = 2
            self.blunder_threshold = 200
            self._set_status("[ENGINE CONFIG]: MEDIUM")
        elif self.mode == "hard":
            self.skill_level = 16
            self.multipv_count = 2
            self.blunder_threshold = 150
            self._set_status("[ENGINE CONFIG]: HARD")
        elif self.mode == "max":
            self.skill_level = 20
            self.multipv_count = 1
            self.blunder_threshold = 0
            self._set_status("[ENGINE CONFIG]: MAX")
        else:
            self.mode = "medium"
            self.skill_level = 10
            self.multipv_count = 2
            self.blunder_threshold = 200
            self._set_status("[ENGINE CONFIG]: MEDIUM")

        if self.engine is not None:
            try:
                self.engine.configure({"Skill Level": self.skill_level})
            except Exception:
                pass

        self._set_ui("ui_mode_var", self.mode.upper())

    def cycle_difficulty_mode(self):
        current_idx = self.modes_list.index(self.mode) if self.mode in self.modes_list else 1
        next_idx = (current_idx + 1) % len(self.modes_list)
        self.set_difficulty_mode(self.modes_list[next_idx])
        self._set_status(f"[MODE]: {self.mode.upper()}")

    # -----------------------------
    # РАБОТА С ДОСКОЙ / FEN
    # -----------------------------
    def detect_auto_color(self, board_matrix):
        """Определяет цвет фигур на нижней части экрана."""
        if not board_matrix or len(board_matrix) < 8:
            return "w"

        white_count = 0
        black_count = 0
        for row in board_matrix[6:8]:
            for cell in row:
                if cell.isupper():
                    white_count += 1
                elif cell.islower():
                    black_count += 1
        return "b" if black_count > white_count else "w"

    def reset_castling_rights(self):
        self.castling_rights = {"K": True, "Q": True, "k": True, "q": True}

    def update_castling_rights(self, move):
        if not move:
            return
        from_sq = move.from_square
        to_sq = move.to_square

        if from_sq == chess.E1:
            self.castling_rights["K"] = False
            self.castling_rights["Q"] = False
        elif from_sq == chess.E8:
            self.castling_rights["k"] = False
            self.castling_rights["q"] = False

        if from_sq == chess.H1 or to_sq == chess.H1:
            self.castling_rights["K"] = False
        if from_sq == chess.A1 or to_sq == chess.A1:
            self.castling_rights["Q"] = False
        if from_sq == chess.H8 or to_sq == chess.H8:
            self.castling_rights["k"] = False
        if from_sq == chess.A8 or to_sq == chess.A8:
            self.castling_rights["q"] = False

    def get_castling_rights(self):
        oriented = self.get_oriented_board_state()
        if not oriented or len(oriented) != 8:
            return "KQkq"

        c_K = self.castling_rights.get("K", True) and oriented[7][4] == "K" and oriented[7][7] == "R"
        c_Q = self.castling_rights.get("Q", True) and oriented[7][4] == "K" and oriented[7][0] == "R"
        c_k = self.castling_rights.get("k", True) and oriented[0][4] == "k" and oriented[0][7] == "r"
        c_q = self.castling_rights.get("q", True) and oriented[0][4] == "k" and oriented[0][0] == "r"

        rights = ""
        if c_K:
            rights += "K"
        if c_Q:
            rights += "Q"
        if c_k:
            rights += "k"
        if c_q:
            rights += "q"
        return rights if rights else "-"

    def get_oriented_board_state(self):
        if not self.board_state:
            return None
        if self.my_color == "w":
            return self.board_state
        return [row[::-1] for row in self.board_state[::-1]]

    def get_oriented_matrix_for(self, state_matrix):
        if self.my_color == "w":
            return state_matrix
        return [row[::-1] for row in state_matrix[::-1]]

    def board_to_oriented_matrix(self, board_obj):
        matrix = []
        for r in range(8):
            row = []
            for c in range(8):
                square = chess.square(c, 7 - r)
                piece = board_obj.piece_at(square)
                row.append(piece.symbol() if piece else "")
            matrix.append(row)
        return matrix

    def is_starting_position(self):
        oriented = self.get_oriented_board_state()
        if not oriented:
            return False
        standard_start = [
            ["r", "n", "b", "q", "k", "b", "n", "r"],
            ["p", "p", "p", "p", "p", "p", "p", "p"],
            ["", "", "", "", "", "", "", ""],
            ["", "", "", "", "", "", "", ""],
            ["", "", "", "", "", "", "", ""],
            ["", "", "", "", "", "", "", ""],
            ["P", "P", "P", "P", "P", "P", "P", "P"],
            ["R", "N", "B", "Q", "K", "B", "N", "R"],
        ]
        return oriented == standard_start

    def get_fen_string(self):
        oriented_board = self.get_oriented_board_state()
        if not oriented_board:
            return ""

        fen_rows = []
        for row in oriented_board:
            empty_count = 0
            row_fen = ""
            for cell in row:
                if cell == "":
                    empty_count += 1
                else:
                    if empty_count > 0:
                        row_fen += str(empty_count)
                        empty_count = 0
                    row_fen += cell
            if empty_count > 0:
                row_fen += str(empty_count)
            fen_rows.append(row_fen)

        position_fen = "/".join(fen_rows)
        ep_str = "-"
        if self.en_passant_target:
            ep_str = self.convert_coords_to_notation(*self.en_passant_target["target_pos"])
        castling_rights = self.get_castling_rights()
        return f"{position_fen} {self.active_color} {castling_rights} {ep_str} {self.halfmove_clock} {self.fullmove_number}"

    def convert_coords_to_notation(self, row, col):
        files = ["a", "b", "c", "d", "e", "f", "g", "h"]
        ranks = ["8", "7", "6", "5", "4", "3", "2", "1"]
        return f"{files[col]}{ranks[row]}"

    def _count_legal_diff(self, old_state, new_state):
        if not old_state or not new_state:
            return 64
        diff = 0
        for r in range(8):
            for c in range(8):
                if old_state[r][c] != new_state[r][c]:
                    diff += 1
        return diff

    def is_legal_board_transition(self, new_board_state):
        current_fen = self.get_fen_string()
        if not current_fen:
            return False, None

        try:
            board = chess.Board(current_fen)
            new_oriented = self.get_oriented_matrix_for(new_board_state)

            # Быстрая отсечка шумных кадров
            diff_cells = self._count_legal_diff(self.get_oriented_board_state(), new_oriented)
            if diff_cells > 8:
                return False, None

            for move in board.legal_moves:
                test_board = board.copy()
                test_board.push(move)
                test_oriented = self.board_to_oriented_matrix(test_board)
                if test_oriented == new_oriented:
                    return True, move

            return False, None
        except Exception as e:
            self._set_status(f"[!] Ошибка валидации: {e}")
            return False, None

    def _accept_transition(self, new_board_state, move_obj, source="legal", detected_color=None):
        self.board_state = new_board_state
        self.update_castling_rights(move_obj)

        try:
            fen_before = self.get_fen_string()
            board = chess.Board(fen_before)
            if move_obj is not None:
                board.push(move_obj)

            self.active_color = "w" if board.turn == chess.WHITE else "b"
            self.halfmove_clock = board.halfmove_clock
            self.fullmove_number = board.fullmove_number
            self.en_passant_target = None if board.ep_square is None else {
                "target_pos": self._square_to_coords(board.ep_square)
            }
        except Exception:
            if detected_color in ("w", "b"):
                self.active_color = detected_color
            self.en_passant_target = None

        self.pending_board_state = None
        self.pending_counter = 0
        self.illegal_counter = 0
        self.last_move_uci = move_obj.uci() if move_obj else "-"
        self.last_fen = self.get_fen_string()

        self._set_status(f"[OK]: Позиция обновлена ({source}).")
        self._sync_ui_state()

    def _resync_with_candidate(self, new_board_state, detected_color=None):
        """Пытается восстановить синхронизацию после серии плохих кадров."""
        try:
            active_color = detected_color if detected_color in ("w", "b") else self.active_color
            fen = self._build_fen_from_state(
                new_board_state,
                active_color=active_color,
                castling=self.get_castling_rights(),
                ep="-",
                halfmove=self.halfmove_clock,
                fullmove=self.fullmove_number,
            )
            board = chess.Board(fen)
            if board.is_valid():
                self.board_state = new_board_state
                self.active_color = active_color
                self.en_passant_target = None
                self.pending_board_state = None
                self.pending_counter = 0
                self.illegal_counter = 0
                self.last_fen = fen
                self._set_status("[RESYNC]: Состояние восстановлено по новому кадру.")
                self._sync_ui_state()
                return True
        except Exception:
            pass
        return False

    # -----------------------------
    # GUI / HOTKEYS
    # -----------------------------
    def on_start_pressed(self):
        self.trigger_analysis = True

    def on_color_toggle_pressed(self):
        self.trigger_toggle_color = True

    def toggle_auto_move_and_execute(self):
        self.mover.toggle_auto_move()
        if self.mover.auto_move_enabled and self.is_analyzed and self.active_color == self.my_color:
            self._set_status("[AUTO-MOVE]: Авто-ход включён. Анализируем позицию...")
            self.evaluate_current_turn()

    def toggle_my_color(self):
        self.my_color = "b" if self.my_color == "w" else "w"
        color_name = "БЕЛЫЕ (w)" if self.my_color == "w" else "ЧЁРНЫЕ (b)"
        self._set_status(f"[COLOR]: {color_name}")
        self.reset_castling_rights()

        if self.is_starting_position():
            self.active_color = "w"

        self._sync_ui_state()
        if self.is_analyzed and self.board_state:
            self.evaluate_current_turn()

    def _build_gui(self):
        self.root = tk.Tk()
        self.root.title("Chess Bot")
        self.root.geometry("700x470")
        self.root.resizable(False, False)
        self.root.protocol("WM_DELETE_WINDOW", self.shutdown)

        main = ttk.Frame(self.root, padding=12)
        main.pack(fill="both", expand=True)

        self.ui_status_var = tk.StringVar(value=self.last_status)
        self.ui_color_var = tk.StringVar(value=self.my_color)
        self.ui_turn_var = tk.StringVar(value=self.active_color)
        self.ui_mode_var = tk.StringVar(value=self.mode.upper())
        self.ui_fen_var = tk.StringVar(value=self.last_fen if self.last_fen else "-")
        self.ui_move_var = tk.StringVar(value=self.last_move_uci if self.last_move_uci else "-")
        self.ui_legal_var = tk.StringVar(value="OK")

        ttk.Label(main, text="Chess Bot", font=("Segoe UI", 16, "bold")).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 10))

        ttk.Label(main, text="Статус:").grid(row=1, column=0, sticky="w")
        ttk.Label(main, textvariable=self.ui_status_var, wraplength=560).grid(row=1, column=1, sticky="w")

        ttk.Label(main, text="Ваш цвет:").grid(row=2, column=0, sticky="w")
        ttk.Label(main, textvariable=self.ui_color_var).grid(row=2, column=1, sticky="w")

        ttk.Label(main, text="Чей ход:").grid(row=3, column=0, sticky="w")
        ttk.Label(main, textvariable=self.ui_turn_var).grid(row=3, column=1, sticky="w")

        ttk.Label(main, text="Режим:").grid(row=4, column=0, sticky="w")
        ttk.Label(main, textvariable=self.ui_mode_var).grid(row=4, column=1, sticky="w")

        ttk.Label(main, text="Последний ход:").grid(row=5, column=0, sticky="w")
        ttk.Label(main, textvariable=self.ui_move_var).grid(row=5, column=1, sticky="w")

        ttk.Label(main, text="Легальность:").grid(row=6, column=0, sticky="w")
        ttk.Label(main, textvariable=self.ui_legal_var).grid(row=6, column=1, sticky="w")

        ttk.Label(main, text="FEN:").grid(row=7, column=0, sticky="nw", pady=(10, 0))
        ttk.Label(main, textvariable=self.ui_fen_var, wraplength=560, justify="left").grid(row=7, column=1, sticky="w", pady=(10, 0))

        btns = ttk.Frame(main)
        btns.grid(row=8, column=0, columnspan=2, sticky="w", pady=(18, 0))

        ttk.Button(btns, text="Старт (S)", command=self.on_start_pressed).grid(row=0, column=0, padx=(0, 6))
        ttk.Button(btns, text="Цвет (C)", command=self.on_color_toggle_pressed).grid(row=0, column=1, padx=(0, 6))
        ttk.Button(btns, text="Режим (M)", command=self.cycle_difficulty_mode).grid(row=0, column=2, padx=(0, 6))
        ttk.Button(btns, text="Автоход (L)", command=self.toggle_auto_move_and_execute).grid(row=0, column=3, padx=(0, 6))
        ttk.Button(btns, text="Humanly (H)", command=self.mover.toggle_humanly_mode).grid(row=0, colu