Загрузка данных


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
STALCRAFT Auction Bot - Полностью рабочий прототип
Версия: 1.0.0
"""

import sys
import os
import json
import time
import threading
import logging
from datetime import datetime
from typing import Optional, Dict, List, Tuple
from dataclasses import dataclass
from pathlib import Path

# Установка зависимостей, если их нет
try:
    import tkinter as tk
    from tkinter import ttk, messagebox, filedialog
    import pyautogui
    import pytesseract
    from PIL import ImageGrab, Image, ImageEnhance, ImageFilter
    import cv2
    import numpy as np
except ImportError as e:
    print(f"Ошибка импорта: {e}")
    print("Устанавливаю зависимости...")
    import subprocess
    subprocess.check_call([sys.executable, "-m", "pip", "install", "pytesseract", "opencv-python", "Pillow", "pyautogui"])
    print("Зависимости установлены. Перезапустите программу.")
    sys.exit(1)

# Настройка логирования
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('stalcraft_bot.log', encoding='utf-8'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# ============= КЛАСС НАСТРОЕК =============
@dataclass
class Config:
    """Класс для хранения всех настроек"""
    # Основные настройки
    search_text: str = ""
    max_price: int = 100000
    pages_count: int = 3
    use_pages: bool = True
    
    # Координаты
    coordinates: Dict = None
    
    # OCR области
    ocr_areas: Dict = None
    
    # OCR настройки
    ocr_settings: Dict = None
    
    # Режимы
    auto_buy: bool = False
    test_mode: bool = True
    
    def __post_init__(self):
        if self.coordinates is None:
            self.coordinates = {
                'input_field': {'x': 0, 'y': 0},
                'refresh_button': {'x': 0, 'y': 0},
                'slider': {'x': 0, 'y': 0},
                'ok_button': {'x': 0, 'y': 0}
            }
        if self.ocr_areas is None:
            self.ocr_areas = {
                'prices': {'x1': 0, 'y1': 0, 'x2': 0, 'y2': 0},
                'buy_button': {'x1': 0, 'y1': 0, 'x2': 0, 'y2': 0},
                'pages': {'x1': 0, 'y1': 0, 'x2': 0, 'y2': 0}
            }
        if self.ocr_settings is None:
            self.ocr_settings = {
                'lang': 'rus',
                'scale': 200,
                'psm': 6,
                'attempts': 3,
                'confidence_threshold': 70,
                'preprocessing': {
                    'increase': True,
                    'bw': True,
                    'contrast': True,
                    'binarization': False,
                    'denoise': True,
                    'sharpness': False
                }
            }
    
    def save(self, filename: str = "config.json"):
        """Сохранение настроек в файл"""
        try:
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(self.__dict__, f, indent=4, ensure_ascii=False)
            logger.info(f"Настройки сохранены в {filename}")
        except Exception as e:
            logger.error(f"Ошибка сохранения настроек: {e}")
    
    def load(self, filename: str = "config.json"):
        """Загрузка настроек из файла"""
        try:
            if os.path.exists(filename):
                with open(filename, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    for key, value in data.items():
                        if hasattr(self, key):
                            setattr(self, key, value)
                logger.info(f"Настройки загружены из {filename}")
                return True
        except Exception as e:
            logger.error(f"Ошибка загрузки настроек: {e}")
        return False

# ============= КЛАСС OCR ОБРАБОТЧИКА =============
class OCRProcessor:
    """Обработка OCR с настраиваемыми параметрами"""
    
    def __init__(self, settings: Dict):
        self.settings = settings
        self.last_results = []
        
        # Проверка наличия Tesseract
        try:
            pytesseract.get_tesseract_version()
        except:
            logger.error("Tesseract не найден! Установите Tesseract OCR")
            messagebox.showerror("Ошибка", "Tesseract OCR не найден!\nУстановите Tesseract с https://github.com/UB-Mannheim/tesseract/wiki")
    
    def preprocess_image(self, image: Image.Image) -> Image.Image:
        """Предобработка изображения"""
        # Конвертация в numpy array
        img = np.array(image)
        
        # Масштабирование
        if self.settings['scale'] != 100:
            scale = self.settings['scale'] / 100
            width = int(img.shape[1] * scale)
            height = int(img.shape[0] * scale)
            img = cv2.resize(img, (width, height), interpolation=cv2.INTER_CUBIC)
        
        # Чёрно-белое
        if self.settings['preprocessing']['bw'] and len(img.shape) == 3:
            img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        
        # Повышение контрастности
        if self.settings['preprocessing']['contrast']:
            if len(img.shape) == 3:
                img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            alpha = 1.5
            beta = 30
            img = cv2.convertScaleAbs(img, alpha=alpha, beta=beta)
        
        # Бинаризация
        if self.settings['preprocessing']['binarization']:
            if len(img.shape) == 3:
                img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            _, img = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
        
        # Удаление шума
        if self.settings['preprocessing']['denoise']:
            if len(img.shape) == 3:
                img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            img = cv2.medianBlur(img, 3)
        
        # Резкость
        if self.settings['preprocessing']['sharpness']:
            if len(img.shape) == 3:
                img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            kernel = np.array([[-1,-1,-1], [-1, 9,-1], [-1,-1,-1]])
            img = cv2.filter2D(img, -1, kernel)
        
        return Image.fromarray(img)
    
    def extract_prices(self, image: Image.Image) -> List[Dict]:
        """Извлечение цен из изображения"""
        results = []
        
        for attempt in range(self.settings['attempts']):
            try:
                # Предобработка
                processed = self.preprocess_image(image)
                
                # Настройка Tesseract для цен
                custom_config = f'--oem 3 --psm {self.settings["psm"]} -c tessedit_char_whitelist=0123456789 '
                
                # Распознавание
                data = pytesseract.image_to_data(
                    processed,
                    lang=self.settings['lang'],
                    config=custom_config,
                    output_type=pytesseract.Output.DICT
                )
                
                # Парсинг результатов
                prices = self._parse_prices(data)
                if prices:
                    results.append(prices)
                    
            except Exception as e:
                logger.error(f"Ошибка OCR (попытка {attempt+1}): {e}")
                continue
        
        # Валидация результатов
        return self._validate_results(results)
    
    def _parse_prices(self, data: Dict) -> List[Dict]:
        """Парсинг цен из данных Tesseract"""
        prices = []
        n_boxes = len(data['text'])
        
        for i in range(n_boxes):
            try:
                text = data['text'][i].strip()
                conf = int(data['conf'][i])
                
                # Проверка уверенности
                if conf < self.settings['confidence_threshold']:
                    continue
                
                # Очистка текста
                clean_text = ''.join(filter(str.isdigit, text))
                if clean_text and len(clean_text) >= 4:  # Минимум 4 цифры для цены
                    price = int(clean_text)
                    if 1000 <= price <= 10000000:  # Реалистичный диапазон цен
                        prices.append({
                            'price': price,
                            'confidence': conf,
                            'bbox': (
                                data['left'][i],
                                data['top'][i],
                                data['width'][i],
                                data['height'][i]
                            ),
                            'text': text
                        })
            except (ValueError, IndexError):
                continue
        
        return prices
    
    def _validate_results(self, results: List[List[Dict]]) -> List[Dict]:
        """Валидация результатов OCR"""
        if not results:
            return []
        
        # Если только одна попытка или результаты нестабильны
        if len(results) == 1:
            return results[0]
        
        # Проверка стабильности результатов
        stable_prices = []
        for i in range(len(results[0])):
            price = results[0][i]['price']
            is_stable = all(
                r[i]['price'] == price if i < len(r) else False
                for r in results[1:]
            )
            if is_stable:
                stable_prices.append(results[0][i])
        
        return stable_prices
    
    def extract_text(self, image: Image.Image) -> Dict:
        """Извлечение текста (для кнопок)"""
        try:
            processed = self.preprocess_image(image)
            custom_config = f'--oem 3 --psm 6 -c tessedit_char_whitelist=АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя '
            
            data = pytesseract.image_to_data(
                processed,
                lang=self.settings['lang'],
                config=custom_config,
                output_type=pytesseract.Output.DICT
            )
            
            # Поиск текста
            texts = {}
            for i, text in enumerate(data['text']):
                if text.strip():
                    texts[text.strip()] = {
                        'bbox': (data['left'][i], data['top'][i], 
                                data['width'][i], data['height'][i]),
                        'conf': int(data['conf'][i])
                    }
            return texts
            
        except Exception as e:
            logger.error(f"Ошибка извлечения текста: {e}")
            return {}

# ============= КЛАСС БОТА =============
class AuctionBot:
    """Основной движок бота"""
    
    def __init__(self, config: Config):
        self.config = config
        self.ocr = OCRProcessor(config.ocr_settings)
        self.is_running = False
        self.is_paused = False
        self.current_page = 1
        self.purchases_count = 0
        self.total_spent = 0
        self.last_status = ""
        
        # Настройка pyautogui
        pyautogui.FAILSAFE = True
        pyautogui.PAUSE = 0.1
    
    def start(self):
        """Запуск бота"""
        if self.is_running:
            return
        
        # Проверка настроек
        if not self._validate_config():
            return
        
        self.is_running = True
        self.current_page = 1
        self.purchases_count = 0
        
        # Запуск в отдельном потоке
        self.bot_thread = threading.Thread(target=self._run)
        self.bot_thread.daemon = True
        self.bot_thread.start()
        
        logger.info("Бот запущен")
    
    def stop(self):
        """Остановка бота"""
        self.is_running = False
        logger.info("Бот остановлен")
    
    def _run(self):
        """Основной цикл бота"""
        while self.is_running:
            try:
                logger.info(f"Цикл бота #{(self.purchases_count + 1)}")
                
                # 1. Ввод товара
                if not self._input_search_text():
                    continue
                
                # 2. Обновление списка
                if not self._refresh_auction():
                    continue
                
                # 3. Поиск лотов на текущей странице
                if not self._search_lots_on_page():
                    # Если не найдено на текущей странице
                    continue
                
                # Небольшая задержка между циклами
                time.sleep(1)
                
            except Exception as e:
                logger.error(f"Ошибка в основном цикле: {e}")
                time.sleep(5)
    
    def _validate_config(self) -> bool:
        """Проверка настроек"""
        if not self.config.search_text:
            logger.warning("Не указан товар для поиска")
            self.last_status = "Ошибка: не указан товар"
            return False
        
        if self.config.max_price <= 0:
            logger.warning("Некорректная максимальная цена")
            self.last_status = "Ошибка: некорректная цена"
            return False
        
        # Проверка координат
        coords = self.config.coordinates
        if not all(coords[k]['x'] > 0 and coords[k]['y'] > 0 for k in coords):
            logger.warning("Не все координаты настроены")
            self.last_status = "Ошибка: не все координаты настроены"
            return False
        
        # Проверка OCR областей
        areas = self.config.ocr_areas
        for area in areas.values():
            if area['x1'] == area['x2'] or area['y1'] == area['y2']:
                logger.warning("Не все OCR области настроены")
                self.last_status = "Ошибка: не все OCR области настроены"
                return False
        
        return True
    
    def _input_search_text(self) -> bool:
        """Ввод текста поиска"""
        try:
            coords = self.config.coordinates['input_field']
            pyautogui.click(coords['x'], coords['y'])
            time.sleep(0.2)
            
            # Очистка поля
            pyautogui.hotkey('ctrl', 'a')
            time.sleep(0.1)
            pyautogui.press('delete')
            time.sleep(0.1)
            
            # Ввод текста
            pyautogui.write(self.config.search_text)
            time.sleep(0.2)
            
            logger.info(f"Введён текст: {self.config.search_text}")
            return True
            
        except Exception as e:
            logger.error(f"Ошибка ввода текста: {e}")
            return False
    
    def _refresh_auction(self) -> bool:
        """Обновление списка аукциона"""
        try:
            coords = self.config.coordinates['refresh_button']
            pyautogui.click(coords['x'], coords['y'])
            time.sleep(1.5)  # Ожидание загрузки
            
            logger.info("Список аукциона обновлён")
            return True
            
        except Exception as e:
            logger.error(f"Ошибка обновления: {e}")
            return False
    
    def _search_lots_on_page(self) -> bool:
        """Поиск лотов на текущей странице"""
        # Сначала проверяем видимую область
        if self._check_and_buy():
            return True
        
        # Прокрутка
        for step in range(80, 401, 80):
            if not self.is_running:
                return False
            
            self._scroll_down(step)
            
            if self._check_and_buy():
                return True
        
        # Если не найдено - переключение страницы
        if self.config.use_pages and self.current_page < self.config.pages_count:
            self.current_page += 1
            if self._switch_page(self.current_page):
                return self._search_lots_on_page()
        
        return False
    
    def _check_and_buy(self) -> bool:
        """Проверка лотов и покупка"""
        # Получение скриншота области цен
        area = self.config.ocr_areas['prices']
        screenshot = ImageGrab.grab(bbox=(
            area['x1'], area['y1'],
            area['x2'], area['y2']
        ))
        
        # Распознавание цен
        prices = self.ocr.extract_prices(screenshot)
        
        if not prices:
            return False
        
        # Поиск минимальной подходящей цены
        max_price = self.config.max_price
        valid_prices = [p for p in prices if p['price'] <= max_price]
        
        if not valid_prices:
            return False
        
        best = min(valid_prices, key=lambda x: x['price'])
        
        logger.info(f"Найден лот за {best['price']:,} (уверенность: {best['confidence']}%)")
        self.last_status = f"Найден лот: {best['price']:,}"
        
        # Если тестовый режим - только логируем
        if self.config.test_mode:
            logger.info("ТЕСТОВЫЙ РЕЖИМ: покупка не совершается")
            self.last_status += " (ТЕСТ)"
            return False
        
        # Покупка
        if self.config.auto_buy:
            return self._buy_item(best)
        
        return False
    
    def _buy_item(self, price_data: Dict) -> bool:
        """Покупка выбранного лота"""
        try:
            # Клик по лоту
            bbox = price_data['bbox']
            area = self.config.ocr_areas['prices']
            
            # Вычисляем координаты клика (в глобальных координатах)
            click_x = area['x1'] + bbox[0] + bbox[2] // 2
            click_y = area['y1'] + bbox[1] + bbox[3] // 2
            
            pyautogui.click(click_x, click_y)
            time.sleep(0.5)  # Ожидание открытия окна
            
            # Поиск кнопки "Выкупить лот"
            if not self._find_and_click_buy_button():
                logger.warning("Кнопка выкупа не найдена")
                return False
            
            # Подтверждение покупки
            coords = self.config.coordinates['ok_button']
            pyautogui.click(coords['x'], coords['y'])
            time.sleep(0.5)
            
            # Успешная покупка
            self.purchases_count += 1
            self.total_spent += price_data['price']
            
            logger.info(f"Покупка совершена! Цена: {price_data['price']:,}")
            self.last_status = f"Куплено за {price_data['price']:,}"
            
            return True
            
        except Exception as e:
            logger.error(f"Ошибка покупки: {e}")
            return False
    
    def _find_and_click_buy_button(self) -> bool:
        """Поиск и клик по кнопке выкупа"""
        try:
            area = self.config.ocr_areas['buy_button']
            screenshot = ImageGrab.grab(bbox=(
                area['x1'], area['y1'],
                area['x2'], area['y2']
            ))
            
            texts = self.ocr.extract_text(screenshot)
            
            # Поиск похожих текстов
            buy_keywords = ['выкупить', 'купить', 'выкуп', 'лот', 'приобрести']
            
            for text, data in texts.items():
                text_lower = text.lower()
                for keyword in buy_keywords:
                    if keyword in text_lower:
                        # Клик в центр текста
                        bbox = data['bbox']
                        click_x = area['x1'] + bbox[0] + bbox[2] // 2
                        click_y = area['y1'] + bbox[1] + bbox[3] // 2
                        
                        pyautogui.click(click_x, click_y)
                        logger.info(f"Клик по кнопке: {text}")
                        return True
            
            return False
            
        except Exception as e:
            logger.error(f"Ошибка поиска кнопки: {e}")
            return False
    
    def _scroll_down(self, steps: int):
        """Прокрутка вниз"""
        try:
            slider = self.config.coordinates['slider']
            pyautogui.moveTo(slider['x'], slider['y'])
            pyautogui.mouseDown()
            pyautogui.moveTo(slider['x'], slider['y'] + steps, duration=0.2)
            pyautogui.mouseUp()
            time.sleep(0.3)
            
        except Exception as e:
            logger.error(f"Ошибка прокрутки: {e}")
    
    def _switch_page(self, page_number: int) -> bool:
        """Переключение на страницу"""
        try:
            area = self.config.ocr_areas['pages']
            screenshot = ImageGrab.grab(bbox=(
                area['x1'], area['y1'],
                area['x2'], area['y2']
            ))
            
            # Распознавание номеров страниц
            processed = self.ocr.preprocess_image(screenshot)
            custom_config = f'--oem 3 --psm 6 -c tessedit_char_whitelist=0123456789 '
            
            data = pytesseract.image_to_data(
                processed,
                lang='rus',
                config=custom_config,
                output_type=pytesseract.Output.DICT
            )
            
            # Поиск номера страницы
            for i, text in enumerate(data['text']):
                if text.strip() == str(page_number):
                    # Клик по номеру страницы
                    bbox = (data['left'][i], data['top'][i],
                           data['width'][i], data['height'][i])
                    
                    click_x = area['x1'] + bbox[0] + bbox[2] // 2
                    click_y = area['y1'] + bbox[1] + bbox[3] // 2
                    
                    pyautogui.click(click_x, click_y)
                    time.sleep(1)
                    
                    logger.info(f"Переключение на страницу {page_number}")
                    return True
            
            return False
            
        except Exception as e:
            logger.error(f"Ошибка переключения страницы: {e}")
            return False
    
    def get_stats(self) -> Dict:
        """Получение статистики"""
        return {
            'purchases': self.purchases_count,
            'spent': self.total_spent,
            'status': self.last_status,
            'running': self.is_running
        }

# ============= КЛАСС ГРАФИЧЕСКОГО ИНТЕРФЕЙСА =============
class BotGUI:
    """Главное окно приложения"""
    
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("STALCRAFT Auction Bot v1.0")
        self.root.geometry("650x750")
        self.root.resizable(False, False)
        
        # Иконка приложения (если есть)
        try:
            self.root.iconbitmap(default='icon.ico')
        except:
            pass
        
        # Настройка стилей
        style = ttk.Style()
        style.theme_use('clam')
        
        # Конфигурация
        self.config = Config()
        self.config.load()
        
        # Бот
        self.bot = AuctionBot(self.config)
        
        # Переменные
        self.coord_vars = {}
        self.area_vars = {}
        
        # Создание интерфейса
        self._setup_ui()
        
        # Обработка закрытия
        self.root.protocol("WM_DELETE_WINDOW", self._on_close)
        
        # Таймер обновления статуса
        self._update_status()
    
    def _setup_ui(self):
        """Настройка интерфейса"""
        # Основной фрейм
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky="nsew")
        
        # Заголовок
        title_label = ttk.Label(main_frame, text="STALCRAFT AUCTION BOT", 
                               font=('Arial', 14, 'bold'))
        title_label.grid(row=0, column=0, columnspan=3, pady=(0, 10))
        
        # == ОСНОВНЫЕ НАСТРОЙКИ ==
        basic_frame = ttk.LabelFrame(main_frame, text="Основные настройки", padding="10")
        basic_frame.grid(row=1, column=0, columnspan=3, sticky="ew", pady=5)
        
        # Товар
        ttk.Label(basic_frame, text="Товар:").grid(row=0, column=0, sticky="w")
        self.search_var = tk.StringVar(value=self.config.search_text)
        entry_search = ttk.Entry(basic_frame, textvariable=self.search_var, width=30)
        entry_search.grid(row=0, column=1, padx=5, sticky="w")
        
        # Максимальная цена
        ttk.Label(basic_frame, text="Макс. цена:").grid(row=1, column=0, sticky="w", pady=5)
        self.price_var = tk.StringVar(value=str(self.config.max_price))
        entry_price = ttk.Entry(basic_frame, textvariable=self.price_var, width=15)
        entry_price.grid(row=1, column=1, padx=5, sticky="w")
        
        # Страницы
        ttk.Label(basic_frame, text="Страниц:").grid(row=2, column=0, sticky="w")
        self.pages_var = tk.StringVar(value=str(self.config.pages_count))
        entry_pages = ttk.Entry(basic_frame, textvariable=self.pages_var, width=8)
        entry_pages.grid(row=2, column=1, padx=5, sticky="w")
        
        self.use_pages_var = tk.BooleanVar(value=self.config.use_pages)
        ttk.Checkbutton(basic_frame, text="Использовать страницы", 
                       variable=self.use_pages_var).grid(row=2, column=2, padx=5)
        
        # == КООРДИНАТЫ ==
        coord_frame = ttk.LabelFrame(main_frame, text="Координаты", padding="10")
        coord_frame.grid(row=2, column=0, columnspan=3, sticky="ew", pady=5)
        
        coord_labels = [
            ("Поле ввода", "input_field"),
            ("Обновить", "refresh_button"),
            ("Ползунок", "slider"),
            ("ОК", "ok_button")
        ]
        
        for i, (label, key) in enumerate(coord_labels):
            ttk.Label(coord_frame, text=f"{label}:").grid(row=i, column=0, sticky="w", padx=5)
            
            self.coord_vars[key] = tk.StringVar(
                value=f"X={self.config.coordinates[key]['x']} Y={self.config.coordinates[key]['y']}"
            )
            ttk.Label(coord_frame, textvariable=self.coord_vars[key]).grid(
                row=i, column=1, padx=5, sticky="w"
            )
            
            ttk.Button(coord_frame, text="Настроить", 
                      command=lambda k=key: self._set_coordinate(k)).grid(
                row=i, column=2, padx=5
            )
        
        # == OCR ОБЛАСТИ ==
        ocr_frame = ttk.LabelFrame(main_frame, text="OCR области", padding="10")
        ocr_frame.grid(row=3, column=0, columnspan=3, sticky="ew", pady=5)
        
        ocr_labels = [
            ("Цены", "prices"),
            ("Кнопка выкупа", "buy_button"),
            ("Страницы", "pages")
        ]
        
        for i, (label, key) in enumerate(ocr_labels):
            ttk.Label(ocr_frame, text=f"{label}:").grid(row=i, column=0, sticky="w", padx=5)
            
            area = self.config.ocr_areas[key]
            self.area_vars[key] = tk.StringVar(
                value=f"({area['x1']},{area['y1']}) → ({area['x2']},{area['y2']})"
            )
            ttk.Label(ocr_frame, textvariable=self.area_vars[key]).grid(
                row=i, column=1, padx=5, sticky="w"
            )
            
            ttk.Button(ocr_frame, text="Настроить", 
                      command=lambda k=key: self._set_ocr_area(k)).grid(
                row=i, column=2, padx=5
            )
        
        # == OCR НАСТРОЙКИ ==
        settings_frame = ttk.LabelFrame(main_frame, text="Параметры OCR", padding="10")
        settings_frame.grid(row=4, column=0, columnspan=3, sticky="ew", pady=5)
        
        # Язык
        ttk.Label(settings_frame, text="Язык:").grid(row=0, column=0, sticky="w")
        self.lang_var = tk.StringVar(value=self.config.ocr_settings['lang'])
        lang_combo = ttk.Combobox(settings_frame, textvariable=self.lang_var,
                                 values=['rus', 'eng', 'rus+eng'], width=10)
        lang_combo.grid(row=0, column=1, padx=5, sticky="w")
        
        # Масштаб
        ttk.Label(settings_frame, text="Масштаб:").grid(row=1, column=0, sticky="w", pady=2)
        self.scale_var = tk.StringVar(value=str(self.config.ocr_settings['scale']))
        scale_combo = ttk.Combobox(settings_frame, textvariable=self.scale_var,
                                  values=['100', '150', '200', '300', '400'], width=10)
        scale_combo.grid(row=1, column=1, padx=5, sticky="w")
        
        # PSM
        ttk.Label(settings_frame, text="PSM:").grid(row=2, column=0, sticky="w", pady=2)
        self.psm_var = tk.StringVar(value=str(self.config.ocr_settings['psm']))
        psm_combo = ttk.Combobox(settings_frame, textvariable=self.psm_var,
                                values=['3', '6', '7', '8', '13'], width=10)
        psm_combo.grid(row=2, column=1, padx=5, sticky="w")
        
        # Попытки
        ttk.Label(settings_frame, text="Попыток:").grid(row=3, column=0, sticky="w", pady=2)
        self.attempts_var = tk.StringVar(value=str(self.config.ocr_settings['attempts']))
        attempts_entry = ttk.Entry(settings_frame, textvariable=self.attempts_var, width=10)
        attempts_entry.grid(row=3, column=1, padx=5, sticky="w")
        
        # Уверенность
        ttk.Label(settings_frame, text="Уверенность %:").grid(row=4, column=0, sticky="w", pady=2)
        self.conf_var = tk.StringVar(value=str(self.config.ocr_settings['confidence_threshold']))
        conf_entry = ttk.Entry(settings_frame, textvariable=self.conf_var, width=10)
        conf_entry.grid(row=4, column=1, padx=5, sticky="w")
        
        # == КНОПКИ УПРАВЛЕНИЯ ==
        control_frame = ttk.Frame(main_frame)
        control_frame.grid(row=5, column=0, columnspan=3, pady=10)
        
        # Режим
        ttk.Label(control_frame, text="Режим:").grid(row=0, column=0, padx=5)
        self.mode_var = tk.StringVar(value="ТЕСТ" if self.config.test_mode else "ВКЛ")
        mode_combo = ttk.Combobox(control_frame, textvariable=self.mode_var,
                                 values=['ТЕСТ', 'ВКЛ'], width=8, state='readonly')
        mode_combo.grid(row=0, column=1, padx=5)
        
        # Кнопки
        ttk.Button(control_frame, text="Проверить OCR", 
                  command=self._test_ocr).grid(row=0, column=2, padx=5)
        
        self.start_btn = ttk.Button(control_frame, text="▶ ЗАПУСК", 
                                   command=self._start_bot)
        self.start_btn.grid(row=0, column=3, padx=5)
        
        self.stop_btn = ttk.Button(control_frame, text="■ СТОП", 
                                  command=self._stop_bot, state='disabled')
        self.stop_btn.grid(row=0, column=4, padx=5)
        
        # == СТАТУС ==
        status_frame = ttk.LabelFrame(main_frame, text="Статус", padding="5")
        status_frame.grid(row=6, column=0, columnspan=3, sticky="ew", pady=5)
        
        self.status_var = tk.StringVar(value="Готов к работе")
        ttk.Label(status_frame, textvariable=self.status_var, font=('Arial', 10)).pack()
        
        self.stats_var = tk.StringVar(value="Покупок: 0 | Потрачено: 0")
        ttk.Label(status_frame, textvariable=self.stats_var, font=('Arial', 9)).pack()
        
        # Кнопка сохранения
        ttk.Button(main_frame, text="Сохранить настройки", 
                  command=self._save_config).grid(row=7, column=0, columnspan=3, pady=10)
    
    def _set_coordinate(self, key: str):
        """Установка координаты через клик мыши"""
        self.root.iconify()
        time.sleep(0.5)
        
        try:
            # Показываем инструкцию
            self.root.deiconify()
            messagebox.showinfo("Установка координат", 
                               "Наведите мышь на нужный элемент и нажмите ПРОБЕЛ")
            self.root.iconify()
            
            # Ожидание пробела
            import keyboard
            keyboard.wait('space')