Skip to content
Все статьи
ИИ8 мин

Как мы учили нейросеть играть в шашки для Damka.uz

Damka.uz — наш собственный продукт, онлайн-платформа для русских шашек. За два года мы трижды переписали её движок. Рассказываю, почему, и что из этого стоило делать, а что нет.

Azamat AbduyaminovОснователь, ведущий инженер

В первой версии Damka в репозитории лежал файл TODO.md, и первым пунктом там было написано примерно следующее: бот использует минимакс, глубина поиска стоит 3, потому что Python медленный, и это не сильный соперник. Дальше шли два варианта решения: взять готовый движок на C или Go, или сделать ИИ, который учится сам.

Мы в итоге сделали оба. Только на это ушло два года и три архитектуры.

Зачем студии свои шашки

Шашки в Узбекистане играют везде: во дворах, в чайханах, в Telegram-группах. Русские шашки на доске 8×8 с обязательным взятием и «дамкой», которая ходит на любое расстояние. При этом нормальной онлайн-площадки на узбекском у нас не было. Мы решили сделать Damka.uz как свой продукт: играть онлайн, против друга по ссылке и против бота.

Для студии это ещё и полигон. На клиентских проектах экспериментировать с WebSocket-нагрузкой и Rust никто не разрешит. На своём можно.

Три версии за два года

  1. 2024
    v1: Django + Nuxt 3. Партии по WebSocket через Django Channels, правила из библиотеки pydraughts, бот на минимаксе с альфа-бета-отсечением, глубина 3.
  2. Начало 2026
    v2: Django 6 + Channels + Nuxt 4. Собственный движок поиска на negamax, матчмейкинг, ELO, наблюдатели, полоса оценки позиции для зрителей.
  3. 2026
    Параллельно: обучение оценочной нейросети в отдельном движке на Go, отдельно от продакшена.
  4. Сейчас
    v3: игровой сервер на Rust (actix-web, WebSocket) с движком и нейросетью внутри, фронт на React, админка на Next.js, бот @DamkaUzBot для игры в группах.

Версия 1: работает, но бот слабый

Первая версия была честной MVP. Партия шла по WebSocket, библиотека pydraughts проверяла правила, бот перебирал ходы минимаксом. Глубина 3 означает, что бот смотрит на полтора хода вперёд с каждой стороны. Любой, кто играет хотя бы во дворе, его обыгрывал. Поднять глубину до 5–6 на Python означало ждать ответа бота по несколько секунд.

Версия 2: свой движок и первая нагрузка

Во второй версии мы выяснили неприятную вещь: встроенный движок библиотеки на русской доске 8×8 работал неправильно. Пришлось писать поиск с нуля: negamax с альфа-бета-отсечением, битборды для оценки, таблица транспозиций, итеративное углубление с бюджетом времени. Всё это выполнялось в пуле процессов, чтобы одна партия с ботом не блокировала остальные.

Заодно появились матчмейкинг на отсортированном множестве в Redis, рейтинг ELO и режим наблюдателя с живой полосой оценки, как на chess.com.

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

Где на самом деле было медленно

Мы прогоняли реальный сценарий: игроки подключаются, находят соперника, делают ходы. Сервер ограничили до размеров типичного VPS на 2 ядра. При 300 одновременных партиях медианная задержка хода была 6,7 секунды, и почти все партии падали с ошибками.

Первая мысль была: Python медленный, переписываем. Но процессор при этом был загружен на 50%. Сервер не был занят, он стоял в очереди.

Профилирование нашло три причины. Один процесс Python упирался в GIL и занимал одно ядро из двух. ORM-запросы выполнялись по одному в одном потоке. И соединения с базой открывались заново на каждый запрос, плюс матчмейкинг сканировал ключи Redis через блокирующий KEYS.

6 700 мс
медианная задержка хода до исправлений (300 партий, 2 ядра)
17 мс
та же задержка после трёх исправлений
~100
одновременных партий комфортно держит сервер на 2 vCPU
~350
на 6 vCPU, рост почти линейный

Несколько воркеров uvicorn, конкурентный доступ к базе и постоянные соединения дали задержку в 17 мс без единой строчки на другом языке. Если бы мы тогда сразу ушли в Rust, мы бы так и не узнали, что проблема была не в языке.

Почему всё-таки Rust

Если Python справился с нагрузкой, зачем было переписывать? Из-за бота.

Сервер игр на Python нормально перекладывает сообщения. Но поиск по дереву ходов — это миллионы позиций в секунду, и здесь интерпретатор проигрывает компилируемому языку в десятки раз. Вариант «движок на Rust, сервер на Python через FFI» мы обсуждали. Получалось два языка, два деплоя и граница между ними, через которую каждый ход нужно сериализовать.

Мы решили, что движок и сервер будут в одном Rust-бинарнике. Сервер подключает движок как обычный Cargo-крейт, и проверка хода стоит как вызов функции. Тот же движок компилируется в WebAssembly, и браузер может сам показывать допустимые ходы, не спрашивая сервер.

Доска в движке занимает 16 байт: два u64. В них помещаются очередь хода, 32 игровые клетки по 3 бита и счётчик ходов одними дамками для правила ничьей.

Нейросеть вместо ручной оценки

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

В v3 позицию оценивает небольшая полносвязная нейросеть. Она обучалась отдельно, в движке на Go, а в Rust мы перенесли уже готовые веса.

// Вход: 32 клетки, на каждой значение от -K до +K
// (шашка = ±1, дамка = ±K, пусто = 0).
// Скрытые слои с tanh, на последний слой
// добавляется простая сумма материала.
pub const LAYERS: [u16; 4] = [32, 40, 10, 1];
pub const WEIGHTS_LEN: usize = 1280 + 400 + 10;

Сеть маленькая намеренно. Её вызывают на каждом листе дерева поиска, и большая сеть съела бы всю выигранную скорость. В движок вшито десять обученных вариантов сети, всего около 140 КБ. Разные варианты дают разный характер игры, и это удобно для уровней сложности.

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

206 КБ
размер движка в WebAssembly
715
позиций в тестовом наборе для сверки с эталоном
10
обученных вариантов нейросети внутри движка
5
дамок максимум в таблице эндшпиля

Как мы проверяли, что ничего не сломали

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

Этот набор нашёл реальную ошибку, только не в новом коде, а в эталоне. В русских шашках, если взятие можно продолжить, его нужно продолжить, и это касается и простых шашек, и дамок. В эталонном движке флаг продолжения терялся, и для дамки иногда генерировалось короткое взятие, которое правила не разрешают. В Rust-версии правило работает правильно, а расхождение записано в тестах отдельно.

Шашки в Telegram

Отдельная история — бот @DamkaUzBot. Мы заметили, что люди не хотят «заходить на сайт поиграть». Они сидят в групповом чате и хотят сыграть прямо там. Бот позволяет начать партию в группе и проводить групповые турниры по правилам IDF: жеребьёвка пар, туры, тай-брейки по Бухгольцу. Такие боты мы делаем и для клиентов, подробнее на странице разработки Telegram-ботов.

v1 v2 v3
Сервер Django + Channels Django 6 + Channels Rust, actix-web
Фронт Nuxt 3 Nuxt 4 React
Оценка позиции Материал и подвижность Ручная функция Нейросеть
Глубина бота 3 По бюджету времени По бюджету времени
Эндшпиль Нет Нет Таблица до 5 дамок

Что бы я сделал иначе

Первое: профилировал бы раньше. Мы потеряли несколько недель на разговоры о том, что «Python не тянет», хотя он тянул. Проблема была в одном процессе и в одном потоке к базе.

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

Третье, и тут я до сих пор сомневаюсь: возможно, не стоило переписывать на Rust весь сервер, а только движок. Сейчас у нас один бинарник и его удобно деплоить, это плюс. Но команде, которая годами пишет на Django, порог входа в Rust-сервер высокий. Если бы Damka был клиентским проектом с поддержкой на годы, я бы оставил сервер на Python. Подробнее о том, как мы вообще выбираем стек, я писал в статье про Nuxt, Next.js и Rust.

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

Есть задача? Напишите — обсудим.

Пишите в Telegram напрямую основателю студии. Расскажите в двух словах, что нужно — ответим с вопросами и примерной оценкой.