Nagsusulat kami ng matchmaking para sa Dota 2014

Kumusta lahat.

Sa tagsibol na ito ay nakatagpo ako ng isang proyekto kung saan natutunan ng mga lalaki kung paano patakbuhin ang bersyon ng server ng Dota 2 2014 at, nang naaayon, maglaro dito. Ako ay isang malaking tagahanga ng larong ito, at hindi ko maaaring palampasin ang natatanging pagkakataong ito upang ilubog ang aking sarili sa aking pagkabata.

Napakalalim ko, at nangyari na nagsulat ako ng isang Discord bot na responsable para sa halos lahat ng pag-andar na hindi sinusuportahan sa lumang bersyon ng laro, lalo na ang paggawa ng mga posporo.
Bago ang lahat ng mga pagbabago sa bot, ang lobby ay ginawa nang manu-mano. Nangolekta kami ng 10 reaksyon sa isang mensahe at manu-manong nag-assemble ng server, o nag-host ng lokal na lobby.

Nagsusulat kami ng matchmaking para sa Dota 2014

Ang aking likas na katangian bilang isang programmer ay hindi makatiis ng napakaraming manu-manong gawain, at sa magdamag ay nag-sketch ako ng pinakasimpleng bersyon ng bot, na awtomatikong nagtaas ng server kapag mayroong 10 tao.

Agad akong nagpasya na magsulat sa mga nodej, dahil hindi ko talaga gusto ang Python, at mas komportable ako sa kapaligirang ito.

Ito ang aking unang karanasan sa pagsulat ng isang bot para sa Discord, ngunit ito ay naging napaka-simple. Ang opisyal na npm module na discord.js ay nagbibigay ng maginhawang interface para sa pagtatrabaho sa mga mensahe, pagkolekta ng mga reaksyon, atbp.

Disclaimer: Ang lahat ng mga halimbawa ng code ay "kasalukuyan", ibig sabihin, dumaan sila sa ilang mga pag-ulit ng muling pagsulat sa gabi.

Ang batayan ng matchmaking ay isang "pila" kung saan ang mga manlalaro na gustong maglaro ay inilalagay at inaalis kapag ayaw nila o makahanap ng laro.

Ito ang hitsura ng esensya ng isang "manlalaro". Sa una ito ay isang user id lamang sa Discord, ngunit may mga planong maglunsad/maghanap ng mga laro mula sa site, ngunit una sa lahat.

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

At narito ang queue interface. Dito, sa halip na "mga manlalaro," isang abstraction sa anyo ng isang "grupo" ang ginagamit. Para sa isang solong manlalaro, ang grupo ay binubuo ng kanyang sarili, at para sa mga manlalaro sa isang grupo, ayon sa pagkakabanggit, ng lahat ng mga manlalaro sa grupo.

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
}

Nagpasya akong gumamit ng mga kaganapan upang makipagpalitan ng konteksto. Ito ay angkop para sa mga kaso - sa kaganapan na "nahanap ang isang laro para sa 10 tao", maaari mong ipadala ang kinakailangang mensahe sa mga manlalaro sa mga pribadong mensahe, at isagawa ang pangunahing lohika ng negosyo - maglunsad ng isang gawain upang suriin ang kahandaan, ihanda ang lobby para sa paglulunsad, at iba pa.

Para sa IOC gumagamit ako ng InversifyJS. Mayroon akong kaaya-ayang karanasan sa pagtatrabaho sa aklatang ito. Mabilis at madali!

Marami kaming pila sa aming server - nagdagdag kami ng 1x1, normal/rated, at ilang custom na mode. Samakatuwid, mayroong isang solong RoomService na nasa pagitan ng user at ng paghahanap ng laro.

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

(Code noodles upang magbigay ng ideya kung ano ang halos hitsura ng mga proseso)

Dito ko sinisimulan ang pila para sa bawat isa sa ipinatupad na mga mode ng laro, at nakikinig din para sa mga pagbabago sa "mga grupo" upang ayusin ang mga pila at maiwasan ang ilang mga salungatan.

Kaya't, magaling, nagpasok ako ng mga piraso ng code na walang kinalaman sa paksa, at ngayon ay lumipat tayo nang direkta sa paggawa ng mga posporo.

Isaalang-alang natin ang kaso:

1) Nais maglaro ang gumagamit.

2) Upang simulan ang paghahanap, ginagamit niya ang Gateway=Discord, iyon ay, naglalagay ng reaksyon sa mensahe:

Nagsusulat kami ng matchmaking para sa Dota 2014

3) Pupunta ang gateway na ito sa RoomService at nagsasabing "Gustong pumasok sa queue ang isang user mula sa discord, mode: unrated game."

4) Tinatanggap ng RoomService ang kahilingan ng gateway at itinutulak ang user (mas tiyak, ang grupo ng user) sa gustong pila.

5) Ang pila ay nagsusuri sa tuwing may sapat na mga manlalaro upang maglaro. Kung maaari, maglabas ng isang kaganapan:

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

6) Ang RoomService ay halatang masayang nakikinig sa bawat pila sa sabik na pag-asa sa kaganapang ito. Nakatanggap kami ng isang listahan ng mga manlalaro bilang input, bumubuo ng isang virtual na "kuwarto" mula sa kanila, at, siyempre, naglalabas ng isang kaganapan:

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) Kaya nakarating kami sa "pinakamataas" na awtoridad - ang klase bot. Sa pangkalahatan, tinatalakay niya ang koneksyon sa pagitan ng mga gateway (hindi ko maintindihan kung gaano ito nakakatawa sa Russian) at ang lohika ng negosyo ng paggawa ng mga posporo. Nadinig ng bot ang kaganapan at inutusan ang DiscordGateway na magpadala ng pagsusuri sa kahandaan sa lahat ng user.

Nagsusulat kami ng matchmaking para sa Dota 2014

8) Kung may tumanggi o hindi tumanggap ng laro sa loob ng 3 minuto, HINDI namin sila ibabalik sa pila. Ibinabalik namin ang iba sa pila at maghintay hanggang may 10 tao muli. Kung tinanggap ng lahat ng mga manlalaro ang laro, magsisimula ang kawili-wiling bahagi.

Nakatuon sa pagsasaayos ng server

Ang aming mga laro ay naka-host sa VDS na may Windows server 2012. Mula dito maaari kaming gumawa ng ilang mga konklusyon:

  1. Walang docker dito, na tumama sa puso ko
  2. Nagtitipid kami sa upa

Ang gawain ay magpatakbo ng isang proseso sa VDS mula sa isang VPS sa Linux. Sumulat ako ng isang simpleng server sa Flask. Oo, hindi ko gusto ang Python, ngunit ano ang magagawa mo? Mas mabilis at mas madaling isulat ang server na ito dito.

Gumaganap ito ng 3 function:

  1. Pagsisimula ng server na may configuration - pagpili ng mapa, ang bilang ng mga manlalaro na magsisimula ng laro, at isang set ng mga plugin. Hindi na ako magsusulat tungkol sa mga plugin ngayon - ibang kuwento iyon sa mga litro ng kape sa gabi na may halong luha at gutay-gutay na buhok.
  2. Ihihinto/i-restart ang server kung sakaling magkaroon ng mga hindi matagumpay na koneksyon, na maaari lang naming hawakan nang manu-mano.

Ang lahat ay simple dito, ang mga halimbawa ng code ay hindi angkop. 100 linyang script

Kaya, nang magsama-sama ang 10 tao at tinanggap ang laro, inilunsad ang server at ang lahat ay sabik na maglaro, isang link upang kumonekta sa laro ay ipinadala sa mga pribadong mensahe.

Nagsusulat kami ng matchmaking para sa Dota 2014

Sa pamamagitan ng pag-click sa link, kumokonekta ang player sa server ng laro, at pagkatapos ay iyon na. Pagkatapos ng ~25 minuto, ang virtual na "kuwarto" na may mga manlalaro ay na-clear.

Humihingi ako ng paumanhin nang maaga para sa awkwardness ng artikulo, matagal na akong hindi nagsusulat dito, at napakaraming code upang i-highlight ang mahahalagang seksyon. Noodles, in short.

Kung makakita ako ng interes sa paksa, magkakaroon ng pangalawang bahagi - maglalaman ito ng aking pagdurusa sa mga plugin para sa srcds (Source dedicated server), at, malamang, isang rating system at mini-dotabuff, isang site na may mga istatistika ng laro.

Ilang link:

  1. Ang aming website (mga istatistika, leaderboard, maliit na landing page at pag-download ng kliyente)
  2. Discord server

Pinagmulan: www.habr.com

Magdagdag ng komento