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


Начало



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


Чит


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


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


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

Движок


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


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

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



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


{
  "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, ждем ход противника:



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



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



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



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



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



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



Вот это та область которая всегда взаимно однозначно позволяет определить, из какой клетки и в какую был сделан ход. В оригинале у 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 ;) посмеялся, играю так же.


Ссылки


Комментарии (20)


  1. ALexKud
    09.05.2024 14:01
    +15

    Заниматься читерством это просто признать себя в шахматах полным нулем. Лучше уж совсем не играть.


    1. Tim_86
      09.05.2024 14:01

      Точно. При этом читерство стало настоящей проблемой современных шахмат, в том числе на самом высоком уровне. На читерстве ловят даже гроссмейстеров. Но часто очень сложно это доказать. Опытные читеры ловко маскируются - например, прибегая к помощи компьютера лишь в критических позициях, несколько раз за игру. Последнее время Крамник много занимается этой темой и освещает её.

      Понятно, что читерство лишает смысла саму игру.


      1. Zveruga_NAX
        09.05.2024 14:01
        +1

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


        1. zartarn
          09.05.2024 14:01

          А в чем проблема садить их играть в "стерильной комнате". наружу только трансляция с задержкой (как стримеры делают для борьбы со стримснайперами) и т.п.?


          1. Travisw
            09.05.2024 14:01

            В клетку Фарадея надо запирать игроков


        1. Tim_86
          09.05.2024 14:01
          +1

          Ну, не считаю что Крамник дискредитирует борьбу с читерами. Он как может привлекает внимание к проблеме нечестной игры. Доводы которые он приводит в отношении некоторых игроков, выглядят обоснованными. В отношении кого-то он конечно может ошибаться. Но, как сказал Грищук в одном интервью, в отношении читерства должна действовать "презумпция виновности". То есть когда есть обоснованные подозрения, этого достаточно. Не бывает так (при честной игре) что ноунейм регулярно занимает призовые места в онлайн турнирах где участвуют ведущие гроссмейстеры, при этом в офлайне не показывая соответствующих результатов.


  1. Ergistael
    09.05.2024 14:01
    +3

    Спасибо за реализацию! Мне поможет, но не для шахмат и не для читерства, а для анализа. Хорошая отправная точка.


  1. ri1wing
    09.05.2024 14:01
    +2

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

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

    На разработку ушло ~ месяцев 6

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


    1. Jessy_James Автор
      09.05.2024 14:01

      Не уточнил, что в течении 6 месяцев этим занимался по 2-3 часа в неделю. Большая часть времени на отладку уходила.


  1. nirom
    09.05.2024 14:01
    +1

    На разработку ушло ~ месяцев 6

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


    1. Jessy_James Автор
      09.05.2024 14:01

      Когда начал играть мой рейтинг был порядка 800, потом немного(4 месяца) разыгрался и поднял до 1230. После занялся этой затеей. И далее шахматы стали механизмом отладки программы. Сейчас хочу новый аккаунт завести и поиграть нормально.


      1. Senti033
        09.05.2024 14:01

        Привет!Добавишь в друзья с нового аккаунта если не сложно?


  1. gurovofficial
    09.05.2024 14:01
    +5

    Здравствуйте.

    С точки зрения программирование - бесполезная тривиальная задача.

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

    С экономической точки зрения - необоснованно-вредительская задача.


    1. Jessy_James Автор
      09.05.2024 14:01

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


  1. KivyMD
    09.05.2024 14:01

    licess каждую партию анализирует. Это бан после нескольких партий!


    1. Jessy_James Автор
      09.05.2024 14:01
      +1

      Да, на игре 3-ей они меня и забанили.


  1. R0ller
    09.05.2024 14:01
    +1

    Странное вложение 6-ти месяцев, конечно) Цель и глупая, и гнусная - непонятно вообще какой смысл тратить время на шахматы и играть с читами. Да и даже если хочется - ничего не надо кодить, чтобы читерить. Наверное, и расширения есть какие нибудь под это уже, если так уж неудобно делать это руками.

    Ну, ладно, может кому-то будет полезно. Надеюсь только для другой цели.


    1. Jessy_James Автор
      09.05.2024 14:01

      Смысл не во времени, и не в игре. А в процессе... Выше написал в комментарии что 6 месяцев от начала и до конца. Что по где-то по 2 часа в неделю, всего 48 часов.


  1. zartarn
    09.05.2024 14:01
    +2

    Все как то слишком серьезно к этому относятся. В разное время от игр мы по разному получаем удовольствие.

    (безотносительно шахмат) Когда у тебя только школа, ты можешь огромное количество времени уделять играм, заниматься гриндом и прочим. Когда ты уже взрослеешь и появляются прочие обязанности. времени становится меньше. в такие моменты кто то начинаешь донатить, от игры при этом меньше удовольствие не получаешь, просто сокращаешь муторную часть. А кому то становится интересно ботоводить. Это же как тамогочи в детстве) взять тот же адреналин (бот для Lineage), и заскриптовать, и настроить, и наладить комуникацию..

    От всего по своему можно получать удовольствие. Если ты получил от этого удовольствие, то время потрачено не зря)

    Всему своё время.

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

    Есть шанс научиться, это какой то спор на значительную сумму. что за год с нуля до какого то условного рейтинга добраться. Или подготовиться к какой то конкретной игре. Просто так "хочу научиться играть в шахматы" для взрослого уже не сработает. И всё равно он будет в большинстве случаев играть слабее детей которые в шахматных школах играют. Они чисто на опыте будут выезжать. Это как на коньках выйти играть против ребенка который занимается в спорт-школе.


  1. dxq3
    09.05.2024 14:01

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