Пішам матчмэйкінг для Доты 2014 года

Усім прывітанне.

Гэтай вясной я натыкнуўся на праект, у якім хлопцы навучыліся запускаць Dota 2 сервер версіі 2014 года і, адпаведна, гуляць на ім. Я вялікі фанат гэтай гульні, і не змог прайсці міма ўнікальнай магчымасці акунуцца ў сваё дзяцінства.

Акунуўся я вельмі глыбока, і так выйшла што я напісаў Discord робата, які адказвае практычна за ўвесь функцыянал, які не падтрымліваецца ў старой версіі гульні, а менавіта матчмэйкінг.
Да ўсіх новаўвядзенняў з ботам лобі стваралася ўручную. Збіралі 10 рэакцый на паведамленне і ўручную збіралі сервер, альбо хосцілі лакальнае лобі.

Пішам матчмэйкінг для Доты 2014 года

Мая натура праграміста не вытрымала такую ​​колькасць ручной працы, і за ноч я накідаў самую простую версію бота, якая аўтаматычна паднімала сервер, калі набіралася 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, гэта значыць ставіць рэакцыю на паведамленне:

Пішам матчмэйкінг для Доты 2014 года

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 адправіць усім карыстальнікам праверку на гатоўнасць.

Пішам матчмэйкінг для Доты 2014 года

8) Калі нехта адхіліў ці не прыняў гульню за 3 хвіліны, то мы НЕ вяртаем іх у чаргу. Усіх астатніх вяртаем у чаргу і чакаем, калі зноў набярэцца 10 чалавек. Калі ўсе гульцы прынялі гульню, то пачынаецца цікавая частка.

Канфігурацыя выдзеленага сервера

У нас гульні хосцяцца на VDS c Windows server 2012. З гэтага можна зрабіць некалькі высноў:

  1. На яго няма докера, што стукнула мяне ў самае сэрца
  2. Мы эканомім на арэндзе

Стаіць задача: з VPS на лінуксе запускаць працэс на VDS. Напісаў просты сервер на Flask. Так, не кахаю пітон, але што парабіць - на ім напісаць гэты сервер хутчэй і прасцей.

Ён выконвае 3 функцыі:

  1. Запуск сервера з канфігурацыяй - выбар карты, колькасці гульцоў для старту гульні, і набор убудоў. Пра плагіны зараз не буду пісаць — гэта асобная гісторыя з літрамі кавы па начах уперамешку са слязамі і вырванымі валасамі.
  2. Прыпынак/перазапуск сервера ў выпадку няўдалых падлучэнняў, якія мы можам апрацаваць толькі ўручную.

Тут усё проста, прыклады кода нават недарэчныя. Скрыпт на 100 радкоў

Такім чынам, калі 10 чалавек сабраліся разам і прынялі гульню, запушчаны сервер і ўсё прагнуць гуляць, у асабістыя паведамленні прыходзіць спасылка на падлучэнне да гульні.

Пішам матчмэйкінг для Доты 2014 года

Па націску спасылкі гульца канэктыт да гульнявога сервера, і далей ужо само ўсё. Праз ~25 хвілін віртуальная «пакой» з гульцамі чысціцца.

Загадзя прашу прабачэння за няскладнасць артыкула, даўно не пісаў сюды, ды і кода занадта шмат, каб вылучыць важныя ўчасткі. Локшына, карацей.

Калі ўбачу цікавасць да тэмы, то будзе другая частка - у ёй будуць мае пакуты з убудовамі для srcds (Source dedicated server), і, мусіць, сістэма рэйтынгу і міні-dotabuff, сайт са статыстыкай гульняў.

Трохі спасылак:

  1. Наш сайт(статыстыка, табліца лідэраў, невялікі лендос і запампоўка кліента)
  2. Discord сервер

Крыніца: habr.com

Дадаць каментар