Проектные соображения — в «Книга дебютов для роботов». Здесь только то, что построено и измерено (07.08.2026): реальный формат, точная семантика фильтров по коду и по данным, и замеренное поведение на живой лестнице. Первый потребитель — azure/hunter-book (см. раннбук).
Формат и ключ
TSV, одна строка на запись, разделитель — табуляция (мигрировали с JSON в engine#573):
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - BPR e2e4,f1c4
Первые четыре поля — канонический normalized_fen (Dfen.normalizedFen), байт-в-байт тот же, что колонка normalized_fen в аналитике. Счётчики полуходов и ходов намеренно отброшены: книга от них не зависит, а без них позиции, различающиеся только счётчиком, делят одну запись. En passant канонизирован — оставлен, только если реально бьётся.
<dice> — буквы выпавших фигур (p n b r q k), отсортированные, в регистре стороны хода: заглавные у белых, строчные у чёрных. Совпадает с колонкой dice_sorted. Пример для белых с слоном+пешкой+ладьёй: BPR.
Значение — один ход как список микро-ходов через запятую. OpeningBookBot сопоставляет его с легальными путями по мультимножеству, поэтому порядок в файле не важен, а нереализуемая запись (устаревшая или битая) молча игнорируется в пользу поиска, а не играется нелегально.
Любое изменение формата — кросс-репозиторный слом
Ключ производят два места независимо: OpeningBook.key в движке (потребитель) и экспортёр аналитики (производитель). Расхождение в один байт означает книгу, которая никогда не срабатывает, — и ничего при этом не падает.
Как экспортируется
ExportOpeningBookApp [minGames] [minRating] [outputPath]# по умолчанию: 100, 2000, opening_book.tsv
Прогон, из которого собрана боевая книга: minGames=100, minRating=2000 → 335 записей, 23 КБ.
Что означают пороги — по коду, не по описанию
minRating=2000 — оба игрока, и NULL отсекается заодно. В SQL буквально:
g.white_rating >= 2000 AND g.black_rating >= 2000
Не «хотя бы один» и не «средний». Побочный эффект, который стоит держать в голове: NULL >= 2000 в SQL даёт NULL, а не true, поэтому партии без записанного рейтинга отсекаются тем же условием.
minGames=100 — это порог на связку «позиция + конкретный бросок», не на позицию. GROUP BY normalized_fen, dice_sorted, position_after HAVING count(*) >= 100. С учётом того, что на позицию приходится 56 возможных мультимножеств костей, это гораздо более жёсткое условие, чем кажется, — и главная причина, почему книга такая маленькая.
Отбор хода внутри ключа:maxBy (winRate, число партий) — берётся линия с лучшим win-rate, при равенстве более сыгранная. Форсированные пасы (played_moves пуст) отброшены: пас не бронируемый ход.
Сколько корпуса реально участвует
партий
доля
Всего в базе
1 456 549
100%
Оба рейтинга проставлены
1 122 354
77%
Оба ≥ 2000 — статистическая часть книги
214 107
14.7%
Для сравнения, оба ≥ 1800
271 876
18.7%
Кто эти игроки — партии, прошедшие фильтр, по типам:
dicechess.com human vs human 213 675 (99.8%)dicechess.com bot vs human 420dicechess.com bot vs bot 14
Практически чистая выборка игр людей против людей. Ровно то, ради чего порог и ставился: книга отражает игру сильных людей, а не то, как боты играют друг с другом на лестнице. Заодно это автоматически исключает самоподкрепление — собственные ладдерные партии наших ботов в книгу не попадают, потому что у них либо нет рейтинга, либо он ниже 2000.
Но 54 записи из 335 фильтр вообще не проходили
favoritesEntries — это SELECT ... FROM opening_book_favorites WHERE cardinality(played_moves) > 0, без minRating и без minGames. Все курируемые записи попадают в книгу безусловно и переопределяют статистический выбор для того же ключа:
Значит из 335 записей боевой книги: 281 статистическая (люди 2000+, ≥100 партий на связку) и 54 курируемые (ручной выбор, любые пороги обойдены). Это ~16% книги. Проверено на экспортированном файле: курируемые ключи вроде … w KQkq - NNN → b1a3,a3b5,b5d6 в нём присутствуют.
Само по себе это фича, а не баг — курирование для того и заведено. Но фраза «книга собрана из партий игроков 2000+» верна только для 84% записей, и об этом легко забыть.
Замерено на живой лестнице
azure/hunter-book, первый день:
первые 2 часа
накопительно
Ходов всего
60
432
Из книги
5
32
Доля попаданий
8.3%
7.4%
Посчитано поиском
55
400
Ошибок / усечений по дедлайну
0
0
Как это считается без отдельной телеметрии. Книжный ход не вызывает поиск, поэтому diagnosticsSink не срабатывает, и строка лога печатает legalTurns=n/a. То есть доля = отношение grep -c "legalTurns=n/a" к общему числу строк [hunter] turn. Это и было целью рефакторинга на sink (dicechess-hunter#23): отличать «решила книга» от «решил поиск» без единой новой строки кода.
7.4% — это примерно 0.4 книжных хода на партию, то есть книга срабатывает где-то в каждой третьей партии. Тоньше, чем предполагала проектная заметка («первые 1–3 хода главных линий»). На 432 ходах оценка уже устойчивая — за первые два часа было 8.3%, разница в пределах шума малой выборки.
Причина — ветвление по костям. Ключ включает бросок, на позицию их 56 вариантов, и порог «100 партий на связку» проходит совсем небольшое их число. Растить тут надо не корпус, а снижать пороги.
Рычаги, если нужно больше покрытия
Прогнано на текущем корпусе (07.08.2026, 1 456 549 партий):
Пороги
Записей
Против боевых
g100 / r2000 (боевые)
335
—
g100 / r1800
422
+26%
g50 / r2000
735
+119%
g50 / r1800
906
+170%
Заметно, что порог по числу партий двигает покрытие сильнее, чем порог по рейтингу: снижение minGames со 100 до 50 даёт +119%, а снижение рейтинга с 2000 до 1800 — только +26%. Логично: узкое место — ветвление по костям, а не дефицит сильных игроков.
Обмен честный и его стоит назвать вслух: вдвое больше покрытие, но за каждым ходом вдвое меньше партий. Что из этого лучше играет — не измерено; чтобы узнать, нужен парный прогон двух книг, а не выбор на глаз.
Числа порогов устаревают вместе с корпусом
В июле те же пороги давали 245 / 304 / 580 / 686 — корпус с тех пор вырос, и все четыре выросли вместе с ним. Если сверяешься с этой таблицей — сначала посмотри на дату.
Что из проекта дошло, а что нет
Сверка с проектной заметкой — полезна тем, что показывает, где реализация разошлась с замыслом:
Проектное предложение
Как вышло
Ключ = позиция × бросок
✅ ровно так
min_rating ≥ 2000, оба игрока
✅ ровно так
Явно исключить партии ботов
⚠️ отдельного фильтра нет — цель достигнута побочно, через рейтинг (99.8% людей)
Хранить топ-K с win% и числом партий
❌ в книге бота один ход на ключ; статистика остаётся в каталоге, но до бота не доезжает
Немного рандома среди близких линий
❌ не сделано — бот детерминирован в дебюте и в принципе выучиваем
JSON
❌ TSV (engine#573)
Валидация ареной (с книгой против без)
✅ сделана 10.08.2026, 12 000 партий — см. ниже
Незакрытым остался один пункт: рандомизации среди близких линий нет, поэтому бот в дебюте детерминирован и в принципе выучиваем. Парный замер теперь есть, и он снял главную неизвестность — помогает ли книга вообще.
Замерено: с книгой против без
Парная дуэль hunter-all против себя же: одна сторона с книгой, вторая без, зеркальные кости, контроль 3+2.
Партий
12 000 (6 000 зеркальных пар)
W/L/D
6104/5873/23
Win rate со стороны книги
50.96%
ci95
±0.73 pp → [50.23%, 51.69%]
Поражений по времени
0 / 0
Книга даёт +0.96 pp, то есть примерно +7 elo. Оценка сошлась: на 7 000 партий было 51.11%, на 9 000 — 51.12%, на 12 000 — 50.96%.
Это ровно тот масштаб, которого стоило ожидать от замеренных выше 7.4% попаданий (~0.4 книжных хода на партию). Книга работает, но рычагом не является — ширина поиска двигает силу на порядок сильнее.
Итог честен только вместе с разбивкой по половинам
Шарды стояли на разных архитектурах, а таймированные замеры машинно-зависимы:
партий
win rate
ci95
сам по себе
x86 (D2as_v5)
4 000
50.60%
±1.27 pp
не значим
ARM (D2ps_v5)
3 000
51.80%
±1.47 pp
значим
Половины расходятся на 1.20 pp при стандартной ошибке разности 0.99 pp (p ≈ 0.23) — то есть не противоречат друг другу, и объединять законно. Но общий вердикт опирается в основном на ARM-половину, а бо́льшая из двух значимости сама не достигает.
Объединять через две архитектуры корректно здесь только потому, что сравнение внутри пары всегда шло на одной машине: оба контейнера (с книгой и без) жили на одном боксе и играли друг с другом, поэтому скорость машины внутри пары сокращается. Складывать так две ОТДЕЛЬНЫЕ модели, замеренные на разных боксах, по-прежнему нельзя.
Побочная гипотеза, не находка: больший эффект показал более медленный бокс (ARM, чанк ~11.5 ч против ~8.8 ч). Механизм правдоподобен — книжный ход минует поиск целиком, значит стоит тем больше, чем хуже поиск успевает. Но разница незначима, так что это повод для проверки, а не вывод.
Сырые чанки, агрегатор и скрипт прогона — в приватном dicechess-ev, logs/timed-duel-book-2026-08-10/.
Где используется
Бот
Книга
azure/hunter-book
✅ монтируется файлом, HUNTER_BOOK_PATH
azure/onnx3-k48
❌ намеренно выключена — иначе размыла бы измеряемый эффект candidateLimit
house-боты (oracle-*)
❌ ORACLE_BOOK не задан
cloudflare/scala-aggressive-book
✅ своя копия, устаревшая (JSON-эпоха)
Регенерация не автоматизирована
Книга обновляется вручную: прогнать ExportOpeningBookApp, скопировать файл, перезапустить контейнер (шаги — в раннбуке). Каденс не задан. Пока боевая книга собрана 07.08.2026 на корпусе в 1.46 млн партий.