Загрузка данных
import tkinter as tk
from tkinter import ttk
import threading
import time
# --- ИМПОРТЫ МОДУЛЕЙ БОТА (с безопасным фоллбэком, если модули еще не подключены) ---
try:
import crop
import analysis
import logic
import move
MODULES_AVAILABLE = True
except ImportError:
MODULES_AVAILABLE = False
print("[GUI Warning] Один или несколько модулей (crop, analysis, logic, move) не найдены. Работает в демо-режиме GUI.")
class GradientFrame(tk.Canvas):
"""Кастомный фрейм с вертикальным градиентом"""
def __init__(self, parent, color1="#2E3239", color2="#1C1E22", **kwargs):
super().__init__(parent, highlightthickness=0, **kwargs)
self.color1 = color1
self.color2 = color2
self.bind('<Configure>', self._draw_gradient)
def _draw_gradient(self, event=None):
self.delete("gradient")
width = self.winfo_width()
height = self.winfo_height()
if width <= 1 or height <= 1:
return
r1, g1, b1 = self.winfo_rgb(self.color1)
r2, g2, b2 = self.winfo_rgb(self.color2)
r_ratio = (r2 - r1) / height
g_ratio = (g2 - g1) / height
b_ratio = (b2 - b1) / height
# Рисуем градиент горизонтальными линиями
for i in range(height):
nr = int((r1 + (r_ratio * i)) / 256)
ng = int((g1 + (g_ratio * i)) / 256)
nb = int((b1 + (b_ratio * i)) / 256)
color = f"#{nr:02x}{ng:02x}{nb:02x}"
self.create_line(0, i, width, i, fill=color, tags="gradient")
self.tag_lower("gradient")
class GradientButton(tk.Canvas):
"""Кастомная кнопка с градиентом, скруглением и переключением состояний"""
def __init__(self, parent, width=120, height=42, text="Start", command=None, **kwargs):
super().__init__(parent, width=width, height=height, highlightthickness=0, bg="#2a2d32", **kwargs)
self.btn_width = width
self.btn_height = height
self.text_str = text
self.command = command
self.is_active = False # False = Синяя (Start), True = Красная (Stop/Active)
# Синий градиент (выключен)
self.blue_top = "#3B82F6"
self.blue_bottom = "#1D4ED8"
# Красный градиент (включен)
self.red_top = "#EF4444"
self.red_bottom = "#B91C1C"
self.bind("<Button-1>", self._on_click)
self.bind("<Enter>", self._on_hover)
self.bind("<Leave>", self._on_leave)
self.draw_button()
def draw_button(self, hover=False):
self.delete("all")
c_top = self.red_top if self.is_active else self.blue_top
c_bottom = self.red_bottom if self.is_active else self.blue_bottom
if hover:
# Чуть более светлый тон при наведении
c_top = "#60A5FA" if not self.is_active else "#F87171"
# Отрисовка скругленного градиентного прямоугольника через срезCanvas
radius = 10
w, h = self.btn_width, self.btn_height
# Фон с градиентной имитацией
r1, g1, b1 = self.winfo_rgb(c_top)
r2, g2, b2 = self.winfo_rgb(c_bottom)
for i in range(h):
nr = int((r1 + ((r2 - r1) / h * i)) / 256)
ng = int((g1 + ((g2 - g1) / h * i)) / 256)
nb = int((b1 + ((b2 - b1) / h * i)) / 256)
color = f"#{nr:02x}{ng:02x}{nb:02x}"
self.create_line(radius, i, w - radius, i, fill=color)
# Закругление углов (верхний и нижний дуги)
self.create_arc(0, 0, radius*2, radius*2, start=90, extent=90, fill=c_top, outline="")
self.create_arc(w-radius*2, 0, w, radius*2, start=0, extent=90, fill=c_top, outline="")
self.create_arc(0, h-radius*2, radius*2, h, start=180, extent=90, fill=c_bottom, outline="")
self.create_arc(w-radius*2, h-radius*2, w, h, start=270, extent=90, fill=c_bottom, outline="")
# Заполнение боковин
self.create_rectangle(0, radius, radius, h-radius, fill=c_top, outline="")
self.create_rectangle(w-radius, radius, w, h-radius, fill=c_top, outline="")
# Текст
lbl_text = "STOP" if self.is_active else self.text_str
self.create_text(w // 2, h // 2, text=lbl_text, fill="white", font=("Segoe UI", 11, "bold"))
def _on_hover(self, event):
self.draw_button(hover=True)
def _on_leave(self, event):
self.draw_button(hover=False)
def _on_click(self, event):
self.is_active = not self.is_active
self.draw_button()
if self.command:
self.command(self.is_active)
class ChessBotApp:
def __init__(self, root):
self.root = root
self.root.title("Chess Bot Control")
self.root.geometry("460x360")
self.root.resizable(False, False)
# Главный фоновый градиентный фрейм
self.bg_frame = GradientFrame(self.root, color1="#32363E", color2="#1A1C20")
self.bg_frame.pack(fill="both", expand=True)
# Состояния галочек и настроек
self.show_moves_var = tk.BooleanVar(value=False)
self.auto_move_var = tk.BooleanVar(value=False)
self.humanize_var = tk.BooleanVar(value=False)
self.stockfish_lvl_var = tk.IntVar(value=10)
self.bot_running = False
self._build_ui()
def _build_ui(self):
# Контейнер для верхнего блока (Кнопка слева, Галочки справа)
top_container = tk.Frame(self.bg_frame, bg="", bd=0)
top_container.place(x=20, y=20, width=420, height=180)
# СЛЕВА: Кнопка Start/Stop с отступом
self.start_btn = GradientButton(
top_container,
width=130,
height=46,
text="Start",
command=self.toggle_bot
)
self.start_btn.pack(side="left", anchor="nw", padx=(5, 10), pady=10)
# СПРАВА: Блок галочек
chk_frame = tk.Frame(top_container, bg="#25282E", bd=1, relief="solid")
chk_frame.pack(side="right", anchor="ne", padx=(0, 5), pady=5, fill="both", expand=True)
# Стилизация ttk Checkbutton
style = ttk.Style()
style.theme_use('default')
style.configure(
"Dark.TCheckbutton",
background="#25282E",
foreground="#E2E8F0",
font=("Segoe UI", 10),
focuscolor=""
)
style.map(
"Dark.TCheckbutton",
foreground=[('disabled', '#64748B'), ('active', '#FFFFFF')],
background=[('disabled', '#25282E'), ('active', '#25282E')]
)
# 1. Галочка "Показ ходов"
self.chk_show_moves = ttk.Checkbutton(
chk_frame,
text="Показ ходов",
variable=self.show_moves_var,
style="Dark.TCheckbutton",
command=self.on_show_moves_toggle
)
self.chk_show_moves.pack(anchor="w", padx=15, pady=(15, 8))
# 2. Галочка "Авто-ход"
self.chk_auto_move = ttk.Checkbutton(
chk_frame,
text="Авто-ход",
variable=self.auto_move_var,
style="Dark.TCheckbutton",
command=self.on_auto_move_toggle
)
self.chk_auto_move.pack(anchor="w", padx=15, pady=8)
# 3. Галочка "Хуманизация" (Снизу от авто-хода, изначально НЕДОСТУПНА)
self.chk_humanize = ttk.Checkbutton(
chk_frame,
text="Хуманизация",
variable=self.humanize_var,
style="Dark.TCheckbutton",
state="disabled",
command=self.on_humanize_toggle
)
self.chk_humanize.pack(anchor="w", padx=30, pady=(2, 12)) # Отступ чуть правее для визуальной иерархии
# СНИЗУ: Блок слайдера Stockfish
slider_container = tk.Frame(self.bg_frame, bg="#25282E", bd=1, relief="solid")
slider_container.place(x=20, y=210, width=420, height=130)
# Заголовок слайдера
self.lbl_slider_title = tk.Label(
slider_container,
text="Уровень Stockfish: 10",
bg="#25282E",
fg="#F1F5F9",
font=("Segoe UI", 10, "bold")
)
self.lbl_slider_title.pack(anchor="w", padx=15, pady=(10, 2))
# Сам ползунок (1-20)
self.slider = tk.Scale(
slider_container,
from_=1,
to=20,
orient="horizontal",
variable=self.stockfish_lvl_var,
bg="#25282E",
fg="#E2E8F0",
highlightthickness=0,
troughcolor="#1E293B",
activebackground="#EF4444",
sliderrelief="flat",
showvalue=False,
command=self.on_slider_move
)
self.slider.pack(fill="x", padx=15, pady=(0, 0))
# Кастомная шкала со сносками и красными отметками
self.ticks_canvas = tk.Canvas(slider_container, height=55, bg="#25282E", highlightthickness=0)
self.ticks_canvas.pack(fill="x", padx=15)
self.ticks_canvas.bind("<Configure>", self._draw_slider_ticks)
def _draw_slider_ticks(self, event=None):
canvas = self.ticks_canvas
canvas.delete("all")
w = canvas.winfo_width()
if w <= 1:
w = 390 # Дефолтная ширина фрейма за вычетом padx
# Позиции от 1 до 20
padding = 10
usable_w = w - 2 * padding
# Определение 4 основных зон (Easy, Medium, Hard, Max)
# Уровень 1..5 -> Easy, 6..10 -> Medium, 11..15 -> Hard, 16..20 -> Max
sections = [
(1, 5, "EASY", 3),
(6, 10, "MEDIUM", 8),
(11, 15, "HARD", 13),
(16, 20, "MAX", 18)
]
# Рисуем деления 1-20
for lvl in range(1, 21):
x = padding + (lvl - 1) * (usable_w / 19.0)
# Проверяем, является ли уровень ключевым красным риской (1, 5, 10, 15, 20)
if lvl in [1, 5, 10, 15, 20]:
# Большая красная сноска
canvas.create_line(x, 0, x, 14, fill="#EF4444", width=2)
canvas.create_text(x, 22, text=str(lvl), fill="#EF4444", font=("Segoe UI", 8, "bold"))
else:
# Маленькая обычная риска
canvas.create_line(x, 0, x, 8, fill="#64748B", width=1)
# Добавляем красные подписи подразделов (Easy, Medium, Hard, Max)
for start_lvl, end_lvl, label, mid_lvl in sections:
x_mid = padding + (mid_lvl - 1) * (usable_w / 19.0)
canvas.create_text(
x_mid, 40,
text=label,
fill="#F87171",
font=("Segoe UI", 9, "bold")
)
def on_slider_move(self, val):
lvl = int(float(val))
self.lbl_slider_title.config(text=f"Уровень Stockfish: {lvl}")
def on_show_moves_toggle(self):
state = self.show_moves_var.get()
print(f"[GUI] Показ ходов: {state}")
def on_auto_move_toggle(self):
is_auto = self.auto_move_var.get()
print(f"[GUI] Авто-ход: {is_auto}")
# Разблокировка/Блокировка галочки Хуманизация
if is_auto:
self.chk_humanize.config(state="normal")
else:
self.chk_humanize.config(state="disabled")
self.humanize_var.set(False) # Автоматически снимаем галочку, если авто-ход выключен
def on_humanize_toggle(self):
state = self.humanize_var.get()
print(f"[GUI] Хуманизация авто-хода: {state}")
def toggle_bot(self, active):
self.bot_running = active
if active:
print("[GUI] >>> БОТ ЗАПУЩЕН! Поиск доски и распознавание фигур (crop.py & analysis.py)...")
# Запуск логики в отдельном потоке, чтобы GUI не зависал
threading.Thread(target=self._run_bot_logic, daemon=True).start()
else:
print("[GUI] <<< БОТ ОСТАНОВЛЕН.")
def _run_bot_logic(self):
"""Фоновый поток работы бота"""
while self.bot_running:
# Здесь будет обращение к crop.py, analysis.py, logic.py и move.py
if MODULES_AVAILABLE:
# Пример вызова функций при подключении:
# board_img = crop.get_board_image()
pass
time.sleep(0.5)
if __name__ == "__main__":
root = tk.Tk()
app = ChessBotApp(root)
root.mainloop()