Загрузка данных
import os
import random
import time
import tkinter as tk
from tkinter import ttk
import chess
import chess.engine
import keyboard
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() if mode else "medium"
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.halfmove_clock = 0
self.fullmove_number = 1
# Восстановление после шумных кадров
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 = None
self.ui_color_var = None
self.ui_turn_var = None
self.ui_mode_var = None
self.ui_fen_var = None
self.ui_move_var = None
self.ui_legal_var = None
self.engine = None
self.set_difficulty_mode(self.mode)
self._open_engine()
# -----------------------------
# БАЗОВЫЕ ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ
# -----------------------------
def _set_status(self, text):
self.last_status = text
if self.ui_ready and self.root is not None and self.ui_status_var is not None:
try:
self.root.after(0, lambda: self.ui_status_var.set(text))
except Exception:
pass
else:
print(text)
def _set_ui(self, var_name, value):
if not self.ui_ready or self.root is None:
return
var = getattr(self, var_name, None)
if var is None:
return
try:
self.root.after(0, lambda v=var, val=value: v.set(val))
except Exception:
pass
def _square_to_coords(self, square_index):
row = 7 - chess.square_rank(square_index)
col = chess.square_file(square_index)
return row, col
def _count_differences(self, a, b):
if not a or not b:
return 64
diff = 0
for r in range(8):
for c in range(8):
if a[r][c] != b[r][c]:
diff += 1
return diff
def _build_fen_from_state(self, board_state, active_color=None, castling=None, ep="-", halfmove=0, fullmove=1):
if not board_state:
return ""
fen_rows = []
for row in board_state:
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)
if active_color is None:
active_color = self.active_color
if castling is None:
castling = self.get_castling_rights()
return f"{position_fen} {active_color} {castling} {ep} {halfmove} {fullmove}"
def _board_signature(self, board_state):
if not board_state:
return None
return tuple(tuple(row) for row in board_state)
def _normalize_image_input(self, image_input):
if image_input is None:
return None, {}
if isinstance(image_input, dict):
return image_input.get("image"), image_input
if isinstance(image_input, str):
if not os.path.exists(image_input):
print(f"[!] Файл '{image_input}' не найден!")
return None, {}
return None if crop is None else __import__("cv2").imread(image_input), {}
return image_input, {}
def _get_board_image(self):
if hasattr(crop, "get_board_image"):
return crop.get_board_image()
if hasattr(crop, "find_chessboard_strictly_1to1"):
return crop.find_chessboard_strictly_1to1()[1]
return None
# -----------------------------
# STOCKFISH
# -----------------------------
def _open_engine(self):
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):
try:
if self.engine is not None:
self.engine.quit()
except Exception:
pass
self.engine = None
# -----------------------------
# РЕЖИМЫ СЛОЖНОСТИ
# -----------------------------
def set_difficulty_mode(self, mode):
self.mode = mode.lower() if mode else "medium"
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")
else:
self.mode = "medium"
self.skill_level = 10
self.multipv_count = 2
self.blunder_threshold = 200
self._set_status("[ENGINE CONFIG]: MEDIUM")
if self.engine is not None:
try:
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()}")
# -----------------------------
# РАБОТА С ДОСКОЙ / FEN
# -----------------------------
def detect_auto_color(self, board_matrix):
"""Определяет цвет фигур на нижней части экрана."""
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
return "b" if black_count > white_count else "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):
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 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} {self.halfmove_clock} {self.fullmove_number}"
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 _count_legal_diff(self, old_state, new_state):
if not old_state or not new_state:
return 64
diff = 0
for r in range(8):
for c in range(8):
if old_state[r][c] != new_state[r][c]:
diff += 1
return diff
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)
# Быстрая отсечка шумных кадров
diff_cells = self._count_legal_diff(self.get_oriented_board_state(), new_oriented)
if diff_cells > 8:
return False, None
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"[!] Ошибка валидации: {e}")
return False, None
def _accept_transition(self, new_board_state, move_obj, source="legal", detected_color=None):
self.board_state = new_board_state
self.update_castling_rights(move_obj)
try:
fen_before = self.get_fen_string()
board = chess.Board(fen_before)
if move_obj is not None:
board.push(move_obj)
self.active_color = "w" if board.turn == chess.WHITE else "b"
self.halfmove_clock = board.halfmove_clock
self.fullmove_number = board.fullmove_number
self.en_passant_target = None if board.ep_square is None else {
"target_pos": self._square_to_coords(board.ep_square)
}
except Exception:
if detected_color in ("w", "b"):
self.active_color = detected_color
self.en_passant_target = None
self.pending_board_state = None
self.pending_counter = 0
self.illegal_counter = 0
self.last_move_uci = move_obj.uci() if move_obj else "-"
self.last_fen = self.get_fen_string()
self._set_status(f"[OK]: Позиция обновлена ({source}).")
self._sync_ui_state()
def _resync_with_candidate(self, new_board_state, detected_color=None):
"""Пытается восстановить синхронизацию после серии плохих кадров."""
try:
active_color = detected_color if detected_color in ("w", "b") else self.active_color
fen = self._build_fen_from_state(
new_board_state,
active_color=active_color,
castling=self.get_castling_rights(),
ep="-",
halfmove=self.halfmove_clock,
fullmove=self.fullmove_number,
)
board = chess.Board(fen)
if board.is_valid():
self.board_state = new_board_state
self.active_color = active_color
self.en_passant_target = None
self.pending_board_state = None
self.pending_counter = 0
self.illegal_counter = 0
self.last_fen = fen
self._set_status("[RESYNC]: Состояние восстановлено по новому кадру.")
self._sync_ui_state()
return True
except Exception:
pass
return False
# -----------------------------
# GUI / HOTKEYS
# -----------------------------
def on_start_pressed(self):
self.trigger_analysis = True
def on_color_toggle_pressed(self):
self.trigger_toggle_color = True
def toggle_auto_move_and_execute(self):
self.mover.toggle_auto_move()
if self.mover.auto_move_enabled and self.is_analyzed and self.active_color == self.my_color:
self._set_status("[AUTO-MOVE]: Авто-ход включён. Анализируем позицию...")
self.evaluate_current_turn()
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 _build_gui(self):
self.root = tk.Tk()
self.root.title("Chess Bot")
self.root.geometry("700x470")
self.root.resizable(False, False)
self.root.protocol("WM_DELETE_WINDOW", self.shutdown)
main = ttk.Frame(self.root, padding=12)
main.pack(fill="both", expand=True)
self.ui_status_var = tk.StringVar(value=self.last_status)
self.ui_color_var = tk.StringVar(value=self.my_color)
self.ui_turn_var = tk.StringVar(value=self.active_color)
self.ui_mode_var = tk.StringVar(value=self.mode.upper())
self.ui_fen_var = tk.StringVar(value=self.last_fen if self.last_fen else "-")
self.ui_move_var = tk.StringVar(value=self.last_move_uci if self.last_move_uci else "-")
self.ui_legal_var = tk.StringVar(value="OK")
ttk.Label(main, text="Chess Bot", font=("Segoe UI", 16, "bold")).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 10))
ttk.Label(main, text="Статус:").grid(row=1, column=0, sticky="w")
ttk.Label(main, textvariable=self.ui_status_var, wraplength=560).grid(row=1, column=1, sticky="w")
ttk.Label(main, text="Ваш цвет:").grid(row=2, column=0, sticky="w")
ttk.Label(main, textvariable=self.ui_color_var).grid(row=2, column=1, sticky="w")
ttk.Label(main, text="Чей ход:").grid(row=3, column=0, sticky="w")
ttk.Label(main, textvariable=self.ui_turn_var).grid(row=3, column=1, sticky="w")
ttk.Label(main, text="Режим:").grid(row=4, column=0, sticky="w")
ttk.Label(main, textvariable=self.ui_mode_var).grid(row=4, column=1, sticky="w")
ttk.Label(main, text="Последний ход:").grid(row=5, column=0, sticky="w")
ttk.Label(main, textvariable=self.ui_move_var).grid(row=5, column=1, sticky="w")
ttk.Label(main, text="Легальность:").grid(row=6, column=0, sticky="w")
ttk.Label(main, textvariable=self.ui_legal_var).grid(row=6, column=1, sticky="w")
ttk.Label(main, text="FEN:").grid(row=7, column=0, sticky="nw", pady=(10, 0))
ttk.Label(main, textvariable=self.ui_fen_var, wraplength=560, justify="left").grid(row=7, column=1, sticky="w", pady=(10, 0))
btns = ttk.Frame(main)
btns.grid(row=8, column=0, columnspan=2, sticky="w", pady=(18, 0))
ttk.Button(btns, text="Старт (S)", command=self.on_start_pressed).grid(row=0, column=0, padx=(0, 6))
ttk.Button(btns, text="Цвет (C)", command=self.on_color_toggle_pressed).grid(row=0, column=1, padx=(0, 6))
ttk.Button(btns, text="Режим (M)", command=self.cycle_difficulty_mode).grid(row=0, column=2, padx=(0, 6))
ttk.Button(btns, text="Автоход (L)", command=self.toggle_auto_move_and_execute).grid(row=0, column=3, padx=(0, 6))
ttk.Button(btns, text="Humanly (H)", command=self.mover.toggle_humanly_mode).grid(row=0, colu