Всем привет!

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

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

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

  • эмитировать действия реального игрока

  • изменять уровень анализа текущей игровой ситуации

  • настраивать глубину прогнозирования действий в игре

  • динамически подбирать стратегию игры

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

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

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

Чтобы не создавать различные варианты расчетов в зависимости от того с какой стороны доски играет бот, я реализовал метод который позволяет "перевернуть" доску в случае необходимости и инвертировать цвет фишек, таким образом бот всегда считает что ходит белыми и начинает с верхней правой части доски, на картинке выше это "Devel's start".

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

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

  • Базовая оценка позиции после хода. Позволяет понять насколько в целом все фишки приближаются в результате этого хода к своему дому. Подсчет рейтинга идет по нарастающей в зависимос��и от расстояния до стартовой позиции. Эта оценка подталкивает бота перемещать фишки в сторону своего дома.

  • Бонус за снятие фишки со стартовой позиции. Это позволяет боту не забывать своевременно выводить фишки в игру.

  • Бонус за создание препятствия на пути соперника. Оценка увеличивается в зависимости от количества выставленных подряд фишек. Это позволяет боту эффективно создавать препятствия и защищать свои позиции.

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

  • Дополнительный бонус за перемещение фишек в дом, после того, как все фишки вышли из стартовой области. Влияет на изменение тактики игры во второй половине партии.

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

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

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

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

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

В качестве заключения я х��тел бы рассказать об одной идее, которая все никак не дает мне покоя. В процессе создания бота я подумал, что мне было бы интересно попробовать посоревноваться в теме создания ботов для игры в нарды. Можно создать механизм безопасного использования сторонней логики бота на сервере и запускать игры с ее использованием так, чтобы в рамках одной игры можно было использовать разную логику для каждого игрока. Понятно, что в нардах порой кубики решают исход игры, но то, как выстроена стратегия и то какие делаются шаги играет существенную роль в достижении победы. Мне это видится, как возможность передать в параметры новой игры ссылку на бессерверную функцию Cloudflare Workers, AWS Lambda, DigitalOcean Functions или любой другой вариант размещения, где по сути функция будет обрабатывать POST запрос содержащий состояние доски и список комбинаций возможных перемещений.

Тело запроса в формате JSON может выглядеть так:

{
  "board": {
    "white": [[0, 13], [1, 1], [4, 1]], 
    "black": [[0, 13], [2, 1], [5, 1]]
  },
  "steps": [
    [
      [0, 3], [3, 7]
    ],
    [
      [1, 4], [4, 8]
    ],
    [
      [4, 7], [7, 11]
    ]
  ]
}

В ответ функция должна вернуть индекс комбинации из массива "steps" и именно это перемещение будет засчитываться как ход.

Простейшая реализация такой функции на платформе Cloudflare может выглядеть так:

export default {
	async fetch(request) {
		if (request.method !== 'POST') {
			return new Response('Bad Request', { status: 400 });
		}

		try {
			const data = await request.json();
			const index = Math.floor(Math.random() * data.steps.length);
			return new Response(JSON.stringify({ index }), {
				headers: {
					'content-type': 'application/json;charset=utf-8'
				}
			});
		} catch (err) {
			return new Response('Bad Request', { status: 400 });
		}
	}
};

Если кому-то будет интересно пообщаться на эту тему - дайте знать в комментариях или пишите мне в telegram.

В последнем опросе треть голосующих проявила интерес к голосовому чату, так что в следующей части я расскажу о нем, а дальше перейду к описанию клиентского приложения.

PS Поиграть в мои нарды можно на сайте или в telegram.