Писане на мачмейкинг за Dota 2014

Здравейте на всички.

Тази пролет попаднах на проект, в който момчетата се научиха как да стартират сървърната версия на Dota 2 2014 и съответно да играят на нея. Аз съм голям фен на тази игра и не можех да пропусна тази уникална възможност да се потопя в детството си.

Гмурнах се много дълбоко и се случи така, че написах бот на Discord, който отговаря за почти цялата функционалност, която не се поддържа в старата версия на играта, а именно намирането на мачове.
Преди всички нововъведения с бота, лобито се създаваше ръчно. Събрахме 10 реакции на съобщение и ръчно сглобихме сървър или хоствахме локално лоби.

Писане на мачмейкинг за Dota 2014

Природата ми на програмист не можеше да издържи толкова много ръчна работа и за една нощ начертах най-простата версия на бота, който автоматично вдигаше сървъра, когато имаше 10 души.

Веднага реших да пиша в nodejs, защото не харесвам много Python и се чувствам по-комфортно в тази среда.

Това е първият ми опит в писането на бот за Discord, но се оказа много просто. Официалният npm модул discord.js предоставя удобен интерфейс за работа със съобщения, събиране на реакции и др.

Отказ от отговорност: Всички примери на кодове са „актуални“, което означава, че са преминали през няколко повторения на пренаписване през нощта.

Основата на сватовството е „опашка“, в която играчите, които искат да играят, се поставят и премахват, когато не искат или не намерят игра.

Ето как изглежда същността на „играча“. Първоначално това беше просто потребителско име в 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. Имам приятно изживяване от работата с тази библиотека. Бързо и лесно!

Имаме няколко опашки на нашия сървър - добавихме 1x1, нормален/оценен и няколко персонализирани режима. Следователно има единичен 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, т.е. поставя реакция на съобщението:

Писане на мачмейкинг за Dota 2014

3) Този шлюз отива към RoomService и казва „Потребител от discord иска да влезе в опашката, режим: неоценена игра.“

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 да изпрати проверка за готовност на всички потребители.

Писане на мачмейкинг за Dota 2014

8) Ако някой отхвърли или не приеме играта в рамките на 3 минути, ние НЕ го връщаме на опашката. Връщаме всички останали на опашката и изчакваме пак да станат 10 човека. Ако всички играчи са приели играта, тогава започва интересната част.

Конфигурация на специален сървър

Нашите игри се хостват на VDS с Windows server 2012. От това можем да направим няколко заключения:

  1. На него няма докер, което ме удари в сърцето
  2. Спестяваме от наем

Задачата е да стартирате процес на VDS от VPS на Linux. Написах прост сървър във Flask. Да, не харесвам Python, но какво можете да направите? По-бързо и по-лесно е да напишете този сървър на него.

Изпълнява 3 функции:

  1. Стартиране на сървър с конфигурация - избор на карта, брой играчи за стартиране на играта и набор от плъгини. Сега няма да пиша за плъгини - това е друга история с литрите кафе през нощта, примесени със сълзи и накъсани коси.
  2. Спиране/рестартиране на сървъра при неуспешни връзки, които можем да обработим само ръчно.

Тук всичко е просто, примерите за код дори не са подходящи. 100 редов скрипт

И така, когато 10 души се събраха и приеха играта, сървърът беше стартиран и всички бяха нетърпеливи да играят, линк за връзка с играта беше изпратен в лични съобщения.

Писане на мачмейкинг за Dota 2014

Щраквайки върху връзката, играчът се свързва със сървъра на играта и това е всичко. След ~25 минути виртуалната „стая“ с играчи се изчиства.

Предварително се извинявам за неудобството на статията, не съм писал тук от дълго време и има твърде много код за подчертаване на важни секции. Юфка, накратко.

Ако видя интерес към темата, ще има втора част - тя ще съдържа моите терзания с плъгини за srcds (Source посветен сървър), и, вероятно, система за рейтинг и mini-dotabuff, сайт със статистика на играта.

Някои връзки:

  1. Нашият уебсайт (статистика, класация, малка целева страница и клиентско изтегляне)
  2. Discord сървър

Източник: www.habr.com

Добавяне на нов коментар