Я только лишь передвигал нужную шашку на нужное поле… (ответ Мариона Тинсли на вопрос, как ему удалось победить)   В Интернете можно откопать сотни, а в англоязычном его сегменте — тысячи, статей о разработке алгоритмов и ИИ для игры в шахматы. Однако шашки почему-то не привлекают такого интереса. В Рунете мне не удалось найти почти ни одной интересной статьи о последовательной разработке алгоритма для игры в шашки, поэтому мне захотелось его создать самому. Так как английские шашки (checkers) уже постигла ничейная смерть, я выбрал русские шашки, которые, кстати, сложнее английских. Краткие правила и тех и других приведены ниже: Шашки ходят только по клеткам черного цвета по диагонали. Простая шашка ходит только вперед на одно поле, а бьет — вперед и назад, перепрыгивая одно поле (в checkers — бьет только вперед) Дамка ходит и бьет вперед и назад на любое количество полей (в checkers — ходит вперед и назад только на 1 поле; бьет, перепрыгивая только 1 поле) Бить обязательно! При наличии нескольких вариантов боя — можно выбрать любой. Проигрывает тот, кто не может сделать ход. Изначально я хотел писать на python, но потом решил сделать крутую красивую игру и выбрал Unity (C#). Спойлер: красивую игру я так и не сделал. Разумеется классы, отвечающие за шашки на экране и в памяти алгоритма, разные. Я не буду останавливаться на MonoBehaviour Unity-скриптах и подробнее расскажу именно про мою реализацию алгоритма. Сначала опишу, как я храню доску в памяти. Класс шашки довольно прост: содержит, главное, тип шашки и ее позицию на поле, а также несколько вспомогательных переменных: Думаю, тут комментировать нечего. Доска же это набор шашек и их ходов. Это не полный код доски, а только важное: Конструктор Board() здесь строит доску по строке из цифр, где каждая цифра обозначает конкретную шашку (см. перечисление PieceType в классе Piece). Также есть конструктор, создающий глубокую копию доски. (Разобью весь класс на части, чтобы не было пелены кода и можно было дать пояснения) Следующие функции используются для поиска возможных ходов. Здесь стоит обратить внимание на то, что все обычные ходы считаются равноценными, а взятия — нет: сильнейшие взятия это те, которые бьют больше шашек соперника. В функции GetAllTakingMoves, которая ищет все ходы-взятия, важную роль играет т.н. турецкое правило, согласно которому побитые шашки снимаются с доски после хода и могут мешать продолжить взятия. Например в позиции ниже, если белые возьмут дамкой e1:a5:d8:f6:d4, они не смогут взять еще и шашку c5, так как, хотя шашка b6 к тому времени уже будет побита, она все еще будет стоять на доске, мешаясь дамке белых. В функции AddMove() интерес также представляет отдельная обработка ситуации, когда шашка своим ходом превращается в дамку — в таком случае можно продолжить взятие по ее правилам. Функция MakeMove совершает ход на доске: Разумеется, это только основные функции. Полный код, мониторящий доску, занимает почти 500 строк. Это довольно много, но не думаю, что можно как-то разделить ответственность: все относится непосредственно к свойствам нынешнего состояния игры.  Это все не очень интересно, и я даже мог бы найти и скачать готовый код шашек в Unity, но я решил написать его сам, чтобы лучше понимать игру. Возможно, тут можно что-то как-то кардинально улучшить и ускорить.  Перейдем к более интересным вещам Выбранный алгоритм — минимакс. Это набор правил для принятия решений для игр с нулевой суммой (один выиграл — другой проиграл). Он основывается на том, чтобы для всех возможных ходов, предугадать ответы соперника, и для каждого ответа — выбрать свой ответ, на который опять предугадать возможные ответы соперника.  То есть мы перебираем все варианты на определенную глубину. Разумеется, мы не можем перебрать все ходы на всю игру, поэтому дойдя до максимальной глубины, мы просто анализируем полученную позицию и понимаем, стоит нам идти в эту ветку игру или нет. Конечно, мы в первую очередь рассчитываем, что противник будет делать сильнейшие (по нашему мнению) ходу.  Если каждую позицию оценить одним числом (например, положительное — преимущество у белых, отрицательное — у черных), то на каждой итерации мы пытаемся максимизировать оценку позиции, если ход наш, и минимизировать, если ход соперника. Действительно, то что если позиция улучшается для соперника, то она ухудшается для нас. Отсюда название алгоритма -- минимакс. Итак, к алгоритму. Мне очень хочется назвать это не алгоритмом, а искусственным интеллектом, хотя он таковым не является. Это потому, что при разработке я назвал класс ИИ и так и продолжал о нем думать, поняв ошибку только позже. Сначала разберемся, как оценивать конкретную позицию на доске, а уже потом будет учиться думать наперед, оценивая получаемые позиции. Класс AI.sc умеет оценивать позицию, подсчитывая качества шашек обоих цветов на доске. Качество рассчитывается как произведение стоимости шашки, специального бонуса клетки (например, шашки в центре дороже шашек с левого или правого края доски) и Y-бонуса (бонус по вертикали: простая шашка тем дороже, чем ближе она к дамочным полям). Качество шашки = value * _squareBonus * yBonus Значения стоимостей и бонусов я выбрал такие: Теперь, когда мы умеем оценивать позицию, будем рекурсивно перебирать все наши ходы, ответы на них соперника, наши ответы на ответы соперника и т.д.  Так как мы не знаем, насколько сложная нынешняя позиция и сколько таких итераций потребуется, то будем увеличивать количество итераций, т.е. сначала проанализируем позицию на глубину 2 хода, потом 4, 6 и т.д. Это называется поиск в глубину с итеративным углублением. При этом чтобы избежать бесконечного поиска введем максимальное время анализа: после его истечения мы немедленно выходим из всех функций и используем результат последней полностью завершенной итерации. В функции IterativeDeepeningMinimax наглядно показано, как мы постепенно углубляемся в поиске. Если очередная итерация завершилась успешно, мы запоминаем лучший согласно ней ход; если она завершилась досрочно и экстренно из-за истечения времени поиска, то она неполная и ее результат, некорректный, нам не нужен. Стоит отметить, что если ход в позиции единственный, то он гарантированно будет сделан, а потому мы не тратим на него вычислительные мощности, а просто делаем его. При этом мы даже не уменьшаем количество оставшихся итераций, чтобы анализировать глубже. Поэтому в итоге мы можем даже получить ветку на несколько итераций глубже запланированного. Я считаю очень важным здесь то, что я не копирую доску, чтобы проанализировать каждый ход: ВЕСЬ поиск лучшего хода выполняется на одной доске (копии игровой), просто мы умеем совершать (make) и отменять ходы (unmake). Таким образом тяжелая функция глубокого копирования доски вызывается лишь единожды. В целом, такой код уже способен более менее играть и даже не зевать фигуры в один-два хода! Profit! Первым улучшением, которое в разы улучшило скорость анализа, стало альфа-бета отсечение.  Его суть заключается в том, что, например, предугадывая ход противника, мы даже не будет рассматривать откровенно глупые ходы, которые он не сделает. Ну, то есть он конечно может их сделать, но если они глупые — то нам же лучше! Например, на картинке ниже мы (играя за максимизирующую оценку сторону (за белых)), просчитав первые 2 варианта хода, видим, что можем получить позицию с оценкой 6. Поэтому, когда мы рассчитываем третий ход и видим, что одна из его дальнейших ветвей приводит к позиции с оценкой 5, то дальше мы даже не рассчитываем, так как, даже если там и будет что-то повыше, соперник лучше выберет 5, ведь он — минимизирующая сторона (черные). А потому мы даже не будем дальше анализировать 3-ю ветку, ведь лучше просто пойти по 2-й.  Таким образом, мы можем отсечь (prune) много лишних ветвей, избавившись от заведомо ненужных вычислений. В интернете много информации об этом улучшении, поэтому я не буду дальше его разжевывать, просто скажу, как я его реализовал. Функции минимакс я добавил аргументы alpha и beta, которые при первом вызове из IterativeDeepeningMinimax передаются как -Infinity и Infinity соответственно. После 115-й строки я добавил проверку на отсечение по альфе: А после 139-й — по бете: Double profit! Во время просчета позиций часто возникают одинаковые позиции. Разумеется, нет смысла рассчитывать их снова и снова в разных ветках минимакса, а потому мы можем сохранять однажды найденный лучший ход позиции. Стоит понимать, что если в позиции X на глубине d найден лучший ход n, то этот же ход является лучшим в той же позиции и на глубинах меньше d. А вот на глубинах больше d — не факт: возможно, нам казалось, что он лучший, но мы просто не досчитали и на самом деле ход проигрывающий. Если же в позиции X ход n ведет к форсированной победе, то глубину можно считать бесконечной, так как мы точно знаем, что ход выигрывающий.  Позиции считаются одинаковыми, если не только все шашки стоят на одинаковых местах, но и очередность хода совпадает. Транспозиции будут храниться в списке private List<Transposition> _transpositions = new List<Transposition>();  Где класс транспозиции выглядит так: В начале функции Minimax() (после, конечно, проверки на истечение времени) будем проверять, не встречали ли мы ранее данную позицию на подходящей глубине: Позиции, кстати, сравниваются как строки, т.к. каждую позицию можно однозначно представить как строку из 32 символов - фигур на черных полях (+1 символ для очередности хода). Если же транспозиция не найдена, то запускаем обычный поиск. Когда мы завершаем цикл, перед тем как вынести решение по позиции, будем добавлять ее в список транспозиций, как вклад в будущее: Функция AddTransposition просто добавляет транспозицию в список, не забывая убедиться, что там уже нет такой же позиции, но на меньшей глубине. В таком случае она стирается, так как зачем нам менее глубокий анализ, если уже подоспел более глубокий?! Итак, это улучшение позволяет выжать еще больше скорости из нашего алгоритма.  Triple profit! Особую сложность при расчетах представляют дебюты (начало игры) и эндшпили (ее окончание). Это так, потому что в дебюте нужно уметь считать очень далеко, чтобы понять последствия нашего хода, ведь фигуры только развиваются; в эндшпиле же фигур меньше, однако гораздо больше ходов (за счет наличия дальнобойных дамок) и анализ даже простейших позиций может быть долгим.  Кстати, по этой же причине именно в эндшпилях транспозиции становятся очень актуальными и полезными. К сожалению, мне не удалось найти каких-либо огромных дебютных и эндшпильных баз данных для шашек, хотя для шахмат такие существуют, причем могут занимать петабайты данных.  Однако мне удалось найти стандартизированные записи шашечных партий и выудить из них дебюты и эндшпили (несколько первых ходов из каждой партии и несколько последних). Поэтому я подключаю к своему алгоритму два блока:  Классы OpeningBook и EndgameBook наследуются от абстрактного TheoryBook, который умеет искать данную позицию в справочнике и возвращать лучший в ней ход: Теперь наша программа умеет делать первые 4-5 ходов моментально, пока у нее не закончится теория. К сожалению, выборка партий была очень маленькой (несколько тысяч), а и эндшпили, и, особенно, дебюты в ней повторялись, поэтому очень сильного прироста качества игры это не дало. Найти же большую выборку или готовые базы мне не удалось... Мы получили работающую программу для игры в русские шашки. Лично меня она разносит в пух и прах (возможно, потому что еще за пару месяцев до начала разработки я не умел нормально играть в шашки и зевал фигуры каждый ход). Я также дал ей сыграть с некоторыми приложениями в плеймаркете и она обыгрывает многие из них на большинстве уровней сложности. Оно также обыгрывает любителей в приложении Quick Checkers.  Разрабатывая алгоритм и его улучшения, я основывался на статьях о разработке программ для шахмат. Именно поэтому мне пришлось самому строить функцию оценки позиции и она может быть неточна.  Например, возможно, отношение стоимости обычной шашки к дамке должно быть не 100:250, а 100:150 или 100:500. Возможно, стоять в центре, а не на краю шашкам выгоднее не в 1.25 раза, а в 1.1 или 1.5.  Возможно, возможно, возможно... Разумеется, это все можно настроить, если реализовать "турнир" между компьютерами и постепенно мутировать эти числа, однако чтобы программа могла адекватно играть, ей нужно 10-15 секунд на КАЖДЫЙ ХОД (что дает глубину анализа 9-10 ходов вперед). Так как в шашечной партии в среднем ходов 30, одна такая партия может занять 5-8 минут, а чтобы построить нормальный процесс мутационной эволюции потребуется организовать, пожалуй, сотни и тысячи партий. Надеюсь, кому-то мой опыт окажется полезным и интересным. Самому мне также интересна эта тема, поэтому я буду рад, если в комментариях найдутся знающие люди, которые смогут подсказать другие возможные улучшения и ускорения алгоритма. Я также не отрицаю, что определенный потолок скорости может задавать Unity, так как, скорее всего, на чистом C++ алгоритм будет работать быстрее, чем на Unity+C#, но переписывать все на С++ я не хочу, к тому же я не знаком с графическими библиотеками С++, чтобы выводить все это на экран.  P.S. Предложениям буду рад, конечно, всем, но что-то высшематематическое вряд ли смогу реализовать. Уровень продвинутой школьной математики:)