все заметки

Шахматный конкурс от Yandex Cloud

2025.01.22 (редактировано: 2025.02.04)
Обновление2025-02-04: Итоговый топ значимо не изменился. Естественно запрос в поддержку отправленный неделю назад (28.02) по поводу формулы остался без ответа

Несколько слов о шахматном конкурсе от Yandex Cloud. Он завершился, а результаты будут 27 января.

На старте конкурса пару раз играл, но так как не умею, то никакого результата и не было. Первое место было у Public Name с каким то рейтингом 3xxx.

И вот неделю назад пришло время вернуться. Рейтинг перестроился, первые три места с одинаковыми очками (5726).

Не столько важно (да-да, я проиграл, и решения у меня нет), важно поэкспериментировать с ботом и движками Stockfish 17 и Leela Chess Zero (lс0). Это обычные консольные считалки ходов. Хорошо просчитывают, качественно.

Но для того чтобы коммуницировать с ними обычное используют GUI и протокол UCI (Universal Chess Interface). А хочется иметь прямой доступ к движку. Послать ход, получить ход.

Сюда врывается превосходный websocketd (для многих OS, в том числе Windows), который создает WebSocket сервер для любого консольного (STDIN/STDOUT) приложения, например cmd:

websocketd.exe --port=9999 cmd.exe
И на порту 9999 через websocket доступен интерпретатор командной строки.

О работе бота

  1. Запускам движок websocketd --port=9999 stockfish
  2. Шлем ему ws.send('position startpos'); ws.send('go nodes 10000');
  3. Получаем от него bestmove <ход>
  4. Засылаем ход в API yandex.cloud/api/chess/moveHuman
  5. Полученный в ответе FEN (текущая расстановка на доске) засылаем движку ws.send('position fen ' + fen); ws.send('go nodes 10000');
  6. Переходим в пункт 3 пока game_status == 'ongoing'

Из забавного. В возвращаемых данных от moveHuman в последнем элементе массива analysis_history есть подсказка для нас, для людей, какой ход будет для нас более выйгрышным: Move.from_uci('g1f3'). По сути это тот же bestmove.

А еще забавнее, что с этими данными получаеся самый быстрый мат.

Бот с данными от Stockfish и lc0 великолепно отрабатывает, постоянные победы. Но очки далеки от топа.

Рейтинг

Возможно стоит почитать правила? Да ну, бред какой-то. Но почитаем.

Пункт про электронных помощников не интересный.

А вот подсчет рейтинга интересный:

Очки (ход или за съестное) * 5 (победа, другое нас не интересует) * 50 / количество ходов.

За каждый ход дается один балл. За поедание такая картина, всего 380:

45 30 30 90    30 30 45\n10 10 10 10 10 10 10 10

Разбор баллов

  1. Леонид Захаров 5726
  2. Maksim S. 5726
  3. Sergey AttackingRU 5726
  4. Игорь Мелекесцев 5450
  5. Dmitrii Bezrukov 5078
  6. Дмитрий Барабанов 4983
  7. Игорь М. 4857
  8. Савин Е.А. 4826
  9. Дмитрий Б. 4756
  10. Software Helicon 4698

Так, максимально и максимально популярно 5726. Что нужно сделать, чтобы заработать столько очков? У нас есть формула, поэтому сгенерим все варианты:

var board = [];\nfor (let i = 1, steps = 50; i <= steps; i++) {\n\tfor (let j = 1; j <= 380 + steps; j++) {\n\t\tboard.push(j + ', ходов: ' + i + ' | ' + (j * 5 * 50 / i));\n\t}\n}

Оке, ищем 5726. Ииии... Нет такого варианта. То есть, математика не знает как по этой формуле получить 5726.

4-8 - имеется, а вот 9 и 10 - отсутствуют.

Проверил все мои партии и рейтинг моих соседей, все имеются в сгенерированном списке. Так же видно, что итоговый подсчет округляется в меньшую сторону (floor). А значит никаких -1.

Какой-то косяк. Ну да ладно. Дождемся итоговых результатов.

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

function eaten(score, steps) {\n\t// очень неочень функция, много дублей\n\tfunction powerset(arr, prefix=[], i=0){\n\t\tif (i === arr.length)\n\t\t\treturn [prefix];\n\n\t\treturn powerset(arr, prefix.concat(arr[i]), i + 1)\n\t\t\t.concat(powerset(arr, prefix, i + 1));\n\t}\n\n\t// очки\n\tvar points = [\n\t\t45, 30, 30, 90,     30, 30, 45,\n\t\t10, 10, 10, 10, 10, 10, 10, 10,\n\t];\n\t\n\t// генерируем все вохможные варианты съестных очков\n\tvar possible = powerset(points);\n\n\t// несъестное\n\tvar turns = [];\n\tfor (let i = 1; i <= steps; i++) {\n\t\tturns.push(1);\n\t}\n\n\tvar data = [];\n\n\tfor (let i = 0; i < possible.length; i++) {\n\t\tif (turns.length - 1 > possible[i].length) {\n\t\t\t// соединяем съеденные с пустыми ходами\n\t\t\tlet s = Object.values(Object.assign({}, turns, possible[i]));\n\t\t\ts = eval(p.join('+'));\n\t\t\t// если в score очки за ходы\n\t\t\tif (s === score) {\n\t\t\t\tdata.push(possible[i]);\n\t\t\t}\n\t\t\t// если в score итоговые баллы\n\t\t\telse if (Math.floor(s * 5 * 50 / steps) === score) {\n\t\t\t\tdata.push(possible[i]);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn data;\n}\n

Анализируем какие очки были получены:

5450
  • eaten(109, 5) = пусто
  • eaten(218, 10) = пусто
  • eaten(327, 15) = [45, 30, 30, 90, 30, 30, 10, 10, 10, 10, 10, 10, 10]
5078
  • eaten(325, 16) = [45, 30, 30, 90, 30, 45, 10, 10, 10, 10, 10]
  • eaten(386, 19) = пусто

3134 Мой результат полученный с помощью lс0 имеет два варианта, но в данном случае был второй:

  • eaten(163, 13) = [30, 30, 30, 10, 10, 10, 10, 10, 10, 10]
  • eaten(326, 26) = [45, 30, 30, 90, 30, 45, 10, 10, 10, 10]

И что нам это дает? Только некоторое представление о партии. Потому, что бот не тупой, и мало съесть эти фигуры, надо еще поставить ему мат.

Короче, я не решил эту задачу.

Итого

За эту неделю узнал много интересного. А шахматный софт превосходен (lc0 > stockfish 17).

Но понял одно: все онлайн шахматные конкурсы в наше время неактуальны.

Быстрые маты

750 от яндекса яндексу (9 ходов и две пешки):

  • eaten(27, 9) = [10, 10]

1650 lc0 в 10 ходов:

  • eaten(66, 10) = [30, 10, 10, 10]

1235 Stockfish 17 (17 ходов):

  • eaten(84, 17) = [30, 30, 10]

Таблица лидеров для истории

Таблица лидеров