[Python, Node.JS, Серверное администрирование, TypeScript, Игры и игровые приставки] Пишем матчмейкинг для Доты 2014 года

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
20-Авг-2020 15:35

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

Моя натура программиста не выдержала такое количество ручной работы, и за ночь я набросал самую простую версию бота, которая автоматически поднимала сервер, когда набиралось 10 человек
Писать сходу решил на nodejs, потому что не очень люблю питон, ну и комфортнее себя чувствую в этой среде.
Это мой первый опыт написания бота для Discord, но оказалось все очень даже просто. Официальный npm модуль discord.js предоставляет удобный интерфейс для работы с сообщениями, сбором реакций и т.д.
Дисклеймер: все примеры кода являются «актуальными», то есть прошли несколько итераций переписывания по ночам.
Основа матчмейкинга — это «очередь», в которую помещаются игроки, которые хотят играть, и убираются, когда расхотели или нашли игру.
Так выглядит сущность «игрока». Изначально это был просто id пользователя в Discord, но в планах лаунчер/поиск игры с сайта, но обо всем по порядку.
export enum Realm {
  DISCORD,
  EXTERNAL,
}
export default class QueuePlayer {
  constructor(public readonly realm: Realm, public readonly id: string) {}
  public is(qp: QueuePlayer): boolean {
    return this.realm === qp.realm && this.id === qp.id;
  }
  static Discord(id: string) {
    return new QueuePlayer(Realm.DISCORD, id);
  }
  static External(id: string) {
    return new QueuePlayer(Realm.EXTERNAL, id);
  }
}

А вот интерфейс очереди. Тут вместо «игроков» используется абстракция в виде «группы». Для одиночного игрока группа состоит из него самого, а для игроков в группе, соответственно, из всех игроков группы.
export default interface IQueue extends EventEmitter {
  inQueue: QueuePlayer[]
  put(uid: Party): boolean;
  remove(uid: Party): boolean;
  removeAll(ids: Party[]): void;
  mode: MatchmakingMode
  roomSize: number;
  clear(): void
}

Решил использовать события для обмена контекстом. Подходило под кейсы — по событию «найдена игра для 10 человек» можно и отправить в личные сообщения игрокам нужное сообщение, и выполнить основную бизнес логику — запустить таск для проверки готовности, подготовить лобби к запуску и так далее.
Для IOC я использую InversifyJS. Имею приятный опыт работы с этой библиотекой. Быстро и просто!
Очередей у нас на сервере несколько — добавились режими 1х1, обычный/рейтинговый, и пара кастомок. Поэтому есть singleton RoomService, который лежит между пользователем и поиском игры.
constructor(
    @inject(GameServers) private gameServers: GameServers,
    @inject(MatchStatsService) private stats: MatchStatsService,
    @inject(PartyService) private partyService: PartyService
  ) {
    super();
    this.initQueue(MatchmakingMode.RANKED);
    this.initQueue(MatchmakingMode.UNRANKED);
    this.initQueue(MatchmakingMode.SOLOMID);
    this.initQueue(MatchmakingMode.DIRETIDE);
    this.initQueue(MatchmakingMode.GREEVILING);
    this.partyService.addListener(
      "party-update",
      (event: PartyUpdatedEvent) => {
        this.queues.forEach((q) => {
          if (has(q.queue, (t) => t.is(event.party))) {
            // if queue has this party, we re-add party
            this.leaveQueue(event.qp, q.mode)
            this.enterQueue(event.qp, q.mode)
          }
        });
      }
    );
    this.partyService.addListener(
      "party-removed",
      (event: PartyUpdatedEvent) => {
        this.queues.forEach((q) => {
          if (has(q.queue, (t) => t.is(event.party))) {
            // if queue has this party, we re-add party
            q.remove(event.party)
          }
        });
      }
    );
  }

(Лапша кода для представления, как примерно выглядят процессы)
Здесь я инициализирую очередь под каждый из реализованных режимов игры, а так же слушаю изменения «групп», чтобы подкорректировать очереди и избежать некоторых конфликтов.
Так, я молодец, я вставил куски кода, которые никак не относятся к топику, а теперь перейдем уже непосредственно к мачтмейкингу.
Рассмотрим кейс:
1) Пользователь хочет поиграть
2) Для того, чтобы начать поиск, он использует Gateway=Discord, то есть ставит реакцию на сообщение

3) Этот гейтвей идет в RoomService, и говорит «Пользователь из дискорда хочет войти в очередь, режим: нерейтинговая игра»
4) RoomService принимает просьбу гейтвея, и пихает в нужную очередь пользователя(точнее, группу пользователя)
5) Очередь при каждом изменении проверяет, хватает ли игроков для игры. Если можно — эмиттим событие
private onRoomFound(players: Party[]) {
    this.emit("room-found", {
      players,
    });
  }

6) RoomService, очевидно, с радостью слушает каждую очередь в трепетном ожидании этого события. На вход мы получаем список игроков, формируем из них виртуальную «комнату», и, конечно же, эмиттим событие
queue.addListener("room-found", (event: RoomFoundEvent) => {
      console.log(
        `Room found mode: [${mode}]. Time to get free room for these guys`
      );
      const room = this.getFreeRoom(mode);
      room.fill(event.players);
      this.onRoomFormed(room);
    });

7) Вот мы и добрались до «высшей» инстанции — класса Bot. В целом он занимается связью между гейтвеями(как это смешно на русском выглядит я не могу) и бизнес логикой матчмейкинга. Бот подслушивает событие, и приказывает DiscordGateway отослать всем пользователям проверку на готовность.

8) Если кто-то отклонил или не принял игру за 3 минуты, то мы НЕ возвращаем их в очередь. Всех остальных возвращаем в очередь и ждем, когда снова наберется 10 человек. Если все игроки приняли игру, то начинается интересная часть.
Конфигурация выделенного сервера
У нас игры хостятся на VDS c Windows server 2012. Из этого можно сделать несколько выводов:
1) На него нет докера, что ударило меня в самое сердце
2) Мы экономим на аренде
Стоит задача: с VPS на линуксе запускать процесс на VDS. Написал простой сервер на Flask. Да, не люблю питон, но что поделать — на нем написать этот сервер быстрее и проще.
Он выполняет 3 функции:
1) Запуск сервера с конфигурацией — выбор карты, количества игроков для старта игры, и набор плагинов. Про плагины сейчас не буду писать — это отдельная история с литрами кофе по ночам вперемешку со слезами и вырванными волосами.
2) Остановка/перезапуск сервера в случае неудачных подключений, которые мы можем обработать только вручную
Тут все просто, примеры кода даже неуместны. Скрипт на 100 строчек
Итак, когда 10 человек собрались вместе и приняли игру, запущен сервер и все жаждут играть, в личные сообщения приходит ссылка на подключение к игре.

По нажатию ссылки игрока коннектит к игровому серверу, и дальше уже само все. Через ~25 минут виртуальная «комната» с игроками очищается.
Заранее извиняюсь за нескладность статьи, давно не писал сюда, да и кода слишком много, чтобы выделить важные участки. Лапша, короче.
Если увижу интерес к теме, то будет вторая часть — в ней будут мои мучения с плагинами для srcds(Source dedicated server), и, наверное, система рейтинга и мини-dotabuff, сайт со статистикой игр.
Немного ссылок:
1) Наш сайт(статистика, таблица лидеров, небольшой лендос и скачивание клиента)
2) Discord сервер
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_python, #_node.js, #_servernoe_administrirovanie (Серверное администрирование), #_typescript, #_igry_i_igrovye_pristavki (Игры и игровые приставки), #_typescript, #_nodejs, #_bot (бот), #_discord, #_dota_2, #_matchmejking (матчмейкинг), #_rejting (рейтинг), #_python, #_node.js, #_servernoe_administrirovanie (
Серверное администрирование
)
, #_typescript, #_igry_i_igrovye_pristavki (
Игры и игровые приставки
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 23-Ноя 00:34
Часовой пояс: UTC + 5