Загрузка данных
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 # Глубина анализа
# Динамические права на рокировку: K=Белые O-O, Q=Белые O-O-O, k=Чёрные O-O, q=Чёрные O-O-O
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.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 # 3 пути
self.blunder_threshold = 300 # Отбрасываем ходы, где теряем > 3 пешек
print(f"\n[ENGINE CONFIG]: Режим 'EASY' | MultiPV: Top-3 | Защита от очевидных зевов (1-2 хода)")
elif self.mode == 'medium':
self.skill_level = 10
self.multipv_count = 2 # 2 пути
self.blunder_threshold = 200 # Отбрасываем ходы, где теряем > 2 пешек
print(f"\n[ENGINE CONFIG]: Режим 'MEDIUM' | MultiPV: Top-2 | Защита от зевов (1-3 хода)")
elif self.mode == 'hard':
self.skill_level = 16
self.multipv_count = 2 # 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 # Только 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 get_castling_rights(self):
"""Формирует FEN-строку рокировок ('KQkq', 'Kg', '-' и т.д.)"""
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):
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 self.is_starting_position():
self.active_color = 'w'
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
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}")
# Инициализация прав на рокировку
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:
return None
try:
board = chess.Board(fen_string)
if not board.is_valid():
print("[! WARN] Позиция на доске невалидна, пропускаем вызов движка...")
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(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)}) отброшен (потеря ~{score_diff/100:.1f} пешек)")
else:
valid_candidates.append((move, cp_score, i + 1))
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:
chosen_tuple = random.choice(valid_candidates)
chosen_move, chosen_score, pv_num = chosen_tuple
choice_info = f"Случайный из {len(valid_candidates)} безопасных кандидатов (Выбран PV{pv_num})"
is_castling = board.is_castling(chosen_move)
san_move = 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_fen = self.get_fen_string()
if not current_fen:
return
print(f"\nТекущий FEN: {current_fen}")
if self.active_color == self.my_color:
print(" Твой ход! Анализируем позицию через Stockfish...")
self.analyze_with_engine(current_fen)
else:
print(" Сейчас ход противника. Ожидаем ответа на доске...\n")
def print_board(self):
if not self.board_state:
return
print("\n a b c d e f g h")
print(" +-----------------+")
for i, row in enumerate(self.board_state):
rank = 8 - i
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
if raw_board_state != self.board_state:
is_legal, detected_move = self.is_legal_board_transition(raw_board_state)
if is_legal:
print(f"[CHANGE DETECTED]: Зафиксирован легальный ход -> {detected_move.uci()}")
self.update_castling_rights(detected_move) # Обновляем права на рокировку
self.board_state = raw_board_state
self.active_color = 'b' if self.active_color == 'w' else 'w'
self.print_board()
self.evaluate_current_turn()
else:
print("[FRAME SKIPPED]: Изменение не по правилам шахмат (фигура перекрыта / помеха). Ждём следующий кадр...")
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Программа завершена.")
if __name__ == '__main__':
logic = ChessLogic(engine_path="stockfish.exe", mode="medium")
logic.run()