javascript

Шахматный советник для тех, кто зевает и не любит читеров

  • понедельник, 31 марта 2025 г. в 00:00:07
https://habr.com/ru/articles/895822/

Это новый вариант статьи, уже выложенной на хабре. Та статья писалась на эмоциях, сразу после первых декабрьских версий. Нынешняя — это результат трехмесячных воскресных посиделок в Visual Studio. Тут и параллельные процессы и манипуляции в js и даже примитивный шахматный движок на C#.

С чего всё началось

Я очень люблю играть в шахматы. Постоянно играю 10-минутные партии на chess.com. Но у меня есть проблема — туннельное мышление. Я вижу один ход и зацикливаюсь на нём. Из-за этого упускаю возможности и зеваю фигуры. Рейтинг не поднимается выше 1500. Если бы был кто-то рядом, кто бы дал мне по рукам за зевок.

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

На Рождество у нас был выходной, и я начал думать, как подружить его с chess.com. Stockfish можно запустить как дочерний процесс, передать в консоль закодированную позицию и настройки (включая время на обдумывание), и… прочитать ответ. Но как получить саму позицию?

Сначала я думал о вариантах с машинным зрением. Но на chess.com десятки вариантов оформления доски и фигур. Плюс размер и положение доски могут меняться. Затем я подумал о подключении к браузеру. Но браузеров много, плюс сложности с чтением чужого процесса… Расширение для браузера? У меня нет опыта их написания. Решение пришло — встроить браузер прямо в приложение. Тогда не будет проблем с чтением текущей веб-страницы.

Итоговая версия выглядит так: простое WPF-приложение с одним окном и веб-контролом, запускающее Stockfish как дочерний процесс и взаимодействующее с ним через консольные потоки ввода-вывода. За пять минут создаю новый WPF-проект в Visual Studio, добавляю WebBrowser как основной элемент, указываю домашнюю страницу chess.com, запускаю… и ошибки JavaScript, ничего не работает.

Встроенный браузер

Старый элемент WebBrowser в WPF всё ещё использует библиотеки Internet Explorer, которые несовместимы с современными веб-страницами. Нужно что-то посвежее — Chromium или Edge (который по сути тоже Chromium, но с другим интерфейсом). Есть библиотеки для обоих вариантов. Мой основной браузер — Microsoft Edge, поэтому я установил компонент от Microsoft — Microsoft.Web.WebView2 через NuGet.

Теперь всё работает. Я даже вошёл в аккаунт, закрыл приложение, снова запустил — и остался в системе. Значит, поддерживаются cookies и сессии. Прекрасно. Однако работать с DOM по-старому уже не получится. В WebView2 нельзя просто получить доступ к HtmlDocument и DOM, как в старые времена. Мир изменился, страницы стали динамическими, и мы взаимодействуем с содержимым иначе:

const string script = "document.documentElement.outerHTML";
var result = await WebBrowser.CoreWebView2.ExecuteScriptAsync(script);
var decodedHtml = Regex.Unescape(result.Trim('"'));

Остаётся понять, как извлечь доску и фигуры из HTML-страницы.

Разбор шахматной доски

В текущей реализации шахматная доска на chess.com кодируется с помощью набора div-элементов:

<div class="piece bb square-55" style=""></div>
<div class="piece square-78 bk" style=""></div>
<div class="piece square-68 br" style=""></div>
...
<div class="piece square-61 wk" style=""></div>
<div class="element-pool" style=""></div>
<div class="piece wq square-41" style=""></div>
<div class="element-pool" style=""></div>

Класс фигуры всегда начинается с "piece", первая буква из двухбуквенного кода — "w" или "b" (белая или чёрная фигура), вторая — обозначение самой фигуры ("r" — ладья, "p" — пешка, "q" — ферзь и т.д.). "square-XY" указывает координаты клетки. Например, 11 — это a1, 88 — h8.

Однако доска может быть перевёрнута, если мы играем чёрными. Это определяется по другому элементу, находящемуся чуть выше:

<wc-chess-board class="board flipped">

Наличие класса flipped говорит о том, что доска перевёрнута. Позиции всех фигур можно извлечь с помощью простого регулярного выражения. Вот так они пишутся в современном .NET:

[GeneratedRegex(@"<wc-chess-board[^>]*\bclass\s*=\s*""([^""]*)""", RegexOptions.IgnoreCase)]
private static partial Regex ChessBoardRegex();

А вот передать их в Stockfish оказалось сложнее.

Взаимодействие со Stockfish

Я скачал бинарную версию stockfish‑windows‑x86–64-avx2.exe отсюда. Установка не требуется — можно просто файл в любую папку (но не забыть прописать путь в файле конфигурации советника). При запуске открывается пустое консольное окно.

Диалог с движком всегда начинается с команды «uci». Движок отвечает информацией о себе и завершает вывод ключевым словом «uciok». Далее желательно указать количество потоков для повышения производительности (по умолчанию используется один поток) командой «setoption name Threads value XXXX». После завершения настройки отправляем команду «ucinewgame», обозначающую начало новой партии.

Затем нужно передать позицию для анализа: «position fen XXXX», где XXXX — позиция в формате FEN (об этом ниже). После этого следует подтвердить готовность командой «isready». Если всё в порядке, движок ответит «readyok».

И наконец, мы начинаем анализ командой «go» с параметрами. Я использую «go movetime XXXX», где XXXX — количество миллисекунд, отведённое на обдумывание.

примерный диалог со Stockfish в консоли
примерный диалог со Stockfish в консоли

После этого движок начинает оценивать варианты (при этом в консоль выводится множество полезной информации, включая оценку позиции), и завершает работу сообщением «bestmove XXXX» — это и есть лучший найденный ход. Именно его я отображаю в строке состояния. После этого потоки и дочерний процесс можно закрыть.

Но что за странная строка «rnbqkbnr/.../RNBQKBNR w KQkq - 0 1»?

"rnbqkbnr/.../RNBQKBNR w KQkq - 0 1" что за...???

Позиция в формате FEN состоит из описания восьми горизонталей, разделённых косыми чертами. Например, третья горизонталь может выглядеть так: «2P2N2». Цифра 2 означает две пустые клетки, далее идёт белая пешка (P — заглавная), снова две пустые клетки, белый конь (N), и ещё две пустые клетки. Чёрные фигуры обозначаются строчными буквами.

Затем следует блок информации из шести полей. Зачем они нужны, если позиция вроде бы и так понятна? На самом деле — нет. В позиции из середины партии часто не хватает дополнительных данных.

Первое поле «w» — означает, что ход белых (если «b» — то чёрных). Далее перечисляются доступные рокировки (всего четыре — «KQkq», по две на каждую сторону), либо «‑», если рокировка невозможна. Например, если король уже сделал ход, даже вернувшись обратно — рокировка уже запрещена, и это должно быть показано.

Следующее поле — возможность взятия на проходе. Она не всегда очевидна, если смотреть только на текущую позицию.

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

Последнее поле — номер полного хода (т. е. с учётом ходов обеих сторон). Честно говоря, непонятно, откуда его брать — обычная картинка доски такой информации не даёт.

В первой версии я всегда передавал «‑ - 0 1». Это, конечно, не совсем точно: рокировка никогда не предлагалась, но зато надёжно и подходит для первой реализации. Иногда получал совет делать рокировку, хотя она невозможна. Позже я это поправил (и открыл ящик Пандоры). За подробностями по FEN рекомендую обратиться к официальному описанию FEN.

Итак, мы прочитали фигуры со страницы, закодировали позицию в FEN (пока только фигуры, без дополнительной информации), передали её в Stockfish, получили «bestmove XXXX» и вывели его в строку состояния.

первый рабочий вариант
первый рабочий вариант

Задача решена? Нет.

Stockfish играет СЛИШКОМ хорошо

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

Пришлось копнуть глубже в UCI-команды. У Stockfish действительно есть настройка уровня игры (либо от 1 до 20, либо по рейтингу Эло от 1320 до 3190), но в текущей версии алгоритм достаточно простой (его легко прочитать в исходниках), и он иногда выбирает случайные, странные, а порой и абсурдные ходы. Кому интересно — вот начало этой функции на плюсах:

Skill(int skill_level, int uci_elo) {
	if (uci_elo) {
        double e = double(uci_elo - LowestElo) / (HighestElo - LowestElo);
        level = std::clamp((((37.2473 * e - 40.8525) * e + 22.2943) * e - 0.311438), 0.0, 19.0);
    }
    else {
        level = double(skill_level);
    }
    ...

Для начала я обнаружил настройку «setoption name MultiPV value N». Она говорит Stockfish вернуть не один, а несколько лучших ходов — N штук, отсортированных по убыванию силы. Кроме того, можно запросить статистику WDL («win‑draw‑loss») для каждого хода с помощью «setoption name UCI_ShowWDL value true». Это три числа, в сумме дающие 100, например, «30–60–10» — их можно использовать для примерной оценки вероятности победы или поражения. Попробуем начать с трёх лучших ходов?

три лучших хода
три лучших хода

Но трёх вариантов мне оказалось мало.

Хочу видеть все ходы

Три хода разной силы — уже лучше, чем один. Но если мы всё равно получаем оценку каждого хода, почему бы не задать желаемый уровень в настройках, например, +3.00 (выигрываем легкую фигуру) или -1.00 (отдаём пешку)? Заодно можно раскрасить ходы в разные цвета — от красного до зелёного.

private static Color InterpolateColor(Color color1, Color color2, double factor)
{
    factor = Math.Max(0, Math.Min(1, factor));
    var a = (byte)(color1.A + (color2.A - color1.A) * factor);
    var r = (byte)(color1.R + (color2.R - color1.R) * factor);
    var g = (byte)(color1.G + (color2.G - color1.G) * factor);
    var b = (byte)(color1.B + (color2.B - color1.B) * factor);
    return Color.FromArgb(a, r, g, b);
}
...
normalizedValue = (double)Math.Min(500, scoreValue) / 500;
return InterpolateColor(Colors.Gray, Colors.Green, normalizedValue);

И вместо того чтобы показывать только топ-3, давайте отобразим все ходы!

вот теперь видны все ходы - от хороших до плохих
вот теперь видны все ходы - от хороших до плохих

Чтобы не раздражали нелепые дебютные ходы, я собрал книгу дебютов из разных источников в единый .csv-файл (относительно небольшой — около 3000 позиций). Если ход встречается в теории — можно показать его название.

сразу видно, что нам известна теория
сразу видно, что нам известна теория

Вот только выглядело там все не так просто. Дебютная книга - это не ходы, а позиции:

rnbqkbnr/pppppppp/8/8/2P5/8/PP1PPPPP/RNBQKBNR,English Opening
rnbqkbnr/ppp1pppp/8/3p4/2P5/5N2/PP1PPPPP/RNBQKB1R,Reti Opening

Чтобы понять, чтобы вот этот вот ход ведет к дебютной позиции, его надо сделать. К счастью, Stockfish умеет делать ходы в «уме»: «position fen moves move» и «d». Берем текущую позицию, делаем ход, получаем новую позицию, и вот ее уже ищем в книге дебютов:

await inputWriter[0].WriteLineAsync($"position fen {currentFen} moves {move.FirstMove}");
await inputWriter[0].FlushAsync();
await inputWriter[0].WriteLineAsync("d");
await inputWriter[0].FlushAsync();
var newFen = string.Empty;
while (await outputReader[0].ReadLineAsync() is { } rawline) {
    if (!rawline.StartsWith("Fen:")) {
        continue;
    }
    newFen = rawline[5..].Trim();
    break;
}

Почему только chess.com?

На этом этапе я похвастался проектом среди знакомых. Один заметил, что на chess.com при игре против бота уже есть встроенные подсказки. А вот настоящие профи играют на lichess.org — и там таких подсказок нет. Пришлось разобраться, как устроена доска на этом сайте.

<cg-board>
<piece class="black rook" style="transform: translate(0px, 0px);"></piece>
<piece class="black bishop" style="transform: translate(174px, 0px);"></piece>
<piece class="black queen" style="transform: translate(174px, 87px);"></piece>
<piece class="black king" style="transform: translate(522px, 0px);"></piece>

Логика в остальном остаётся прежней. Вся доска находится внутри <cg‑board>, только регулярные выражения надо использовать другие. Итог — можно свободно переключаться между сайтами в любой момент.

играем, где хотим
играем, где хотим

Мне нужен советник, а не бот

Кажется, всё работает, но не так, как я изначально хотел. Я решил делать ходы сам, а от помощника мне нужно только быстрое подтверждение: «не зевнул ли я чего‑нибудь серьёзного». Искать свой ход среди разноцветных полос утомительно. Поэтому я решил сгруппировать ходы по фигурам:

var groups = _moves
	.Values
	.OrderByDescending(move => move.Score)
	.GroupBy(move => move.FirstMove[..2])
	.Select(group => group.ToArray())
	.OrderByDescending(list => list.First().Score)
	.ToArray();
красиво, но рябит в глазах
красиво, но рябит в глазах

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

Так я пришел к текущему интерфейсу. В нем быстро можно проверить свой ход на «вшивость».

сразу видно, какими фигурами можно ходить и куда
сразу видно, какими фигурами можно ходить и куда

Ходы, близкие к заданному уровню (например, +5.00 на скриншоте), показываются полностью. Остальные — в виде полос (просто все не помещались). Требуемый уровень игры можно менять, выбирая нужное значение справа от шкалы — подходящие ходы обновятся автоматически. Либо можно просто кликнуть по интересующему ходу, и уровень подстроится под него.

оп-па, мы можем заматовать!
оп-па, мы можем заматовать!

Здесь (вверху) мы решили понемногу проигрывать, задав уровень -1.50. Неудивительно, что все ходы стали красными… кроме одного зелёного. Он показан как оптимистичная зелёная полоска, и если навести на неё курсор — можно увидеть, что в нашей проигранной позиции есть возможность поставить мат в четыре хода.

Но всё же читать нотации и мысленно переносить h5f7 на доску утомительно. Было бы здорово нарисовать ход прямо на доске…

Рисование хода

Сначала мне казалось, что это будет просто: разместить Canvas поверх элемента WebBrowser, получить смещение доски относительно левого верхнего угла окна через JavaScript-функцию и рисовать всё, что угодно — линии, текст и так далее. Но не сработало. WebView2 использует аппаратное ускорение и DirectComposition для рендеринга, что сильно осложняет интеграцию с системой отрисовки WPF. Получилось лишь наложить поверх браузера отдельное окно без рамки и заголовка, которое отслеживает все движения основного окна, сворачивание, перекрытия… Это был кошмар, и работало всё нестабильно.

Тогда я решил пойти другим путём — рисовать свои элементы прямо на самой веб-странице, как это делает chess.com. То есть, рисовать стрелку как SVG-полигон и вставлять её напрямую в код страницы с помощью JavaScript.

var x1 = (src[0] - 'a') * 12.5 + 6.25;
var x2 = (dst[0] - 'a') * 12.5 + 6.25;
var y1 = ('8' - src[1]) * 12.5 + 6.25;
var y2 = ('8' - dst[1]) * 12.5 + 6.25;
if (!isWhite) {
	x1 = 100.0 - x1;
	x2 = 100.0 - x2;
	y1 = 100.0 - y1;
	y2 = 100.0 - y2;
}
var dx = x2 - x1;
var dy = y1 - y2;
var angle = Math.Round(Math.Atan2(dx, dy) * (180.0 / Math.PI), 2);
var length = Math.Round(Math.Sqrt(dx * dx + dy * dy), 2);
const double headRadius = 1.5;
var point1X = x1 + headRadius;
var point2Y = y1 - length + headRadius * 2;
var point3X = x1 + headRadius * 2;
var point4Y = y1 - length;
var point5X = x1 - headRadius * 2;
var point6X = x1 - headRadius;
var points = $"{point1X},{y1} {point1X},{point2Y} {point3X},{point2Y} {x1},{point4Y} {point5X},{point2Y} {point6X},{point2Y} {point6X},{y1}";
var svgElement = $"<svg viewBox='0 0 100 100'><polygon transform='rotate({angle} {x1} {y1})' points='{points}' style='fill: rgb(255, 255, 0); opacity: 0.7;' /></svg>";

Получилось довольно симпатично:

наша первая стрелка
наша первая стрелка

Но есть один минус — стрелка не исчезает автоматически, если соперник делает ход. Нужен механизм, который будет её удалять при любом изменении на доске. Например, MutationObserver. Добавляем стрелку, включаем MutationObserver, он срабатывает (например, если мы или противник двигаем фигуру) — и стрелка исчезает. На деле она исчезает даже при начале нашего хода, поскольку перетаскивание фигуры мышкой уже считается изменением DOM.

window._disableArrowObserver = false;
window._chessBoardObserver = new MutationObserver(function(mutations){{
    if(window._disableArrowObserver){{
        return;
    }}
    mutations.forEach(function(mutation){{
        if(mutation.type === 'childList' || mutation.type === 'attributes'){{
            removeArrow();
            window._disableArrowObserver = true;
        }}
    }});
}});
if(chessBoard){{
    window._chessBoardObserver.observe(chessBoard, {{
        childList: true,
        attributes: true,
        subtree: true
    }});
}}

Улучшение на 10% требует 90% усилий

Осталась одна проблема, которую я оставил на закуску. Проблема в том, что позиция FEN, которую я использую, неточная — в конце я всегда указывал "KQkq - 0 1". Но ведь может быть взятие на проходе, может быть уже потеряно право на рокировку, неизвестно, сколько полуходов прошло без движения пешек... Без всей этой информации Stockfish будет анализировать позицию с ошибкой в каком-то проценте случаев (и действительно наблюдал предложения сделать фантомные, невозможные рокировки). Я даже не предполагал, насколько сложной окажется эта «мелкая» задача.

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

Но на странице всё же есть история ходов. Она есть и на chess.com, и на lichess.org, но в разном формате.

запись партии справа от доски
запись партии справа от доски

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

<i5z>8</i5z><kwdb class="">Nf3</kwdb><kwdb class="">Nc6</kwdb>
<i5z>9</i5z><kwdb class="">Be2</kwdb><kwdb class="">a6</kwdb>
<i5z>10</i5z><kwdb class="">Nbxd4</kwdb><kwdb class="">Bc5</kwdb>
<i5z>11</i5z><kwdb class="">c3</kwdb><kwdb class="">O-O</kwdb>
<i5z>12</i5z><kwdb class="">Bd3</kwdb><kwdb class="">Ne7</kwdb>

Вроде бы просто — создаём массив 8x8, где символы обозначают фигуры, и двигаем их по мере поступления ходов… Но для этого нужно знать, откуда пошла фигура. А в SAN-нотации (стандартной шахматной записи) этой информации нет. Перевести ход вроде "Bd3" в "c1d3" (формат UCI) — задача нетривиальная: бывают неоднозначности, когда несколько фигур могут пойти в одну и ту же клетку. Приходится учитывать дополнительные символы SAN. А ещё есть рокировки, шахи, взятия и другие нюансы.

К сожалению, сам Stockfish не умеет переводить SAN в UCI. Либо нужно писать собственный шахматный движок, либо использовать стороннюю библиотеку.

Я нашёл отличную шахматную библиотеку — Geras1mleo (60 звёзд на GitHub — и одна от меня) — и начал разбираться в исходниках. Сначала хотел использовать её как есть, но потом решил подправить под свои нужды. Тем более, 80% кода мне не понадобились (печать, парсинг FEN и PGN, преобразование UCI в SAN, проверка корректности хода). Зато не хватало возможности вручную задать фигуры на доске. А мне нужно и то, и другое — и восстановление позиции из истории ходов, и ручная расстановка (например, при решении задач).

Повторюсь, библиотека отличная. Некоторые тонкости FEN стали мне понятны именно после изучения её кода. Я использовал её частично, сохранив имена некоторых функций. Но в итоге написал почти 1000 строк собственного кода и покрыл всё юнит-тестами. Добавление расчёта точной FEN-позиции заняло семь вечеров. Там было много багов, особенно при взятиях на проходе и превращениях пешек.

private bool IsValidEnPassant(Move move, int v, int h)
{
    if (move.Piece == null) {
        return false;
    }

    if (Math.Abs(v) == 1 && Math.Abs(h) == 1) {
        var piece = GetPiece(new Position(move.To.Y - v, move.To.X));
        if (piece != null && piece.Color != move.Piece.Color && piece.Type == 'p') {
            var lastMove = LastMoveEnPassantPosition();
            return lastMove.X == move.To.X && lastMove.Y == move.To.Y;
        }
    }

    return false;
}

Зато теперь у нас есть настоящая запись текущей позиции, которая так упорно прячется от игрока:

наконец-то она!
наконец-то она!

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

"kq - 2 12" здесь означает, что только чёрные могут сделать рокировку в обе стороны (kq), взятие на проходе недоступно (-), два полухода прошло без движения пешек, и всего сыграно 12 полных ходов. Теперь анализ позиции в Stockfish стал на 100% точным.

теперь все точно - и рокировки и взятия на проходе
теперь все точно - и рокировки и взятия на проходе

Всё выглядит отлично, но меня попросили добавить ещё одну функцию…

Противник — читер?

Раздражает, когда противник пользуется подсказками. Это можно определить так: откатываемся на ход назад, анализируем позицию со стороны соперника и оцениваем его ход. Если это всегда лучший или один из лучших ходов — повод задуматься. По оценкам, даже у лучших игроков мира с рейтингом 2800 эффективность составляет 0.8–0.9, то есть каждый пятый-десятый ход не является оптимальным — и это нормально.

Поэтому к нашей жёлтой стрелке я добавляю ещё две: лучшая возможная подсказка для соперника по мнению Stockfish и фактически сделанный им ход. Примерно вот так:

противник сыграл слоном, а надо было пешку бить
противник сыграл слоном, а надо было пешку бить

Зелёная стрелка с надписью "BEST" означает лучший ход соперника (например, e5f4) если бы он его сделал, а вторая — серая, с надписью "-0.52" — это тот ход, который он на самом деле сделал. Вторая стрелка, сделанный ход, окрашивается в зависимости от силы хода: зелёная — хороший ход (если соперник делает только зелёные — он почти наверняка жульничает), серая — слабый ход, красная — зевок.

противник сыграл пешкой, а зря
противник сыграл пешкой, а зря

Числа внутри стрелки помогают оценить силу хода. Если около -1, значит, ход плохой — соперник зевнул пешку или упустил инициативу, -3 — зевнул лёгкую фигуру и т.д. Большие отрицательные значения говорят о том, что соперник не заметил быстрый мат с нашей стороны.

Стрелки, и наши, и противника рисуются внутри svg:

div.innerHTML = `<svg viewBox='0 0 100 100'>{_opponentArrow}{playerArrow}</svg>`;

Для честного игрока нормально делать микс из зелёных, серых и иногда красных ходов. Читер делает только зелёные. Видел я такого. Рейтинг 1300, играет одними зелеными ходами.

Все хорошо, но...

Медленно!

Мы сначала анализируем позицию со стороны противника, потом с нашей... В текущей версии я запускаю сразу два процесса Stockfish — один для позиции соперника, второй — для своей, и показываю результаты их работы одновременно. Скорость анализа сразу выросла вдвое.

for (var i = 0; i < 2; i++) {
    stockfish[i] = new Process {
	StartInfo = new ProcessStartInfo {
	    FileName = _stockfishPath,
	    RedirectStandardInput = true,
	    RedirectStandardOutput = true,
	    UseShellExecute = false,
	    CreateNoWindow = true
	}
    };
    stockfish[i].Start();
    ...

Немного моральных терзаний

Я сделал эту штуку (и делюсь ею бесплатно) просто ради удовольствия. Напоминаю, что использование читов — нечестно и несправедливо по отношению к вашему противнику. С другой стороны, этот помощник позволяет уравнять силу игры, если соперники играют на разном уровне. Партии становятся интересными.

Если у противника рейтинг, скажем, 1800, вы можете задать уровень игры +0.50 — и будете играть на уровне примерно 1900–2000. Конечно, делать это стоит только с согласия и по договорённости с соперником. Например, в обучающих целях.

Ссылки

Где проект?

Тут: https://github.com/wmlabtx/chezzz

Там же можно скачать свежую portable сборку. Или собрать самому.

Хотите отблагодарить? Кофе не нужно, поставьте ⭐ на GitHub.