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


import time
import random
import keyboard
import chess
import chess.engine
import os
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          # Уровень навыка Stockfish (0..20)
        self.multipv_count = 2         # Количество путей
        self.blunder_threshold = 200   # Порог отбраковки зевов (в сантипешках)
        self.fixed_depth = 12          # Глубина анализа

        # Флаги и состояние
        self.trigger_analysis = False
        self.trigger_toggle_color = False
        self.is_analyzed = False

        # ИСТОРИЯ ПАРТИИ (Строго по стандарту python-chess)
        self.board = chess.Board()

        self.my_color = 'w'
        self.set_difficulty_mode(self.mode)

    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 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):
        """Конвертирует текущую шахматную доску в 8x8 матрицу под глаза игрока"""
        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)

        if self.my_color == 'w':
            return matrix
        return [row[::-1] for row in matrix[::-1]]

    def board_matrix_to_fen(self, raw_board_state, active_color='w'):
        """Преобразует снимок с экрана в валидный FEN"""
        oriented = self.get_oriented_matrix_for(raw_board_state)

        fen_rows = []
        for row in oriented:
            empty = 0
            row_str = ""
            for cell in row:
                if cell == "":
                    empty += 1
                else:
                    if empty > 0:
                        row_str += str(empty)
                        empty = 0
                    row_str += cell
            if empty > 0:
                row_str += str(empty)
            fen_rows.append(row_str)

        position_fen = "/".join(fen_rows)

        castling = ""
        if oriented[7][4] == 'K' and oriented[7][7] == 'R': castling += 'K'
        if oriented[7][4] == 'K' and oriented[7][0] == 'R': castling += 'Q'
        if oriented[0][4] == 'k' and oriented[0][7] == 'r': castling += 'k'
        if oriented[0][4] == 'k' and oriented[0][0] == 'r': castling += 'q'
        if not castling:
            castling = "-"

        return f"{position_fen} {active_color} {castling} - 0 1"

    def find_legal_move_for_screen(self, screen_matrix):
        """
        Перебирает ВСЕ легальные ходы из текущего состояния history (self.board)
        и ищет тот, чей результат в точности совпадет с тем, что нарисовано на экране.
        """
        oriented_screen = self.get_oriented_matrix_for(screen_matrix)

        for move in self.board.legal_moves:
            test_board = self.board.copy()
            test_board.push(move)
            test_matrix = self.board_to_oriented_matrix(test_board)

            if test_matrix == oriented_screen:
                return move

        return None

    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

        raw_board_state = self.analyzer.get_board_state(board_img)
        if raw_board_state:
            self.is_analyzed = True

            self.my_color = self.detect_auto_color(raw_board_state)
            color_str = "БЕЛЫЕ (w)" if self.my_color == 'w' else "ЧЁРНЫЕ (b) [Доска развернута]"
            print(f"[AUTO-COLOR]: Определён цвет фигур внизу -> {color_str}")

            active_turn = 'w'
            fen_str = self.board_matrix_to_fen(raw_board_state, active_turn)

            try:
                self.board = chess.Board(fen_str)
                print(f"[√] История успешно инициализирована с экрана! FEN: {self.board.fen()}")
            except Exception as e:
                print(f"[!] Ошибка FEN ({e}), загружаем стандартный старт...")
                self.board = chess.Board()

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

    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) [Доска развернута]"
        print(f"\n[MANUAL COLOR SET]: Цвет вручную изменён -> {color_name}")

        if self.is_analyzed:
            self.evaluate_current_turn()

    def toggle_auto_move_and_execute(self):
        self.mover.toggle_auto_move()
        if self.mover.auto_move_enabled:
            current_turn = 'w' if self.board.turn == chess.WHITE else 'b'
            if self.is_analyzed and current_turn == self.my_color:
                print("[AUTO-MOVE TRIGGER]: Авто-ход включён! Запрашиваем и выполняем лучший ход...")
                self.evaluate_current_turn()

    def analyze_with_engine(self, time_limit=0.5):
        try:
            if not self.board.is_valid():
                print("[! WARN] Позиция невалидна для Stockfish...")
                return None

            with chess.engine.SimpleEngine.popen_uci(self.engine_path) as engine:
                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 = engine.analyse(self.board, limit, multipv=pv_count)

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

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

                # 1. Отбраковка зевов
                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(self.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} ({self.board.san(move)}) отброшен (потеря ~{score_diff/100:.1f} пешек)")
                            else:
                                valid_candidates.append((move, cp_score, i + 1))

                # 2. ПРИОРИТЕТ ВЗЯТИЯ: если среди безопасных ходов есть взятие фигуры, выбираем его
                capture_candidates = [c for c in valid_candidates if self.board.is_capture(c[0])]

                if capture_candidates:
                    print(f" [CAPTURE DETECTED] Найден безопасный ход со взятием фигуры! Отбрасываем тихие ходы.")
                    selected_pool = capture_candidates
                else:
                    selected_pool = valid_candidates

                # 3. Выбор итогового хода
                if self.mode == 'max' or len(selected_pool) == 1:
                    chosen_move, chosen_score, pv_num = selected_pool[0]
                    choice_info = f"PV{pv_num} (Лучший ход)"
                else:
                    chosen_tuple = random.choice(selected_pool)
                    chosen_move, chosen_score, pv_num = chosen_tuple
                    choice_info = f"Случайный из {len(selected_pool)} безопасных (Выбран PV{pv_num})"

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

                print(f" [ВАШ ХОД]: {san_move} (UCI: {best_move_uci}) | {choice_info}")
                if is_castling:
                    print(" [INFO] Рекомендуемый ход - РОКИРОВКА!")

                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:
            print(f"[!] Ошибка Stockfish: {e}")
            return None

    def evaluate_current_turn(self):
        current_turn = 'w' if self.board.turn == chess.WHITE else 'b'
        print(f"\nТекущий FEN из истории: {self.board.fen()}")

        if current_turn == self.my_color:
            print(" Твой ход! Анализируем позицию через Stockfish...")
            self.analyze_with_engine()
        else:
            print(" Сейчас ход противника. Ожидаем ответа на доске...\n")

    def print_board(self):
        oriented_matrix = self.board_to_oriented_matrix(self.board)
        print("\n  a b c d e f g h")
        print(" +-----------------+")
        for i, row in enumerate(oriented_matrix):
            rank = 8 - i if self.my_color == 'w' else i + 1
            row_str = " | ".join([p if p != "" else "." for p in row])
            print(f"{rank} | {row_str} | {rank}")
        print(" +-----------------+")
        print("  a b c d e f g h\n")

    def track_board_changes(self):
        if hasattr(self, 'mover') and self.mover.is_moving:
            return

        board_img = crop.get_board_image() if hasattr(crop, 'get_board_image') else crop.find_chessboard_strictly_1to1()[1]
        if board_img is None:
            return

        raw_board_state = self.analyzer.get_board_state(board_img)
        if not raw_board_state:
            return

        # 1. Быстрая проверка: если доска в точности совпадает с нашей историей — ничего не делаем
        current_board_matrix = self.board_to_oriented_matrix(self.board)
        oriented_screen = self.get_oriented_matrix_for(raw_board_state)

        if current_board_matrix == oriented_screen:
            time.sleep(0.3)
            return

        # 2. Картинка изменилась! Проверяем легальность хода противника или нашего хода
        legal_move = self.find_legal_move_for_screen(raw_board_state)

        if legal_move:
            print(f"\n[LEGAL MOVE CONFIRMED]: Ход записан в историю -> {self.board.san(legal_move)} ({legal_move.uci()})")
            self.board.push(legal_move)
            self.print_board()
            self.evaluate_current_turn()
        else:
            # Если доска изменилась, но ни один легальный ход не подошёл (например, анимация не завершена), 
            # мы просто молча пропускаем этот кадр, не ломая игру.
            pass

        time.sleep(0.3)

    def run(self):
        print("==================================================")
        print(" Шахматный бот запущен.")
        print(" Нажмите 'S' - Старт анализа.")
        print(" Нажмите 'C' - Ручная смена цвета (Белые/Чёрные).")
        print(" Нажмите 'M' - Смена режима сложности (Easy/Medium/Hard/Max).")
        print(" Нажмите 'L' - Вкл/Выкл физического авто-хода.")
        print(" Нажмите 'H' - Режим мыши (Humanly / Click).")
        print(" Нажмите '+' / '-' - Регулировка скорости мыши.")
        print("==================================================")

        keyboard.add_hotkey('s', self.on_start_pressed)
        keyboard.add_hotkey('c', self.on_color_toggle_pressed)
        keyboard.add_hotkey('m', self.cycle_difficulty_mode)
        
        keyboard.add_hotkey('l', self.toggle_auto_move_and_execute)
        keyboard.add_hotkey('h', self.mover.toggle_humanly_mode)
        keyboard.add_hotkey('+', self.mover.speed_up)
        keyboard.add_hotkey('-', self.mover.speed_down)

        try:
            while True:
                if self.trigger_analysis:
                    self.trigger_analysis = False
                    self.process_initial_analysis()

                if self.trigger_toggle_color:
                    self.trigger_toggle_color = False
                    self.toggle_my_color()

                if self.is_analyzed:
                    self.track_board_changes()

                time.sleep(0.5)
        except KeyboardInterrupt:
            print("\nПрограмма завершена.")

    def on_start_pressed(self):
        self.trigger_analysis = True

    def on_color_toggle_pressed(self):
        self.trigger_toggle_color = True


if __name__ == '__main__':
    logic = ChessLogic(engine_path="stockfish.exe", mode="medium")
    logic.run()
import time
import chess
import chess.engine
from analysis import ChessAnalyzer
import crop

# Попытка импорта модуля авто-хода
try:
    import move
    MOVE_AVAILABLE = True
except ImportError:
    MOVE_AVAILABLE = False


class ChessLogic:
    def __init__(self, engine_path="stockfish.exe", gui_callback=None):
        self.analyzer = ChessAnalyzer()
        self.engine_path = engine_path
        self.gui_callback = gui_callback

        self.board_state = None
        self.my_color = 'w'
        self.active_color = 'w'
        self.en_passant_target = None
        self.is_analyzed = False

    def _get_board_img(self):
        """Безопасный забор снимка доски"""
        try:
            if hasattr(crop, 'get_board_image'):
                return crop.get_board_image()
            res = crop.find_chessboard_strictly_1to1()
            if isinstance(res, (tuple, list)) and len(res) > 1:
                return res[1]
            return res
        except Exception as e:
            print(f"[Logic Error] Ошибка захвата кадра: {e}")
            return None

    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_castling_rights(self):
        if not self.board_state or len(self.board_state) != 8:
            return "KQkq"
            
        rights = ""
        if self.board_state[7][4] == 'K':
            if self.board_state[7][7] == 'R': rights += 'K'
            if self.board_state[7][0] == 'R': rights += 'Q'

        if self.board_state[0][4] == 'k':
            if self.board_state[0][7] == 'r': rights += 'k'
            if self.board_state[0][0] == 'r': rights += 'q'

        return rights if rights else "-"

    def find_king_position(self, board_matrix, king_symbol):
        for r in range(8):
            for c in range(8):
                if board_matrix[r][c] == king_symbol:
                    return r, c
        return None

    def restore_missing_kings(self, new_board_state):
        if not self.board_state:
            return new_board_state

        restored_state = [row[:] for row in new_board_state]
        for king_symbol in ['K', 'k']:
            new_pos = self.find_king_position(restored_state, king_symbol)
            if new_pos is None:
                old_pos = self.find_king_position(self.board_state, king_symbol)
                if old_pos:
                    r, c = old_pos
                    restored_state[r][c] = king_symbol
        return restored_state

    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_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 == ' ' or 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 process_initial_analysis(self, skill_level=10, auto_move=False):
        """Первичный анализ доски с учётом актуальных настроек GUI"""
        board_img = self._get_board_img()
        if board_img is None:
            return False

        self.board_state = self.analyzer.get_board_state(board_img)
        if self.board_state:
            self.is_analyzed = True
            self.active_color = self.my_color
            print("[Logic] Доска успешно распознана!")
            self.evaluate_current_turn(skill_level=skill_level, auto_move=auto_move)
            return True
        return False

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

        try:
            board = chess.Board(fen_string)
            if not board.is_valid():
                return None

            with chess.engine.SimpleEngine.popen_uci(self.engine_path) as engine:
                try:
                    engine.configure({"Skill Level": skill_level})
                except Exception:
                    pass

                result = engine.play(board, chess.engine.Limit(time=time_limit))
                best_move = result.move
                return best_move.uci(), board.san(best_move)
        except Exception as e:
            print(f"[Logic Error] Ошибка Stockfish (проверьте путь k stockfish.exe): {e}")
            return None

    def evaluate_current_turn(self, skill_level=10, auto_move=False):
        current_fen = self.get_fen_string()
        if not current_fen:
            return

        # Отправляем FEN в GUI всегда
        if self.gui_callback:
            self.gui_callback(fen=current_fen)

        if self.active_color == self.my_color:
            res = self.analyze_with_engine(current_fen, skill_level=skill_level)
            if res:
                uci_move, san_move = res
                print(f"[Logic] Рекомендованный ход: {san_move} ({uci_move})")
                
                if self.gui_callback:
                    self.gui_callback(best_move=uci_move, san_move=san_move)

                if auto_move and MOVE_AVAILABLE:
                    print(f"[Logic] Выполняем авто-ход: {uci_move}")
                    move.make_move(uci_move, self.my_color)

    def track_board_changes(self, skill_level=10, auto_move=False, force_eval=False):
        board_img = self._get_board_img()
        if board_img is None:
            return

        raw_board_state = self.analyzer.get_board_state(board_img)
        if not raw_board_state:
            return

        new_board_state = self.restore_missing_kings(raw_board_state)

        # Если фигура сдвинулась или принудительно пересчитываем
        if new_board_state != self.board_state or force_eval:
            if new_board_state != self.board_state:
                self.board_state = new_board_state
                self.active_color = 'b' if self.active_color == 'w' else 'w'
            
            self.evaluate_current_turn(skill_level=skill_level, auto_move=auto_move)