Шахматы. От начала до читов

sgd-p6hahngqmx9vudoapt9p5ig.jpeg

Как-то пару лет назад youtube начал мне подсовывать шахматные видео. Смотрел их, и спустя какое-то время начал играть. Сначала против компа на телефоне, затем на lichess. В какой-то прекрасный вечер мне надоело проигрывать и задался вопросом как бы не проигрывать или после отыгрываться. В итоге игра превратилась в написание чита.


Начало

Нашел на github код который сулил уничтожение всех и каждого. Естественно он не заработал. Наверное какие-то библиотеки изменились за несколько лет, а может автор умышленно что-то подправил. Но взяв его за основу и подчеркнув из него идеи, переработал и сделал свое. Заодно узнал для себя новое, как скринить экран и сравнивать изображения.


Чит

Всего один шаг от игры до игры с читами оказался. Если можно решить проблему программно, то почему не решить.


Этапы реализации


  • игровой движок
  • не привязываться к конкретной шахматной площадке
  • распознавать цвет
  • распознавать свой ход
  • распознавать ход противника
  • не палиться программно


Движок

Свой движок делать нет смысла конечно, уже есть готовые. Оригинальный чит использует Stockfishches, для него есть библиотека на python. Так что берем то, что работает.

self.stockfish = Stockfish(path, depth=5, 
    parameters={"Threads": 2, ,"Skill Level": 10, "Hash": 8})


Шахматная доска


eywpqpcizdqozu9wfdrvfo_avak.jpeg

Выносим настройки доски в конфиг. Задаю координаты шахматной доски, координату взятия цвета (белый/черный) и выделяю доску серой рамкой для наглядности.

{
  "X_START": 570,
  "Y_START": 185,
  "X_END": 1285,
  "Y_END": 900,

  "COLOR_X": 615,
  "COLOR_Y": 845,

  "CELL_COUNT": 8
}

Так же здаю колличество ячеек в ряду доски. Мало ли будут шахматы не 8 на 8, а 10 на 10. Привычка все выносить в конфиги, от нее уже не избавиться.


Цвет

Он берется по координатам из конфига и сравнивается с шаблоном:

self.p = QPixmap()
self.descktop = QApplication.desktop()
self.p = QScreen.grabWindow(self.parent.primaryScreen(), self.descktop.winId())
self.img = QImage()
self.img = self.p.toImage()
self.b = self.img.pixel(x, y)
self.c = QColor()
self.c.setRgb(self.b)
self.c = QColor()
self.c.setRgb(self.b)

if self.c.name() == black:
    self.color_palyer = self.color_palyer_black
    return self.color_palyer
elif self.c.name() == white:
    self.color_palyer = self.color_palyer_white
    return self.color_palyer

где x, y значения из конфига. Если мы белые то делаем свой ход, если черные ждем хода противника.


Свой ход

Отслеживается по координатам откликов мыши, кнопка нажата/отпущена.

def wait_for_click():
    state_left = win32api.GetAsyncKeyState(0x01)  # Left button down = 0 or 1. Button up = -127 or -128
    a = state_left

    while a == state_left:
        a = win32api.GetAsyncKeyState(0x01)
        time.sleep(0.025)

    return position()

Позиция возвращается в координатах. Например при игре за черных (886 757) (902 577), её привожу к виду e7e5. Так же делаю защиту на проверку валидности хода, и игнорирую нажатия за пределами доски (за серой рамкой).


Ход противника

Здесь делал двумя вариантами от простого к сложному.
Первой была идея через повторение хода противника, перетаскивал его фигуру. Далее по полученным координатам переводил к ходу понятному движку (d2d4). Так как за основу берется реализация своего хода которая была уже сделана. Здесь можно спалиться программно, т.к. слишком много движений мышкой, которые шахматный сервер определит.

Вторая реализация через скриншоты доски (как в оригинальном чите) и ожидания изменения её. Далее на анализе того что было и что стало определить какой ход сделал противник. Это было самое сложное, но сокращающее количество моих телодвижений вдвое.
После своего хода c2c3, ждем ход противника:


mktzk_c1id7ecfanekdbixmw_ke.jpeg

Дождавшись изменения доски e7e5, парсим эти изменения.


gjerqc2sf3b0fjnli0jmyfw9uim.jpeg

Подсвечиваем черным ход противника. Отдаем его движку, и ждем от него наилучший для себя ход. Подсвечиваем его зеленым:


l3q7w_e6kyqbbftqsmpdjyxg_r8.jpeg

Сложности были в ситуациях с зелеными точками/квадратами, которые не успевали пропасть после моего хода. И в таких случаях:


5jxyvvqbzrdctoooo4zdpgiczei.png

Когда скриншоты брались в не то время. Определить из того что было, как стало — каким образом так случилось невозможно.


vluzpw62huypd0q_q7lfp5zcwe8.png

Так же необходимо игнорировать короля под шахом. Его красная подсветка портила всю малину, его можно защитить другой фигурой, но клетка претерпевает при этом изменения и надо как то учесть, что в этой клетке хода не было. А еще рокировки, когда состояния меняют сразу четыре клетки.


klhvqkuyjkhtflzhg3cep-jsi6w.jpeg

Пришел к выводу, что сравнивать имеет смысл не всю клетку с фигурой, а её малую часть. Т.к. черная фигура не может скушать черную, а белая белую.


tvntaa2dteaomp-1atuet4nwhem.jpeg

Вот это та область которая всегда взаимно однозначно позволяет определить, из какой клетки и в какую был сделан ход. В оригинале у ofeksadlo были шаблоны какие фигуры и клетки нужно игнорировать. Вроде мой путь легче и проще.

При таком варианте программно палиться недолжно, но кто его знает. Я не спец. по вебу, и мне было бы интересно узнать может ли он определять запущенные приложения по верх браузера. Возможно определяет изменение ракурса при отрисовке ходов.


Заключение

На разработку ушло ~ месяцев 6, самые продуктивные периоды были сразу после проигрывания какой-то принципиальной партии.

Из недоделанного:


  • обработка перехода пешки в высшую фигуру
  • события от мыши на callback-и
  • на текущий момент позиция учитывается ход за ходом. Если произойдет ошибка на любом ходу, то все пойдет лесом. В идеале отталкиваться от текущего положения доски и его передавать движку.

Подпортил настроение многим своей разработкой. Но меня забанили еще на первой реализации обработки хода противника противника практически сразу. Теперь со своего аккаунта могу играть с такими же читаками как и я. Так что все честно)

Видео тестовой игры:

Меня забавляет когда в комментариях на youtube на того же Ханса Ниманна пишут: глядите он в бок косится на другой экран, значит точно читерит. Вот как все выглядит и ни куда смотреть не надо.

Чит выкладывать не буду, дабы не портить игру всем тем кто честно играет. А вот коды взятия скриншота (приведение к массиву) и отрисовки квадратиков на экране пожалуйста:

import cv2
import numpy
from pyautogui import position, screenshot

def screen_get(self):
    return screenshot(region=(self.settings['X_START'], self.settings['Y_START'],
                              self.settings['X_END'] - self.settings['X_START'],
                              self.settings['Y_END'] - self.settings['Y_START']))

def screen_get_numpy(self):
    img = self.screen_get()
    img_array = cv2.cvtColor(numpy.array(img), cv2.COLOR_RGB2BGR)
    return img_array, img
import sys

from PyQt5.QtCore import Qt, QRect
from PyQt5.QtGui import QPainter, QBrush, QPen, QPixmap, QColor, QImage, QScreen
from PyQt5.QtWidgets import QApplication, QMainWindow

class DrawingWindow(QMainWindow):
    def __init__(self, parent = None):
        super().__init__()
        self.setMouseTracking(True)

        self.parent = parent

        self.setWindowTitle("Transparent Drawing Window")
        self.setGeometry(0, 0, QApplication.desktop().screenGeometry().width(),
                         QApplication.desktop().screenGeometry().height())
        self.setAttribute(Qt.WA_TranslucentBackground, True)
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)

        self.painter = QPainter()
        self.painter.setRenderHint(QPainter.Antialiasing)

        self.pen_color_red = QColor(255, 0, 0)  # Set the initial pen color to red
        self.pen_color_black = QColor(0, 0, 0)
        self.pen_color_green = QColor(0, 125, 0)
        self.pen_color_gray = QColor(128, 128, 128)
        self.pen_width = 4  # Set the initial pen width to 4

        self.color = self.pen_color_green

        self.color_palyer = ''
        self.color_palyer_white = 'white'
        self.color_palyer_black = 'black'

    def get_opponent_color(self):
        if self.color_palyer == self.color_palyer_white:
            return self.color_palyer_black
        elif self.color_palyer == self.color_palyer_black:
            return self.color_palyer_white

        return self.color_palyer

    def get_my_color(self, x=973, y=763, white='#ffffff', black='#000000'):
        if self.parent == None:
            self.color_palyer = self.color_palyer_white
            return self.color_palyer

        self.p = QPixmap()
        self.descktop = QApplication.desktop()
        self.p = QScreen.grabWindow(self.parent.primaryScreen(), self.descktop.winId())
        self.img = QImage()
        self.img = self.p.toImage()
        self.b = self.img.pixel(x, y)
        self.c = QColor()
        self.c.setRgb(self.b)

        if self.c.name() == black:
            self.color_palyer = self.color_palyer_black
            return self.color_palyer
        elif self.c.name() == white:
            self.color_palyer = self.color_palyer_white
            return self.color_palyer

        self.color_palyer = self.color_palyer_white
        return self.color_palyer

    def update_coordinates(self, coordinates, color='green'):
        self.coordinates = coordinates
        if color == 'red':
            self.color = self.pen_color_red
        elif color == 'green':
            self.color = self.pen_color_green
        elif color == 'gray':
            self.color = self.pen_color_gray
        else :
            self.color = self.pen_color_black

    def paintEvent(self, event):
        self.painter.begin(self)
        self.painter.setPen(Qt.NoPen)
        self.painter.setBrush(QBrush(Qt.transparent))
        self.painter.drawRect(QRect(0, 0, self.width(), self.height()))  # Draw a transparent background

        self.painter.setPen(QPen(QColor(self.color), self.pen_width))
        self.painter.setBrush(QBrush(Qt.transparent))

        for coord in self.coordinates:
            x, y, width, height = coord
            self.painter.drawRect(x, y, width, height)  # Draw rectangles using the provided coordinates

        self.painter.end()

if __name__ == "__main__":
    coordinates = [(851, 716, 82, 82), (851, 532, 82, 82)]

    app = QApplication(sys.argv)

    window = DrawingWindow()  # Create an instance of the DrawingWindow class with the given coordinates
    window.update_coordinates(coordinates)
    window.show()  # Display the window

    sys.exit(app.exec_())


Благодарности

Никите за Alexandra Botez;) посмеялся, играю так же.


Ссылки


Habrahabr.ru прочитано 2994 раза