Как-то пару лет назад 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)
Ergistael
09.05.2024 14:01+3Спасибо за реализацию! Мне поможет, но не для шахмат и не для читерства, а для анализа. Хорошая отправная точка.
ri1wing
09.05.2024 14:01+2Меня забавляет когда в комментариях на youtube на того же Ханса Ниманна пишут: глядите он в бок косится на другой экран, значит точно читерит. Вот как все выглядит и ни куда смотреть не надо.
Кэп намекает, что в шахматных видео доска на экране. Слишком палевно будет прямо на ней подсказки рисовать.
На разработку ушло ~ месяцев 6
А мог бы потратить это время на решение задачек, разбор своих и чужих партий, выучить пару ходовых дебютов... Глядишь и скил бы вырос.
Jessy_James Автор
09.05.2024 14:01Не уточнил, что в течении 6 месяцев этим занимался по 2-3 часа в неделю. Большая часть времени на отладку уходила.
nirom
09.05.2024 14:01+1На разработку ушло ~ месяцев 6
За это время можно пройти какой-нибудь классический учебник по шахматам, того же Авербаха.
Jessy_James Автор
09.05.2024 14:01Когда начал играть мой рейтинг был порядка 800, потом немного(4 месяца) разыгрался и поднял до 1230. После занялся этой затеей. И далее шахматы стали механизмом отладки программы. Сейчас хочу новый аккаунт завести и поиграть нормально.
gurovofficial
09.05.2024 14:01+5Здравствуйте.
С точки зрения программирование - бесполезная тривиальная задача.
С точки зрения шахмат - вы в большом минусе, так как за эти полгода - вы могли бы заниматься шахматами и уже эти знания у вас никто бы не отнял.
С экономической точки зрения - необоснованно-вредительская задача.
Jessy_James Автор
09.05.2024 14:01За эти пол года узнал большинство причин по которым машина плохо заводится ;) Так же как менять термостат и выгонять воздушную пробку. И ещё что со временем пружины под воздействием температуры могут становиться жёстче, всегда думал что они ослабевают только. И все эти знания мне только настроение портили.
R0ller
09.05.2024 14:01+1Странное вложение 6-ти месяцев, конечно) Цель и глупая, и гнусная - непонятно вообще какой смысл тратить время на шахматы и играть с читами. Да и даже если хочется - ничего не надо кодить, чтобы читерить. Наверное, и расширения есть какие нибудь под это уже, если так уж неудобно делать это руками.
Ну, ладно, может кому-то будет полезно. Надеюсь только для другой цели.
Jessy_James Автор
09.05.2024 14:01Смысл не во времени, и не в игре. А в процессе... Выше написал в комментарии что 6 месяцев от начала и до конца. Что по где-то по 2 часа в неделю, всего 48 часов.
zartarn
09.05.2024 14:01+2Все как то слишком серьезно к этому относятся. В разное время от игр мы по разному получаем удовольствие.
(безотносительно шахмат) Когда у тебя только школа, ты можешь огромное количество времени уделять играм, заниматься гриндом и прочим. Когда ты уже взрослеешь и появляются прочие обязанности. времени становится меньше. в такие моменты кто то начинаешь донатить, от игры при этом меньше удовольствие не получаешь, просто сокращаешь муторную часть. А кому то становится интересно ботоводить. Это же как тамогочи в детстве) взять тот же адреналин (бот для Lineage), и заскриптовать, и настроить, и наладить комуникацию..
От всего по своему можно получать удовольствие. Если ты получил от этого удовольствие, то время потрачено не зря)
Всему своё время.
Про сообщения "лучше бы учился играть" и т.п. - у взрослого человека крайне мало мотивации заниматься шахматами. Свободного времени не так много, а если еще ты вместо отдыха получаешь проигрыши и разочарование на этом фоне, то мотивация стремится к нулю, тиак как это стресс а не отдых. Поэтому взрослого практически невозможно научить, не потому что мозги как то не так соображают, а потому что забот полон рот а свободного времени для отдыха не так много.
Есть шанс научиться, это какой то спор на значительную сумму. что за год с нуля до какого то условного рейтинга добраться. Или подготовиться к какой то конкретной игре. Просто так "хочу научиться играть в шахматы" для взрослого уже не сработает. И всё равно он будет в большинстве случаев играть слабее детей которые в шахматных школах играют. Они чисто на опыте будут выезжать. Это как на коньках выйти играть против ребенка который занимается в спорт-школе.
dxq3
09.05.2024 14:01Следующий челлендж - приехать на погрузчике в тренажерку и поднять самую тяжелую штангу, какую можно там собрать. И написать потом статью, как стать и умным и сильным
ALexKud
Заниматься читерством это просто признать себя в шахматах полным нулем. Лучше уж совсем не играть.
Tim_86
Точно. При этом читерство стало настоящей проблемой современных шахмат, в том числе на самом высоком уровне. На читерстве ловят даже гроссмейстеров. Но часто очень сложно это доказать. Опытные читеры ловко маскируются - например, прибегая к помощи компьютера лишь в критических позициях, несколько раз за игру. Последнее время Крамник много занимается этой темой и освещает её.
Понятно, что читерство лишает смысла саму игру.
Zveruga_NAX
Да, и читерство не просто проблема, а вообще ставит под угрозу шахматы как вид спорта. Слишком легко читерить, был случай на офлайне турнире, шахматисту подсказывал тренер, ходы были зашифрованы в перемещениях тренера по залу, и поймали их только потому что подружка их третьего сообщника, отправляющего ходы по смс, заподозрила его в измене увидев странные сообщения и подняла шум. В онлайн шахматах совершено не понятно как ловить читеров, можно поймать только самых глупых. И со всеми уважением к Крамнику, но он только дискредитирует борьбу с читерами. Его можно понять, он всю жизнь посветил этому спорту, а теперь даже примерный процент читеров в топе не понятен, ясно что они есть. И он обвиняет всёх подряд, окончательно превращая всё в клоунаду.
zartarn
А в чем проблема садить их играть в "стерильной комнате". наружу только трансляция с задержкой (как стримеры делают для борьбы со стримснайперами) и т.п.?
Travisw
В клетку Фарадея надо запирать игроков
Tim_86
Ну, не считаю что Крамник дискредитирует борьбу с читерами. Он как может привлекает внимание к проблеме нечестной игры. Доводы которые он приводит в отношении некоторых игроков, выглядят обоснованными. В отношении кого-то он конечно может ошибаться. Но, как сказал Грищук в одном интервью, в отношении читерства должна действовать "презумпция виновности". То есть когда есть обоснованные подозрения, этого достаточно. Не бывает так (при честной игре) что ноунейм регулярно занимает призовые места в онлайн турнирах где участвуют ведущие гроссмейстеры, при этом в офлайне не показывая соответствующих результатов.