Загрузка данных
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: