惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LINUX DO - 最新话题
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
PCI Perspectives
PCI Perspectives
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
H
Heimdal Security Blog
S
Security @ Cisco Blogs
N
News | PayPal Newsroom
J
Java Code Geeks
罗磊的独立博客
Security Archives - TechRepublic
Security Archives - TechRepublic
N
News and Events Feed by Topic
V
V2EX
WordPress大学
WordPress大学
Google Online Security Blog
Google Online Security Blog
N
News and Events Feed by Topic
www.infosecurity-magazine.com
www.infosecurity-magazine.com
月光博客
月光博客
AI
AI
小众软件
小众软件
The GitHub Blog
The GitHub Blog
MongoDB | Blog
MongoDB | Blog
A
Arctic Wolf
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
美团技术团队
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Tailwind CSS Blog
S
Schneier on Security
博客园 - 三生石上(FineUI控件)
F
Full Disclosure
B
Blog RSS Feed
Forbes - Security
Forbes - Security
S
SegmentFault 最新的问题
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
人人都是产品经理
人人都是产品经理
云风的 BLOG
云风的 BLOG
Jina AI
Jina AI
Cisco Talos Blog
Cisco Talos Blog
U
Unit 42
Project Zero
Project Zero
H
Hacker News: Front Page
Y
Y Combinator Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
The Cloudflare Blog
大猫的无限游戏
大猫的无限游戏
S
Secure Thoughts
The Hacker News
The Hacker News
Microsoft Azure Blog
Microsoft Azure Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Шахматные программы VII. Основной перебор Stockfish
Rom77 · 2026-06-01 · via Все публикации подряд на Хабре

Средний

19 мин

6K

Итак, мы добрались до самого сердца шахматной программы Стокфиш. Ее основной функции - search. Именно здесь бьется пульс этого движка.

Сейчас мы произведем разбор этой функции. О ее важности уже не раз говорилось на предыдущих страницах. Теперь нам предстоит понять, как основной поиск выглядит в реальной программе. Еще раз напомню, что функцию search можно найти в файле search.cpp. Обзорному описанию содержания этого файла была посвящена предыдущая часть

< К предыдущей части

615	Value Search::Worker::search(
	    Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, bool cutNode)

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

На старте производится инициализация параметров и вычисление некоторых простейших функций, на которых мы не будем подробно останавливаться. Основное, что следует отметить - здесь происходит выход на QS, к форсированному перебору, если достигнута предельная глубина (depth=0).

Для большей наглядности каждый этап перебора разделен на выделенные "шаги" (Step). Наиболее важные из них начинаются с 7-го, поэтому по первым шагам пробежимся коротко.

Step 1. Initialize node

На первом шаге собственно происходит инициализация параметров для узла (позиции) дерева перебора в которой мы сейчас находимся.

Step 2. Check for aborted search and immediate draw

На втором шаге проверяются условия прерывания поиска и возможность ничейного результата.

Step 3. Mate distance pruning

На третьем шаге производим отсечение по мату. Логика проста - если в какой-либо ветке дерева перебора гарантированно ставится мат, допустим в пять ходов, то в остальных ветках нет смысла смотреть глубже. Лучшим вариантом все равно будет ветвь с наиболее коротким матом. По достижении аналогичной глубины расчетов в любой ветке можно прерывать перебор, поскольку он нам больше ничего не даст.

Step 4. Transposition table

На четвертом шаге проверяем, нет ли позиции в таблице перестановок (хеш-таблицах). То есть, не просматривалась ли данная позиция ранее на требуемую или большую глубину. Проверка осуществляется по хеш-ключу. Если позиция присутствует, то перебор из нее проводить не обязательно, используем данные из таблиц.

Step 5. Tablebases probe

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

Step 6. Static evaluation of the position

На шестом шаге производится оценка позиции. Здесь через функцию evaluate(pos) и далее файл evaluate.cpp, происходит обращение к нейросети NNUE.

Это будет так называемая статическая оценка позиции, то есть оценка выполненная нейросетью на основе количества и взаимного расположения фигур, без их перемещения (а не та оценка, которую мы получаем после форсированного перебора qsearch).

Оценка целочисленная и рассчитывается в сантипешках. Это не значит, что цена пешки строго равна 100 единицам, но она достаточно близка к этому значению. Оценка возвращается с точки зрения той стороны, чей ход в данной позиции.

734 	else
735 	{
736 	    unadjustedStaticEval = evaluate(pos);
737 	    ss->staticEval = eval = to_corrected_static_eval(unadjustedStaticEval, correctionValue);

739 	    // Static evaluation is saved as it was before adjustment by correction history
740 	    ttWriter.write(posKey, VALUE_NONE, ss->ttPv, BOUND_NONE, DEPTH_UNSEARCHED, Move::none(),
741 	                   unadjustedStaticEval, tt.generation());
742 	}

Надо сказать, что оценка нейросети не используется в переборе в чистом виде. Она предварительно корректируется с помощью некоторой статистики предыдущего перебора, называемой correctionHistory. Таким образом, на данном шаге сначала производится оценка позиции нейросетью, а затем ее корректировка.

Корректировка оценки основана на разнице между чистой оценкой нейросети и оценкой полученной после полноценного перебора из той же позиции. После каждого прохода поиска данные заносятся в специальные таблицы. Со временем correctionHistory накапливает статистику о разнице оценок. Вычисляемая на основании табличных значений величина поправки оперативно вносится на этом шаге. Для пополнения таблиц, позиция совсем не обязательно должна быть идентична предыдущим, достаточно лишь совпадения отдельных ключевых элементов. Для быстрого распознавания одинаковых структур используются хеш-ключи.

Общую структуру системы корректировки оценок можно посмотреть в файле history.h, там же где находятся сортировки по истории.

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

Всего в основном поиске 21 шаг и наиболее важные среди них шаги с 7-го по 20-й. Именно в них прописаны основное методы производящие сокращения/продления дерева перебора.

Нетрудно заметить, что каждый шаг выглядит достаточно типовым образом. Обычно его предваряют входные условия (if). Если они не выполняются, то шаг пропускается и происходит переход далее, к следующему шагу, где будет предпринята очередная попытка произвести отсечение.

if (...)
    {
	value = -search(...);
    }

Если условия выполняются, то могут быть проведены некоторые дополнительные процедуры. Далее, либо сразу производится отсечение, либо выполняется перебор (search) ходов из данной позиции на определенных условиях и на определенную глубину. Из него мы получаем динамическую оценку позиции (value), на основании которой принимается решение об отсечении данной ветки, переходе к следующему шагу или выбору другого направления перебора. Но обычно происходит переход к следующему шагу.

Сначала пробуем методы, которые могут привести к немедленному отсечению, без излишнего перебора.

Step 7. Razoring
Step 8. Futility pruning

На седьмом и восьмом шагах производятся отсечения, если перебор приблизился к предельной/заданной глубине (низкой depth).

870 	// Step 7. Razoring
873 	if (!PvNode && eval < alpha - 485 - 281 * depth * depth)
874 	    return qsearch<NonPV>(pos, ss, alpha, beta);

876 	// Step 8. Futility pruning: child node
878 	{
879 	    auto futility_margin = [&](Depth d) {
...	    ...
885 	    };

887 	    if (!ss->ttPv && depth < 14 && eval - futility_margin(depth) >= beta && eval >= beta
888 	        && (!ttData.move || ttCapture) && !is_loss(beta) && !is_win(eval))
889 	        return (2 * beta + eval) / 3;
890 	}

Основной принцип следующий. Если по мере углубления по дереву перебора остался всего один полуход до достижения заданной глубины, а оценка позиции при этом уже настолько плоха, что последним полуходом ее никак не исправить, тогда этот ход можно не генерировать и не рассматривать и прервать перебор уже сейчас. Под плохой оценкой здесь подразумевается оценка ниже оценки основного варианта, обязательно с приличным запасом (futility_margin).

Если последний полуход "за соперника", то он никак не сможет повысить нам оценку, поэтому можно сразу отсекать. Такой метод называется Futility Pruning.

Если последний полуход "за нас", то переходим к qsearch. Обязательно нужно посмотреть форсированные ходы, поскольку хорошее взятие все еще может резко поднять нашу оценку. Тихие ходы можно пропустить. Такой метод называется Razoring.

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

Step 9. Null move search

На девятом шаге выполняются отсечения уже известным нам нулевым ходом (Null Move Pruning). Он подробно рассматривался во II и III частях настоящей статьи.

892 	// Step 9. Null move search with verification search
893 	if (cutNode && ss->staticEval >= beta - 18 * depth + 350 && !excludedMove
894 	    && pos.non_pawn_material(us) && ss->ply >= nmpMinPly && !is_loss(beta))
895 	{
...	...
898 	    // Null move dynamic reduction based on depth
899 	    Depth R = 7 + depth / 3;
900 	    do_null_move(pos, st, ss);
902 	    Value nullValue = -search<NonPV>(pos, ss + 1, -beta, -beta + 1, depth - R, false);
904 	    undo_null_move(pos);

906 	    // Do not return unproven mate or TB scores
907 	    if (nullValue >= beta && !is_win(nullValue))
908 	    {
...	    ...
914 	        // Do verification search at high depths, with null move pruning disabled
915 	        // until ply exceeds nmpMinPly.
916 	        nmpMinPly = ss->ply + 3 * (depth - R) / 4;
918 	        Value v = search<NonPV>(pos, ss, beta - 1, beta, depth - R, false);
920 	        nmpMinPly = 0;

922 	        if (v >= beta)
923 	            return nullValue;
924 	    }
925 	}

Аналогично, здесь сначала производится проверка условий допустимости нулевого хода, потом собственно пропуск хода (do_null) и пробный рекурсивный перебор за ту же сторону. Величина сокращения пробного перебора (R) рассчитывается в зависимости от глубины. При положительном исходе пробного перебора выполняется верификационный перебор, который окончательно решает, производить отсечение или нет.

Step 10. Internal iterative reductions (IIR)

На десятом шаге производится сокращение глубины, если позиции нет в хеш-таблице.

929 	// Step 10. Internal iterative reductions
932 	if (!allNode && depth >= 6 && !ttData.move && priorReduction <= 3)
933 	    depth--;

Для таких позиций оказалось выгоднее выполнять перебор с сокращенной глубиной. IIR очень похож на применявшийся ранее метод IID (Internal Iterative Deepening), но подходит к вопросу несколько иначе.

Step 11. ProbCut
Step 12. A small Probcut idea

На одиннадцатом и двенадцатом шаге выполняется отсечение методом ProbCut.

935 	// Step 11. ProbCut
936 	// If we have a good enough capture (or queen promotion) and a reduced search
937 	// returns a value much above beta, we can (almost) safely prune the previous move.
938 	probCutBeta = beta + 235 - 63 * improving;
939 	if (depth >= 3
940 	    && !is_decisive(beta)
941 	    // If value from transposition table is lower than probCutBeta, don't attempt probCut there
943 	    && !(is_valid(ttData.value) && ttData.value < probCutBeta))
944 	{
...	...
947 	    MovePicker mp(pos, ttData.move, probCutBeta - ss->staticEval, &captureHistory);
948 	    Depth      probCutDepth = std::clamp(depth - 5 - (ss->staticEval - beta) / 315, 0, depth);

950 	    while ((move = mp.next_move()) != Move::none())
951 	    {
...	        ...
959 	        do_move(pos, move, st, ss);

961 	        // Perform a preliminary qsearch to verify that the move holds
962 	        value = -qsearch<NonPV>(pos, ss + 1, -probCutBeta, -probCutBeta + 1);

964 	        // If the qsearch held, perform the regular search
965 	        if (value >= probCutBeta && probCutDepth > 0)
966 	            value = -search<NonPV>(pos, ss + 1, -probCutBeta, -probCutBeta + 1, probCutDepth,
967 	                                   !cutNode);

969 	        undo_move(pos, move);

971 	        if (value >= probCutBeta)
972 	        {
973 	            // Save ProbCut data into transposition table
974 	            ttWriter.write(posKey, value_to_tt(value, ss->ply), ss->ttPv, BOUND_LOWER,
975 	                           probCutDepth + 1, move, unadjustedStaticEval, tt.generation());

977 	            if (!is_decisive(value))
978 	                return value - (probCutBeta - beta);
979 	        }
980 	    }
981 	}

Как мы уже знаем, альфа-бета рассматривает как минимум одно продолжение из позиции на заданную глубину, и если его оценка превышает бету, то производится отсечение. При этом не учитывается, насколько превышена бета. Но ведь можно учитывать величину превышения. Идея метода ProbCut заключается в том, что если оценка намного превышает бету, то перебор этого продолжения можно выполнить и на глубину меньше заданной.

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

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

ProbCut производит отсечения примерно в той же области дерева перебора что и нулевой ход. При этом он сравнительно менее эффективен. Поэтому как правило он применяется после нулевого хода и как бы "вдовесок" к нему. Работая на уже расчищенном нулевым ходом "поле", он приносит немного пользы, а потому применяется обычно только в ведущих программах.

В Стокфише ProbCut применяется для взятий с высокой оценкой. Код во многом напоминает основной перебор в миниатюре.

*****
Перед тринадцатым шагом производится инициализация некоторых параметров, в том числе параметров для сортировки.

996 	MovePicker mp(pos, ttData.move, depth, &mainHistory, &lowPlyHistory, &captureHistory, contHist,
997 	              &sharedHistory, ss->ply);

999 	value = bestValue;

1001 	int moveCount = 0;

Step 13. Loop through all pseudo-legal moves until no moves remain or a beta cutoff occurs

На тринадцатом шаге запускается цикл генерации ходов (move = mp.next_move) из рассматриваемой позиции. По мере его выполнения будут рассмотрены все возможные ходы из позиции, если конечно в какой-то момент не произойдет отсечение и выход из цикла.

Ходы выдаются по одному за раз. Но пока они только определяются, а не выполняются на "внутренней доске" программы - эта операция будет произведена на 16 шаге. Выполнять ходы не нужно, поскольку программа сначала попытается их отсечь максимально быстро, используя только информацию о ходе. Перебор начнется позже.

Класс MovePicker, в том числе генератор ходов mp.next_move можно найти в файлах movepick. Как уже упоминалось ранее, для экономии ресурсов ходы генерируются "порционно", по стадиям сортировки.

1003 	// Step 13. Loop through all pseudo-legal moves until no moves remain
1004 	// or a beta cutoff occurs.
1005 	while ((move = mp.next_move()) != Move::none())
1006 	{
...	    ...
1009 	    if (move == excludedMove)
1010 	        continue;

1012 	    // Check for legality
1013 	    if (!pos.legal(move))
1014 	        continue;
...	    ...
1022 	    ss->moveCount = ++moveCount;
...	    ...
1029 	    if (PvNode)
1030 	        (ss + 1)->pv = nullptr;

1032 	    extension  = 0;
1033 	    capture    = pos.capture_stage(move);
1034 	    movedPiece = pos.moved_piece(move);
1035 	    givesCheck = pos.gives_check(move);

1037 	    // Calculate new depth for this move
1038 	    newDepth = depth - 1;

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

Таблицы истории вынесены в отдельный файл history.h. Сам процесс сортировки можно посмотреть в файле movepick.cpp.

Таблицы ButterflyHistory и PieceToHistory в принципе одинаковы и отражают классическое представление таблиц истории. Различия между ними только в характере записи ходов. Аналогично как в записи шахматной партии, в одном случае используется запись хода координатами начального и конечного полей, а в другом запись фигура - конечное поле.

Конечно сейчас система сортировок стала более разнообразной, чем в старых программах. Кроме обычных таблиц истории здесь накапливается статистика отдельно для пешечных структур PawnHistory, для перебора вблизи корня LowPlyHistory, для взятий CapturePieceToHistory и таблицы отражающие связь предшествующих и последующих ходов ContinuationHistory. PawnHistory определяет пешечные формации с помощью хеш-ключей, аналогично тому как поступают таблицы корректировки оценки (см. Step 6).

После определения хода, который будет рассматриваться далее, проверяется его корректность с помощью функции pos.legal(move). Дело в том, что генератор не всегда выдает корректные ходы с точки зрения правил. Поэтому ходы выходящие из под генератора ходов называют псевдолегальными.

Некоторые ходы слишком трудоемко полноценно вычислять на этапе генерации ходов. Например взятия на проходе или рокировки. Можно проверить их на корректность позже, когда они уже будут "заиграны". Проще сделать такой ход и проверить, подвергается ли король атаке. Аналогичным образом функция pos.legal(move) проверяет также ходы королем под шах и связки.

Таким образом сначала генерируются так называемые псевдолегальные ходы, а недопустимые среди них отсеиваются уже после генерации, по ходу цикла. Еще раз напомню, что функции начинающиеся с pos. можно найти в файле position.cpp

Step 14. Pruning at shallow depth

На четырнадцатом шаге снова выполняются отсечения на предельных глубинах (низких depth). Но на этот раз для их выполнения требуется знать ход в позиции. Генерация хода производилась на предыдущем шаге.

1049 	    // Step 14. Pruning at shallow depths.
1051 	    if (!rootNode && pos.non_pawn_material(us) && !is_loss(bestValue))
1052 	    {
1053 	        // Skip quiet moves if movecount exceeds our FutilityMoveCount threshold
1054 	        if (moveCount >= (3 + depth * depth) / (2 - improving))
1055 	            mp.skip_quiet_moves();

1057 	        // Reduced depth of the next LMR search
1058 	        int lmrDepth = newDepth - r / 1024;

1060 	        if (capture || givesCheck)
1061 	        {
...	        ...
1081 	        }
1082 	        else
1083 	        {
1084 	            int history = (*contHist[0])[movedPiece][move.to_sq()]
1085 	                               + (*contHist[1])[movedPiece][move.to_sq()]
1086 	                               + sharedHistory.pawn_entry(pos)[movedPiece][move.to_sq()];

1088 	            // Continuation history based pruning
1089 	            if (history < -4083 * depth)
1090 	                continue;

1092 	            history += 69 * mainHistory[us][move.raw()] / 32;
1095 	            lmrDepth += history / 3208;
1097 	            Value futilityValue = ss->staticEval + 42 + 161 * !bestMove + 127 * lmrDepth
1098 	                                + 85 * (ss->staticEval > alpha);

1100 	            // Futility pruning: parent node
1103 	            if (!ss->inCheck && lmrDepth < 13 && futilityValue <= alpha)
1104 	            {
1105 	                if (bestValue <= futilityValue && !is_decisive(bestValue)
1106 	                    && !is_win(futilityValue))
1107 	                    bestValue = futilityValue;
1108 	                continue;
1109 	            }

1111 	            lmrDepth = std::max(lmrDepth, 0);

1113 	            // Prune moves with negative SEE
1114 	            if (!pos.see_ge(move, -25 * lmrDepth * lmrDepth))
1115 	                continue;
1116 	        }
1117 	    }

Вблизи предельной/заданной глубины не обязательно отсекать только по оценке, как это делали Razoring и Futility Pruning ранее. Можно сформулировать правила отсечения и на основании иных принципов. Поэтому помимо Futility pruning здесь производятся отсечения поздних ходов по сортировке, ходов с плохой историей, отсечения по SEE.

Отсечения производятся раздельно для взятий и тихих ходов.

Step 15. Extensions

На пятнадцатом шаге мы обратимся к методам продления вариантов.

1119 	    // Step 15. Extensions
1120 	    // Singular extension search
1129 	    if (!rootNode && move == ttData.move && !excludedMove && depth >= 6 + ss->ttPv
1130 	        && is_valid(ttData.value) && !is_decisive(ttData.value) && (ttData.bound & BOUND_LOWER)
1131 	        && ttData.depth >= depth - 3 && !is_shuffling(move, ss, pos))
1132 	    {
1133 	        Value singularBeta  = ttData.value - (53 + 75 * (ss->ttPv && !PvNode)) * depth / 60;
1134 	        Depth singularDepth = newDepth / 2;

1136 	        ss->excludedMove = move;
1137 	        value = search<NonPV>(pos, ss, singularBeta - 1, singularBeta, singularDepth, cutNode);
1138 	        ss->excludedMove = Move::none();

1140 	        if (value < singularBeta)
1141 	        {
1142 	            int corrValAdj   = std::abs(correctionValue) / 230673;
1143 	            int doubleMargin = -4 + 199 * PvNode - 201 * !ttCapture - corrValAdj
1144 	                             - 897 * ttMoveHistory / 127649 - (ss->ply > rootDepth) * 42;
1145 	            int tripleMargin = 73 + 302 * PvNode - 248 * !ttCapture + 90 * ss->ttPv - corrValAdj
1146 	                             - (ss->ply * 2 > rootDepth * 3) * 50;

1148 	            extension =
1149 	              1 + (value < singularBeta - doubleMargin) + (value < singularBeta - tripleMargin);

1151 	            depth++;
1152 	        }

1154 	        // Multi-cut pruning
1160 	        else if (value >= beta && !is_decisive(value))
1161 	        {
1162 	            ttMoveHistory << std::max(-400 - 100 * depth, -4000);
1163 	            return value;
1164 	        }

1166 	        // Negative extensions

1173 	        // If the ttMove is assumed to fail high over current beta
1174 	        else if (ttData.value >= beta)
1175 	            extension = -3;

1177 	        // If we are on a cutNode but the ttMove is not assumed to fail high over current beta
1179 	        else if (cutNode)
1180 	            extension = -2;
1181 	    }

При изложении различных методов отсечений и сокращений (reductions) в предыдущих частях не раз упоминались и продления (extensions). Но за исключением QS, который традиционно не принято к ним относить, ни одного примера продлений так и не было приведено. Тем не менее, хотя в программах продления и не играют решающей роли, они очень важны. Всегда полезно продлить определенную ветку, досчитаться до конкретных последствий некоторых ходов и тем самым уточнить ее оценку.

По тексту программы можно заметить, что небольшие продления вариантов периодически происходят то здесь, то там. Но единственным "глобальным" методом продлений, выделенным отдельным пунктом, являются Singular Extensions - продления единственных ходов.

Те, кому приходилось играть против обычных шахматных программ, знают что они могут быть чрезвычайно изворотливы, особенно если попадают в сложную ситуацию. Раз за разом они отыскивают неочевидные ходы, которые только на время отодвигают материальные потери. Иногда даже чрезмерно отодвигают (так называемый "эффект горизонта"). Но против человека такой подход иногда срабатывает.

Если программа считает на фиксированную глубину, то она может неадекватно высоко оценивать такие варианты, тогда как это всего лишь отсрочка неизбежного. Поэтому логично их немного продлить, чтобы получить истинную оценку. Идея Singular Extensions заключается в продлении одного из ходов, если его оценка намного выше, чем у других из данной позиции. Тем более, что если такой ход единственный, то продлить его будет не слишком затратно.

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

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

Здесь вступает в действие метод MultiCut, который наоборот, производит отсечение узла. Его основной принцип заключается в том, что мы по сути уже дважды отсекли этот узел на сокращенной глубине, сначала исключенным ходом из хеш-таблиц, затем вторым ходом на предварительном переборе. Если позиция отсекалась уже несколькими ходами на сокращенной глубине, то почти наверняка хотя бы один из таких ходов отсечет ее и на полной (заданной) глубине. Производим отсечение.

В этом отношении MultiCut очень похож на ProbCut и нулевой ход. Точно так же он отсекает по бете с перебором на сокращенную глубину, но с некоторым условием. Это условие у каждого метода разное - пропуск хода у Null Move, большой запас в превышении беты у ProbCut, и многократные отсечения у MultiCut.

Так как методы похожи, то и отсекают они примерно одни и те же узлы. Нулевой ход наиболее эффективен в шахматах, поэтому он выполняется раньше. Два других метода вынуждены отсекать уже на "очищенном" дереве и соответственно дают не слишком большую прибавку к силе игры, а потому имеют вспомогательный характер.

Если же и MultiCut не сработает, то продолжим далее. Теперь мы можем если не отсечь, то хотя бы сократить глубину перебора.

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

Step 16. Make the move

Здесь собственно делается ход. Позиция на "внутренней доске" программы меняется. Производится углубление по дереву вариантов. Попутно обновляется счетчик позиций.

1183 	    // Step 16. Make the move
1184 	    do_move(pos, move, st, givesCheck, ss);

Между 16 и 17 шагом, в зависимости от некоторых условий вводятся различные поправки (r) к глубине на основании типа и статистики узлов. Это дополнительные величины сокращений и продлений для хода из нашей позиции. Они потребуются в основном на следующем шаге, для LMR.

1037 	    // Calculate new depth for this move
1038 	    newDepth = depth - 1;
1040 	    int delta = beta - alpha;
1042 	    Depth r = reduction(improving, depth, moveCount, delta);
...	    ...
1186 	    // Add extension to new depth
1187 	    newDepth += extension;
...	    ...
1199 	    // Increase reduction for cut nodes
1200 	    if (cutNode)
1201 	        r += 3372 + 997 * !ttData.move;

1203 	    // Increase reduction if ttMove is a capture
1204 	    if (ttCapture)
1205 	        r += 1119;
...	    ...
1226 	    // Scale up reductions for expected ALL nodes
1227 	    if (allNode)
1228 	        r += r / (depth + 1);

Step 17. Late moves reduction (LMR)

Метод LMR описан во II и III частях. Согласно нему, на ходах после первого по сортировке (moveCount > 1) производится перебор на сокращенную глубину. При этом используется минимальное окно по методу PVS. Если альфа превышена, то для проверки корректности оценки перебор повторяется, уже на полную глубину.

Сокращения (r, reduction) для LMR вычисляются выше, отдельной функцией в строке 1042. В Стокфише при вычислении сокращений учитываются уже четыре параметра, а не один лишь номер хода (moveCount), как практиковалось в старых программах.

1230 	    // Step 17. Late moves reduction / extension (LMR)
1231 	    if (depth >= 2 && moveCount > 1)
1232 	    {
1238 	        Depth d = std::max(1, std::min(newDepth - r / 1024, newDepth + 2)) + PvNode;

1240 	        ss->reduction = newDepth - d;
1241 	        value         = -search<NonPV>(pos, ss + 1, -(alpha + 1), -alpha, d, true);
1242 	        ss->reduction = 0;

1244 	        // Do a full-depth search when reduced LMR search fails high
1246 	        if (value > alpha)
1247 	        {
1248 	            // Adjust full-depth search based on LMR results - if the result was
1249 	            // good enough search deeper, if it was bad enough search shallower.
1250 	            const bool doDeeperSearch    = d < newDepth && value > bestValue + 50;
1251 	            const bool doShallowerSearch = value < bestValue + 9;

1253 	            newDepth += doDeeperSearch - doShallowerSearch;

1255 	            if (newDepth > d)
1256 	                value = -search<NonPV>(pos, ss + 1, -(alpha + 1), -alpha, newDepth, !cutNode);

1258 	            // Post LMR continuation history updates
1259 	            update_continuation_histories(ss, movedPiece, move.to_sq(), 1365);
1260 	        }
1261 	    }

Step 18. Full-depth search

Когда все предыдущие способы прекратить или сократить перебор исчерпаны, то выполняется основной перебор на заданную глубину. Если ход первый по сортировке (moveCount = 1) и является PV-node, он выполняется с полным альфа-бета окном. Для ходов после первого по сортировке выполняется перебор с минимальным окном. Аналогично предыдущему шагу, при превышении минимального окна, перебор может быть проведен повторно, с полным окном (см. PVS). При запуске задается тип узла <PV> или <NonPV> , Cut или All.

1263 	    // Step 18. Full-depth search when LMR is skipped
1264 	    else if (!PvNode || moveCount > 1)
1265 	    {
...	    ...
1270 	        // Note that if expected reduction is high, we reduce search depth here
1271 	        value = -search<NonPV>(pos, ss + 1, -(alpha + 1), -alpha,
1272 	                               newDepth - (r > 3957) - (r > 5654 && newDepth > 2), !cutNode);
1273 	    }

1275 	    // For PV nodes only, do a full PV search on the first move or after a fail high,
1276 	    // otherwise let the parent node fail low with value <= alpha and try another move.
1277 	    if (PvNode && (moveCount == 1 || value > alpha))
1278 	    {
1279 	        (ss + 1)->pv    = pv;
1280 	        (ss + 1)->pv[0] = Move::none();

1282 	        // Extend move from transposition table if we are about to dive into qsearch.
1283 	        // decisive score handling improves mate finding and retrograde analysis.
1284 	        if (move == ttData.move
1285 	            && ((is_valid(ttData.value) && is_decisive(ttData.value) && ttData.depth > 0)
1286 	                || ttData.depth > 1))
1287 	            newDepth = std::max(newDepth, 1);

1289 	        value = -search<PV>(pos, ss + 1, -beta, -alpha, newDepth, false);
1290 	    }

Step 19. Undo move

После того как ветка на предыдущем шаге исследована на заданную глубину, возвращаем ход назад. Поднимаемся выше по дереву вариантов.

1292 	    // Step 19. Undo move
1293 	    undo_move(pos, move);

Step 20. Check for a new best move

Здесь подводятся итоги основного перебора выполненного ранее, конечно если уже не произошло отсечения на одном из предыдущих шагов. Полученная оценка позиции последовательно сравнивается с оценкой текущего лучшего хода из позиции bestValue, альфой и бетой, по нарастающей.

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

1297 	    // Step 20. Check for a new best move
1298 	    // Finished searching the move. If a stop occurred, the return value of
1299 	    // the search cannot be trusted, and we return immediately without updating
1300 	    // best move, principal variation nor transposition table.
1301 	    if (threads.stop.load(std::memory_order_relaxed))
1302 	        return VALUE_ZERO;

1304 	    if (rootNode)
1305 	    {
...	    ...
1353 	    }

1355 	    // In case we have an alternative move equal in eval to the current bestmove,
1356 	    // promote it to bestmove by pretending it just exceeds alpha (but not beta).
1357 	    int inc = (value == bestValue && ss->ply + 2 >= rootDepth && (int(nodes) & 14) == 0
1358 	               && !is_win(std::abs(value) + 1));

1360 	    if (value + inc > bestValue)
1361 	    {
1362 	        bestValue = value;

1364 	        if (value + inc > alpha)
1365 	        {
1366 	            bestMove = move;

1368 	            if (PvNode && !rootNode)  // Update pv even in fail-high case
1369 	                update_pv(ss->pv, move, (ss + 1)->pv);

1371 	            if (value >= beta)
1372 	            {

1374 	                ss->cutoffCnt += (extension < 2) || PvNode;
1375 	                assert(value >= beta);  // Fail high
1376 	                break;
1377 	            }

1379 	            // Reduce other moves if we have found at least one score improvement
1380 	            if (depth > 2 && depth < 14 && !is_decisive(value))
1381 	                depth -= 2;
...	            ...		
1384 	            alpha = value;  // Update alpha! Always alpha < beta
1385 	        }
1386 	    }

После 20 шага цикл генерации и просмотра ходов из позиции, который начинался с 13 шага, заканчивается. Далее следует подведение итогов.

После 21 шага в основном обновляются хеш-таблицы и различная статистика по результатам прошедшего перебора. На этом основной перебор в данной позиции заканчивается. Функция search возвращает в дерево перебора оценку bestValue рассматриваемой позиции, предполагая некий лучший ход из нее, с перебором выполненным на заданную глубину.

Окончание следует…

Все части цикла: