Psaní matchmakingu pro Dota 2014

Ahoj všichni

Letos na jaře jsem narazil na projekt, ve kterém se kluci naučili provozovat server Dota 2 verze 2014 a podle toho na něm hrát. Jsem velkým fanouškem této hry a nemohl jsem si nechat ujít tuto jedinečnou příležitost ponořit se do svého dětství.

Ponořil jsem se velmi hluboko, a tak se stalo, že jsem napsal Discord bota, který je zodpovědný za téměř všechny funkce, které nejsou podporovány ve staré verzi hry, konkrétně matchmaking.
Před všemi inovacemi s botem byla lobby vytvořena ručně. Shromáždili jsme 10 reakcí na zprávu a ručně jsme sestavili server nebo hostili místní lobby.

Psaní matchmakingu pro Dota 2014

Moje povaha programátora nevydržela tolik ruční práce a přes noc jsem načrtl nejjednodušší verzi bota, který automaticky zvedl server, když bylo 10 lidí.

Okamžitě jsem se rozhodl psát v nodejs, protože nemám moc rád Python a v tomto prostředí se cítím pohodlněji.

Toto je moje první zkušenost s psaním bota pro Discord, ale ukázalo se, že je to velmi jednoduché. Oficiální npm modul discord.js poskytuje pohodlné rozhraní pro práci se zprávami, sběr reakcí atp.

Upozornění: Všechny příklady kódu jsou „aktuální“, což znamená, že prošly několika iteracemi přepisování v noci.

Základem matchmakingu je „fronta“, ve které jsou hráči, kteří chtějí hrát, umístěni a odstraněni, když nechtějí nebo najdou hru.

Tak vypadá podstata „hráče“. Zpočátku to bylo jen ID uživatele v Discordu, ale existují plány na spuštění/vyhledání her z webu, ale nejdřív.

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 zde je rozhraní fronty. Zde se místo „hráčů“ používá abstrakce ve formě „skupiny“. Pro jednoho hráče se skupina skládá z něj samotného a pro hráče ve skupině ze všech hráčů ve skupině.

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 využít události k výměně kontextu. Bylo to vhodné pro případy - při události „našla se hra pro 10 lidí“, můžete hráčům poslat potřebnou zprávu v soukromých zprávách a provést základní obchodní logiku - spustit úlohu 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 snadné!

Na našem serveru máme několik front – přidali jsme 1x1, normální/hodnocený a několik vlastních režimů. Proto existuje singleton RoomService, který leží mezi uživatelem a hledá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))) {
            // 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)
          }
        });
      }
    );
  }

(Napište nudle, abyste měli představu, jak procesy zhruba vypadají)

Zde inicializujem frontu pro každý z implementovaných herních režimů a také poslouchám změny ve „skupinách“, abych upravil fronty a vyhnul se některým konfliktům.

Takže výborně, vložil jsem kousky kódu, které nemají s tématem nic společného, ​​a nyní přejděme přímo k matchmakingu.

Podívejme se na případ:

1) Uživatel chce hrát.

2) Pro spuštění vyhledávání použije Gateway=Discord, to znamená, že na zprávu reaguje:

Psaní matchmakingu pro Dota 2014

3) Tato brána přejde do RoomService a řekne „Uživatel z discordu chce vstoupit do fronty, režim: neohodnocená hra.“

4) RoomService přijme požadavek brány a přesune uživatele (přesněji skupinu uživatelů) do požadované fronty.

5) Fronta kontroluje pokaždé, když je dostatek hráčů ke hře. Pokud je to možné, vygenerujte událost:

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

6) RoomService zjevně s radostí poslouchá každou frontu v napjatém očekávání této události. Jako vstup obdržíme seznam hráčů, vytvoříme z nich virtuální „místnost“ a samozřejmě vydáme událost:

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) Dostali jsme se tedy k „nejvyšší“ autoritě – třídě Bot. Obecně se zabývá spojením mezi bránami (nechápu, jak vtipně to vypadá v ruštině) a obchodní logikou matchmakingu. Bot zaslechne událost a nařídí DiscordGateway, aby všem uživatelům odeslala kontrolu připravenosti.

Psaní matchmakingu pro Dota 2014

8) Pokud někdo hru odmítne nebo nepřijme do 3 minut, NEVRÁTÍME ho do fronty. Všechny ostatní vracíme do fronty a čekáme, až bude opět 10 lidí. Pokud všichni hráči hru přijali, začíná zajímavá část.

Konfigurace vyhrazeného serveru

Naše hry jsou hostovány na VDS s Windows serverem 2012. Z toho můžeme vyvodit několik závěrů:

  1. Není na něm žádný docker, což mě zasáhlo u srdce
  2. Šetříme na nájemném

Úkolem je spustit proces na VDS z VPS na Linuxu. Napsal jsem jednoduchý server ve Flasku. Ano, nemám rád Python, ale co se dá dělat? Je rychlejší a jednodušší napsat tento server na něj.

Plní 3 funkce:

  1. Spuštění serveru s konfigurací - výběr mapy, počtu hráčů pro spuštění hry a sady pluginů. Nebudu teď psát o pluginech - to je jiný příběh s litry kávy v noci smíchanými se slzami a vytrhanými vlasy.
  2. Zastavení/restart serveru v případě neúspěšného připojení, které můžeme řešit pouze ručně.

Všechno je zde jednoduché, příklady kódu nejsou ani vhodné. 100 řádkový skript

Když se tedy sešlo 10 lidí a hru přijali, server byl spuštěn a všichni se těšili na hraní, v soukromých zprávách byl zaslán odkaz na připojení ke hře.

Psaní matchmakingu pro Dota 2014

Kliknutím na odkaz se hráč připojí k hernímu serveru a je to. Po ~25 minutách se virtuální „místnost“ s hráči vyčistí.

Předem se omlouvám za nešikovnost článku, dlouho jsem sem nepsal a je tam příliš mnoho kódu na zvýraznění důležitých sekcí. Nudle, zkrátka.

Pokud uvidím zájem o téma, bude druhý díl - bude obsahovat moje muka s pluginy pro srcds (Source dedikovaný server) a pravděpodobně i systém hodnocení a mini-dotabuff, web se statistikami her.

Nějaké odkazy:

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

Zdroj: www.habr.com

Přidat komentář