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


import time
import random
import keyboard
import chess
import chess.engine
import os
import threading
import queue
import tkinter as tk
from tkinter import ttk

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()
        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.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 = tk.StringVar(value="Ожидание запуска") if tk._default_root else None
        self.ui_color_var = tk.StringVar(value="w") if tk._default_root else None
        self.ui_turn_var = tk.StringVar(value="w") if tk._default_root else None
        self.ui_mode_var = tk.StringVar(value=self.mode.upper()) if tk._default_root else None
        self.ui_fen_var = tk.StringVar(value="-") if tk._default_root else None
        self.ui_move_var = tk.StringVar(value="-") if tk._default_root else None
        self.ui_legal_var = tk.StringVar(value="-") if tk._default_root else None

        self.set_difficulty_mode(self.mode)
        self._open_engine()

    def _open_engine(self):
        """Открывает Stockfish один раз."""
        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):
        """Закрывает Stockfish безопасно."""
        try:
            if getattr(self, "engine", None) is not None:
                self.engine.quit()
        except Exception:
            pass
        self.engine = None

    def _set_status(self, text):
        self.last_status = text
        if self.ui_ready and self.root is not None:
            self.root.after(0, lambda: self.ui_status_var.set(text))
        else:
            print(text)

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

    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
            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")

        # Если engine уже запущен — обновим настройки
        try:
            if getattr(self, "engine", None) is not None:
                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()}")

    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):
        """Сбрасывает права на рокировку к состоянию по умолчанию"""
        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):
        """Формирует 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:
            self._set_status(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:
                self._set_status("[AUTO-MOVE]: Авто-ход включён. Анализируем позицию...")
                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):
        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 _sync_ui_state(self):
        self._set_ui("ui_color_var", self.my_color)
        self._set_ui("ui_turn_var", self.active_color)
        self._set_ui("ui_fen_var", self.last_fen if self.last_fen else "-")
        self._set_ui("ui_move_var", self.last_move_uci if self.last_move_uci else "-")
        self._set_ui("ui_legal_var", "OK" if self.illegal_counter == 0 else f"DESYNC x{self.illegal_counter}")

    def process_initial_analysis(self):
        self._set_status("[START]: Первичный анализ доски...")

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

        result = self.analyzer.get_board_state_with_color(board_img)
        if not result:
            self._set_status("[!] Ошибка: Не удалось распознать фигуры на доске.")
            return

        board_state, detected_color = result
        self.board_state = board_state
        if board_state:
            self.is_analyzed = True
            self._set_status("[OK]: Позиция успешно проанализирована.")

            self.my_color = self.detect_auto_color(self.board_state)
            self.last_detected_color = detected_color
            self.reset_castling_rights()

            if detected_color in ('w', 'b'):
                self.active_color = detected_color
            elif self.is_starting_position():
                self.active_color = 'w'
            else:
                self.active_color = self.my_color

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

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

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

            if self.engine is None:
                self._open_engine()
                if self.engine is None:
                    self._set_status("[!] Stockfish недоступен.")
                    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)

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

            if not analysis:
                self._set_status("[!] Движок не предложил ход.")
                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:
                            pass
                        else:
                            valid_candidates.append((move, cp_score, i + 1))

            capture_candidates = [c for c in valid_candidates if board.is_capture(c[0])]
            selected_pool = capture_candidates if capture_candidates else valid_candidates

            if not selected_pool:
                self._set_status("[!] Нет подходящих ходов после фильтрации.")
                return None

            if self.mode == 'max' or len(selected_pool) == 1:
                chosen_move, chosen_score, pv_num = selected_pool[0]
            else:
                chosen_move, chosen_score, pv_num = random.choice(selected_pool)

            is_castling = board.is_castling(chosen_move)
            san_move = board.san(chosen_move)
            best_move_uci = chosen_move.uci()

            self.last_move_uci = best_move_uci
            self._set_status(f"[MOVE]: {san_move} ({best_move_uci})")
            if is_castling:
                self._set_status("[INFO]: Рекомендована рокировка.")

            self._set_ui("ui_move_var", self.last_move_uci)
            self._set_ui("ui_legal_var", "OK")

            if self.mover and self.mover.auto_move_enabled:
                self.mover.execute_move(best_move_uci, self.my_color)

            return best_move_uci

        except Exception as e:
            self._set_status(f"[!] Ошибка Stockfish: {e}")
            return None

    def evaluate_current_turn(self):
        current_fen = self.get_fen_string()
        if not current_fen:
            return

        self.last_fen = current_fen
        self._set_ui("ui_fen_var", current_fen)
        self._set_ui("ui_turn_var", self.active_color)

        if self.active_color == self.my_color:
            self._set_status("Твой ход. Анализ позиции через Stockfish...")
            self.analyze_with_engine(current_fen)
        else:
            self._set_status("Сейчас ход противника. Ожидание...")