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


import time
import random
import keyboard
import chess
import chess.engine
import os
import threading
import tkinter as tk
import ctypes

from analysis import ChessAnalyzer
import crop
from move import ChessMover


class ScreenOverlay:
    """Прозрачный оверлей поверх экрана для отрисовки подсветки ходов"""
    def __init__(self):
        self.root = None
        self.canvas = None
        self.show_highlight = False
        self.current_move = None

    def start(self):
        self.root = tk.Tk()
        # Без рамок, всегда поверх всех окон
        self.root.overrideredirect(True)
        self.root.attributes("-topmost", True)
        self.root.attributes("-transparentcolor", "black")

        screen_w = self.root.winfo_screenwidth()
        screen_h = self.root.winfo_screenheight()
        self.root.geometry(f"{screen_w}x{screen_h}+0+0")

        # Включаем пропуск кликов мыши сквозь окно (Click-Through для Windows)
        hwnd = ctypes.windll.user32.GetParent(self.root.winfo_id())
        style = ctypes.windll.user32.GetWindowLongW(hwnd, -20)
        ctypes.windll.user32.SetWindowLongW(hwnd, -20, style | 0x00000020 | 0x00080000)

        self.canvas = tk.Canvas(self.root, bg="black", highlightthickness=0)
        self.canvas.pack(fill="both", expand=True)

        self.root.mainloop()

    def toggle_highlight(self):
        """Включение/выключение отображения по клавише V"""
        self.show_highlight = not self.show_highlight
        status = "ВКЛЮЧЕНА" if self.show_highlight else "ВЫКЛЮЧЕНА"
        print(f"\n[OVERLAY]: Подсветка ходов {status}")
        if self.root:
            self.root.after(0, self.redraw)

    def update_move(self, src_box, dst_box):
        """Обновление координат хода (Зеленый / Красный)"""
        self.current_move = {"from": src_box, "to": dst_box}
        if self.root and self.show_highlight:
            self.root.after(0, self.redraw)

    def clear_move(self):
        self.current_move = None
        if self.root:
            self.root.after(0, self.redraw)

    def redraw(self):
        if not self.canvas:
            return
        
        self.canvas.delete("all")
        if not self.show_highlight or not self.current_move:
            return

        # 1. Зелёная рамка (Откуда сходить)
        fx, fy, fw, fh = self.current_move["from"]
        self.canvas.create_rectangle(
            fx, fy, fx + fw, fy + fh, 
            outline="#00FF00", width=4
        )

        # 2. Красная рамка (Куда сходить)
        tx, ty, tw, th = self.current_move["to"]
        self.canvas.create_rectangle(
            tx, ty, tx + tw, ty + th, 
            outline="#FF0000", width=4
        )


class ChessLogic:
    def __init__(self, engine_path="stockfish.exe", mode="medium"):
        self.analyzer = ChessAnalyzer()
        self.mover = ChessMover()     # Модуль физических ходов мышью
        self.engine_path = engine_path

        # --- ИНИЦИАЛИЗА ОВЕРЛЕЯ ---
        self.overlay = ScreenOverlay()
        self.overlay_thread = threading.Thread(target=self.overlay.start, daemon=True)
        self.overlay_thread.start()

        # --- ИНИЦИАЛИЗА ДВИЖКА (Единый процесс) ---
        self.engine = None
        self._init_engine()

        # --- НАСТРОЙКИ СИЛЫ И ДВИЖКА ---
        self.modes_list = ['easy', 'medium', 'hard', 'max']
        self.mode = mode.lower()
        self.skill_level = 10          # Уровень навыка Stockfish (0..20)
        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.board_state = []
        self.my_color = 'w'
        self.active_color = 'w'
        self.en_passant_target = None

        # Обработка зависаний и авто-синхронизация
        self.stuck_frame_count = 0
        self.last_raw_board = None

        self.set_difficulty_mode(self.mode)

    def _init_engine(self):
        """Запуск единого постоянного процесса Stockfish"""
        try:
            if os.path.exists(self.engine_path):
                self.engine = chess.engine.SimpleEngine.popen_uci(self.engine_path)
                print(f"[ENGINE]: Stockfish успешно запущен из {self.engine_path}")
            else:
                print(f"[!] Ошибка: Исполняемый файл Stockfish не найден по пути: {self.engine_path}")
        except Exception as e:
            print(f"[!] Не удалось инициализировать Stockfish: {e}")

    def close(self):
        """Завершение процесса движка при выходе"""
        if self.engine:
            try:
                self.engine.quit()
                print("[ENGINE]: Процесс Stockfish остановлен.")
            except Exception:
                pass

    def get_square_rect(self, sq_str):
        """Вычисляет экранные координаты (x, y, w, h) конкретной клетки (например, 'e2')"""
        board_rect = None
        # Пытаемся получить координаты доски из модуля crop
        if hasattr(crop, 'get_board_rect'):
            board_rect = crop.get_board_rect()
        elif hasattr(crop, 'last_board_rect'):
            board_rect = crop.last_board_rect
        elif hasattr(crop, 'board_rect'):
            board_rect = crop.board_rect

        if not board_rect and hasattr(crop, 'find_chessboard_strictly_1to1'):
            res = crop.find_chessboard_strictly_1to1()
            if res and isinstance(res, tuple) and len(res) >= 1 and isinstance(res[0], (tuple, list)):
                board_rect = res[0]  # (x, y, w, h)

        if not board_rect:
            return None

        bx, by, bw, bh = board_rect
        sq_w = bw / 8.0
        sq_h = bh / 8.0

        file_idx = ord(sq_str[0]) - ord('a')  # 0..7 (a..h)
        rank_idx = int(sq_str[1]) - 1         # 0..7 (1..8)

        if self.my_color == 'w':
            col = file_idx
            row = 7 - rank_idx
        else:
            col = 7 - file_idx
            row = rank_idx

        x = int(bx + col * sq_w)
        y = int(by + row * sq_h)
        return (x, y, int(sq_w), int(sq_h))

    def set_difficulty_mode(self, mode):
        """Конфигурация параметров под выбранный режим"""
        self.mode = mode.lower()

        if self.mode == 'easy':
            self.skill_level = 3
            self.multipv_count = 3
            self.blunder_threshold = 300
            print(f"\n[ENGINE CONFIG]: Режим 'EASY' | MultiPV: Top-3 | Защита от очевидных зевов")

        elif self.mode == 'medium':
            self.skill_level = 10
            self.multipv_count = 2
            self.blunder_threshold = 200
            print(f"\n[ENGINE CONFIG]: Режим 'MEDIUM' | MultiPV: Top-2 | Защита от зевов")

        elif self.mode == 'hard':
            self.skill_level = 16
            self.multipv_count = 2
            self.blunder_threshold = 150
            print(f"\n[ENGINE CONFIG]: Режим 'HARD' | MultiPV: Top-2 | Строгий контроль зевов")

        elif self.mode == 'max':
            self.skill_level = 20
            self.multipv_count = 1
            self.blunder_threshold = 0
            print(f"\n[ENGINE CONFIG]: Режим 'MAX' | MultiPV: 1 (Идеальный ход)")

    def cycle_difficulty_mode(self):
        """Переключение уровня сложности по кругу (Клавиша M)"""
        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])

    def detect_auto_color(self, board_matrix):
        """Определяет цвет игрока по фигуры на 2 нижних строчках экрана"""
        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

        if black_count > white_count:
            return 'b'
        return 'w'

    def reset_castling_rights(self):
        """Проверяет наличие королей и ладей на стартовых позициях"""
        oriented = self.get_oriented_board_state()
        if not oriented or len(oriented) != 8:
            self.castling_rights = {'K': True, 'Q': True, 'k': True, 'q': True}
            return

        self.castling_rights = {
            'K': (oriented[7][4] == 'K' and oriented[7][7] == 'R'),
            'Q': (oriented[7][4] == 'K' and oriented[7][0] == 'R'),
            'k': (oriented[0][4] == 'k' and oriented[0][7] == 'r'),
            'q': (oriented[0][4] == 'k' and oriented[0][0] == 'r')
        }

    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 update_en_passant_target(self, move, board_obj):
        """Рассчитывает поле для взятия на проходе"""
        if not move or not board_obj:
            self.en_passant_target = None
            return

        piece = board_obj.piece_at(move.from_square)
        if piece and piece.piece_type == chess.PAWN:
            from_rank = chess.square_rank(move.from_square)
            to_rank = chess.square_rank(move.to_square)
            if abs(from_rank - to_rank) == 2:
                ep_rank = (from_rank + to_rank) // 2
                ep_file = chess.square_file(move.from_square)
                row = 7 - ep_rank
                col = ep_file
                self.en_passant_target = {'target_pos': (row, col)}
                return

        self.en_passant_target = None

    def get_castling_rights(self):
        """Формирует FEN-строку рокировок"""
        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 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 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} 0 1"

    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)

            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:
            print(f"[!] Ошибка валидации FEN/Правил: {e}")
            return False, None

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

    def on_start_pressed(self):
        self.trigger_analysis = True

    def on_color_toggle_pressed(self):
        self.trigger_toggle_color = True

    def toggle_my_color(self):
        """Принудительная смена цвета игрока и ориентации доски (Клавиша C)"""
        self.my_color = 'b' if self.my_color == 'w' else 'w'
        color_name = "БЕЛЫЕ (w) [Прямая ориентация]" if self.my_color == 'w' else "ЧЁРНЫЕ (b) [Развёрнутая ориентация]"
        print(f"\n[MANUAL COLOR SET]: Принудительно установлен ваш цвет -> {color_name}")
        
        self.reset_castling_rights()

        if not self.is_starting_position():
            self.active_color = self.my_color
        else:
            self.active_color = 'w'

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

    def toggle_active_turn(self):
        """Принудительное переключение чей сейчас ход (Клавиша T)"""
        self.active_color = 'b' if self.active_color == 'w' else 'w'
        turn_str = "БЕЛЫЕ (w)" if self.active_color == 'w' else "ЧЁРНЫЕ (b)"
        print(f"\n[MANUAL TURN TOGGLE]: Очередь хода изменена -> Сейчас ходят {turn_str}")

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

    def process_initial_analysis(self):
        print("\n[!] Нажата 'S'. Первичный анализ доски...")
        board_img = crop.get_board_image() if hasattr(crop, 'get_board_image') else crop.find_chessboard_strictly_1to1()[1]
        if board_img is None:
            print("[!] Ошибка: Не удалось получить изображение доски!")
            return

        self.board_state = self.analyzer.get_board_state(board_img)
        if self.board_state:
            self.is_analyzed = True
            self.stuck_frame_count = 0
            self.last_raw_board = None
            print("[√] Позиция успешно проанализирована!")

            # Авто-определение цвета
            self.my_color = self.detect_auto_color(self.board_state)
            color_str = "БЕЛЫЕ (w)" if self.my_color == 'w' else "ЧЁРНЫЕ (b) [Развернута]"
            print(f"[AUTO-COLOR]: Авто-определение фигуры внизу -> {color_str}")
            print("  (Если бот ошибся с цветом в эндшпиле — нажмите 'C' для принудительного переключения)")

            self.reset_castling_rights()

            if self.is_starting_position():
                self.active_color = 'w'
                print("[INFO] Обнаружена начальная расстановка: первый ход делают БЕЛЫЕ ('w').")
            else:
                self.active_color = self.my_color

            self.print_board()
            self.evaluate_current_turn()
        else:
            print("[!] Ошибка: Не удалось распознать фигуры на доске.")

    def analyze_with_engine(self, fen_string, time_limit=0.5):
        if not fen_string or not self.engine:
            return None

        try:
            board = chess.Board(fen_string)
            if not board.is_valid():
                print("[! WARN] Позиция на доске невалидна, пропускаем вызов движка...")
                return None

            self.engine.configure({"Skill Level": self.skill_level})

            pv_count = 1 if self.mode == 'max' else self.multipv_count
            limit = chess.engine.Limit(time=time_limit, depth=self.fixed_depth)

            print(f"[ENGINE THINKING] Глубина: {self.fixed_depth} | Skill Level: {self.skill_level}/20 | Mode: {self.mode.upper()}")

            analysis = self.engine.analyse(board, limit, multipv=pv_count)

            if not analysis:
                print("[!] Ошибка: Движок не предложил ни одного хода!")
                return None

            if not isinstance(analysis, list):
                analysis = [analysis]

            valid_candidates = []
            best_score = None

            for i, info in enumerate(analysis):
                if "pv" in info and len(info["pv"]) > 0:
                    move = info["pv"][0]
                    score_obj = info.get("score")
                    cp_score = score_obj.pov(board.turn).score(mate_score=10000) if score_obj else 0

                    if i == 0:
                        best_score = cp_score
                        valid_candidates.append((move, cp_score, 1))
                    else:
                        score_diff = (best_score - cp_score) if best_score is not None else 0

                        if self.blunder_threshold > 0 and score_diff >= self.blunder_threshold:
                            print(f" [BLUNDER PREVENTED] Ход PV{i+1} ({board.san(move)}) отброшен")
                        else:
                            valid_candidates.append((move, cp_score, i + 1))

            if not valid_candidates:
                return None

            if self.mode == 'max' or len(valid_candidates) == 1:
                chosen_move, chosen_score, pv_num = valid_candidates[0]
                choice_info = f"PV{pv_num} (Лучший ход)"
            else: