Píšeme matchmaking pro Dotu 2014

Ahoj všichni.

Toto jaro jsem narazil na projekt, kde se kluci naučili spouštět server Dota 2 verze 2014 a samozřejmě na něm hrát. Jsem velkým fanouškem této hry a nemohl jsem si nechat ujít jedinečnou příležitost ponořit se do svého dětství.

Ponořil jsem se velmi hluboko, a tak jsem napsal Discord bota, který pokrývá prakticky veškerou funkcionalitu, která není podporována ve staré verzi hry, konkrétně matchmaking.
Před všemi novinkami s botem se lobby vytvářela ručně. Sesbírali jsme 10 reakcí na zprávu a ručně jsme sešli server, nebo jsme hostovali lokální lobby.

Píšeme matchmaking pro Dotu 2014

Moje povaha programátora nevydržela tolik ruční práce, a za noc jsem spíchl nejjednodušší verzi bota, která automaticky zvedla server, když se přihlásilo 10 lidí.

Rozhodl jsem se psát rovnou v nodejs, protože moc nemám rád Python, a v této prostředí se cítím pohodlněji.

Tohle byl můj první pokus napsat bota pro Discord, ale ukázalo se, že to je vlastně velmi jednoduché. Oficiální npm modul discord.js poskytuje pohodlné rozhraní pro práci se zprávami, sběrem reakcí atd.

Disklaimer: všechny příklady kódu jsou 'aktuální', což znamená, že prošly několika iteracemi přepisování přes noc.

Základem matchmakingu je 'fronta', do které se umisťují hráči, kteří chtějí hrát, a vyjíždějí, když si to rozmyslí nebo najdou hru.

Takto vypadá entita 'hráče'. Zpočátku to byl jen id uživatele na Discordu, ale máme v plánu launcher/hledání hry z webu, ale o tom postupně.

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);
  }
}

A tohle je rozhraní fronty. Zde místo 'hráčů' používáme abstrakci ve formě 'skupiny'. Pro jednotlivého hráče se skupina skládá z něj samotného, a pro hráče ve skupině, odpovídajícím způsobem, ze všech hráčů skupiny.

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
}

Rozhodl jsem se použít události pro výměnu kontextu. Hodilo se to do případů — při události „nalezeno hráčů pro 10 osob“ lze poslat potřebnou zprávu osobními zprávami hráčům a provést hlavní obchodní logiku — spustit úkol pro kontrolu připravenosti, připravit lobby pro spuštění a tak dále.

Pro IOC používám InversifyJS. Mám příjemnou zkušenost s touto knihovnou. Rychlé a jednoduché!

Na našem serveru máme několik front — přidaly se režimy 1x1, běžný/hodnocený, a pár customek. Proto existuje singleton RoomService, který leží mezi uživatelem a vyhledáváním hry.

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))) {
            // pokud fronta má tuto skupinu, znovu přidáme skupinu
            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))) {
            // pokud fronta má tuto skupinu, odstraníme skupinu
            q.remove(event.party)
          }
        });
      }
    );
  }

(Ukázkový kód pro představu, jak přibližně vypadají procesy)

Zde inicializuji frontu pro každý z implementovaných režimů hry, a také naslouchám změnám „skupin“, abych upravil fronty a vyhnul se některým konfliktům.

Tak, jsem dobrý, vložil jsem kousky kódu, které se nijak netýkají tématu, a teď přejdeme k matchmakingu.

Zvažme případ:

1) Uživatel chce hrát.

2) Aby začal vyhledávání, používá Gateway=Discord, tedy reaguje na zprávu:

Píšeme matchmaking pro Dotu 2014

3) Tento gateway jde do RoomService a říká: „Uživatel z Discordu chce vstoupit do fronty, režim: nezávislá hra.“

4) RoomService přijímá žádost gatewaye a přidává uživatele (přesněji, skupinu uživatele) do potřebné fronty.

5) Fronta při každé změně kontroluje, zda je dostatek hráčů na hru. Pokud ano — emitujeme událost:

private onRoomFound(players: Party[]) {
    this.emit("room-found", {
      players,
    });
  }

6) RoomService, samozřejmě, s radostí poslouchá každou frontu v vzrušení očekávání této události. Na vstupu dostáváme seznam hráčů, tvoříme z nich virtuální "místnost" a samozřejmě emitujeme událost:

queue.addListener("room-found", (event: RoomFoundEvent) => {
      console.log(
        `Room found mode: [${mode}]. Čas získat bezplatnou místnost pro tyto lidi`
      );
      const room = this.getFreeRoom(mode);
      room.fill(event.players);

      this.onRoomFormed(room);
    });

7) A dostali jsme se k "vyšší" instanci — třídě Bot. Celkově se zabývá komunikací mezi bránami (jak to směšně v ruštině zní, nemohu si pomoci) a podnikatelskou logikou matchmakingu. Bot naslouchá události a přikazuje DiscordGateway poslat všem uživatelům kontrolu na připravenost.

Píšeme matchmaking pro Dotu 2014

8) Pokud někdo odmítl nebo nepřijal hru do 3 minut, NEvracíme je zpět do fronty. Všechny ostatní vracíme do fronty a čekáme, až se opět nasbírá 10 lidí. Pokud všichni hráči hru přijmuli, začne zajímavá část.

Konfigurace dedikovaného serveru

Naše hry jsou hostovány na VDS s Windows server 2012. Z toho lze udělat několik závěrů:

  1. Nemáme tam Docker, což mě zasáhlo do samého srdce
  2. Šetříme na nájmu

Úkolem je spustit proces na VDS z VPS s Linuxem. Napsal jsem jednoduchý server na Flask. Ano, Python nemám rád, ale co dělat — napsat tento server na něm je rychlejší a jednodušší.

Plní 3 funkce:

  1. Spuštění serveru s konfigurací — výběr mapy, počtu hráčů pro zahájení hry a sada pluginů. O pluginech nyní psát nebudu — to je zvláštní příběh s litry kávy v noci smíchanými se slzami a vytrhanými vlasy.
  2. Zastavení/znovu spuštění serveru v případě neúspěšných připojení, která můžeme zpracovat pouze ručně.

Tady je to jednoduché, příklady kódu jsou dokonce nevhodné. Skript má 100 řádků

Takže, když se 10 lidí sešlo a přijali hru, server je spuštěn a všichni touží hrát, přichází do soukromých zpráv odkaz na připojení ke hře.

Píšeme matchmaking pro Dotu 2014

Po kliknutí na odkaz se hráč připojuje k hernímu serveru, a dál už je to samo. Za přibližně 25 minut se virtuální "místnost" s hráči vyčistí.

Omlouvám se za nešikovnost článku, dlouho jsem sem nic nepsal, a je tu také příliš mnoho kódu, abych vyzdvihl důležité části. Prostě nudle.

Pokud uvidím zájem o téma, udělám druhou část – ta se zaměří na mé trápení s pluginy pro srcds (Source dedicated server), a pravděpodobně na systém hodnocení a mini-dotabuff, web se statistikami her.

Několik odkazů:

  1. Naše webová stránka (statistiky, žebříček, malá landing page a stažení klienta)
  2. Discord server

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster