Загрузка данных
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("Сейчас ход противника. Ожидание...")