Всем привет!

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

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

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

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

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

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

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

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

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

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

Чтобы не создавать различные варианты расчетов в зависимости от того с какой стороны доски играет бот, я реализовал метод который позволяет "перевернуть" доску в случае необходимости и инвертировать цвет фишек, таким образом бот всегда считает что ходит белыми и начинает с верхней правой части доски, на картинке выше это "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.

Комментарии (0)