Vi skriver matchmaking för Dota 2014

Hej alla.

I våras stötte jag på ett projekt där killarna lärde sig att köra Dota 2-serverversionen 2014 och följaktligen spela på den. Jag är ett stort fan av det här spelet, och jag kunde inte missa denna unika möjlighet att fördjupa mig i min barndom.

Jag dök väldigt djupt, och det blev så att jag skrev en Discord-bot som ansvarar för nästan all funktionalitet som inte stöds i den gamla versionen av spelet, nämligen matchmaking.
Innan alla innovationer med boten skapades lobbyn manuellt. Vi samlade in 10 reaktioner på ett meddelande och monterade manuellt en server, eller var värd för en lokal lobby.

Vi skriver matchmaking för Dota 2014

Min natur som programmerare tålde inte så mycket manuellt arbete och över en natt skissade jag på den enklaste versionen av boten, som automatiskt höjde servern när det var 10 personer.

Jag bestämde mig omedelbart för att skriva i nodejs, eftersom jag inte gillar Python, och jag känner mig mer bekväm i den här miljön.

Detta är min första erfarenhet av att skriva en bot för Discord, men det visade sig vara väldigt enkelt. Den officiella npm-modulen discord.js ger ett bekvämt gränssnitt för att arbeta med meddelanden, samla in reaktioner, etc.

Friskrivningsklausul: Alla kodexempel är "aktuella", vilket betyder att de har gått igenom flera iterationer av omskrivning på natten.

Grunden för matchmaking är en "kö" där spelare som vill spela placeras i och tas bort när de inte vill eller hittar ett spel.

Så här ser essensen av en "spelare" ut. Från början var det bara ett användar-id i Discord, men det finns planer på att lansera/söka efter spel från sidan, men först till kvarn.

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

Och här är kögränssnittet. Här används istället för "spelare" en abstraktion i form av en "grupp". För en enskild spelare består gruppen av honom själv, respektive för spelare i en grupp, av alla spelare i gruppen.

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
}

Jag bestämde mig för att använda händelser för att utbyta sammanhang. Det var lämpligt för fall - vid evenemanget "ett spel för 10 personer hittades", kan du skicka det nödvändiga meddelandet till spelarna i privata meddelanden och utföra den grundläggande affärslogiken - starta en uppgift för att kontrollera beredskapen, förbereda lobbyn för lansering och så vidare.

För IOC använder jag InversifyJS. Jag har en trevlig erfarenhet av att arbeta med det här biblioteket. Snabbt och enkelt!

Vi har flera köer på vår server - vi har lagt till 1x1, normal/klassad, och ett par anpassade lägen. Därför finns det en singleton RoomService som ligger mellan användaren och spelsökningen.

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

(Kodnudlar för att ge en uppfattning om hur processerna ungefär ser ut)

Här initierar jag kön för vart och ett av de implementerade spellägena, och lyssnar även efter ändringar i "grupper" för att justera köerna och undvika vissa konflikter.

Så bra jobbat, jag infogade kodbitar som inte har något med ämnet att göra, och låt oss nu gå vidare direkt till matchmaking.

Låt oss överväga fallet:

1) Användaren vill spela.

2) För att starta sökningen använder han Gateway=Discord, det vill säga sätter en reaktion på meddelandet:

Vi skriver matchmaking för Dota 2014

3) Den här gatewayen går till RoomService och säger "En användare från discord vill gå in i kön, läge: oklassat spel."

4) RoomService accepterar gatewayens begäran och skjuter användaren (närmare bestämt användargruppen) till önskad kö.

5) Kön kontrolleras varje gång det finns tillräckligt med spelare att spela. Om möjligt, sänd en händelse:

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

6) RoomService lyssnar uppenbarligen glatt på varje kö i orolig väntan på denna händelse. Vi får en lista över spelare som input, bildar ett virtuellt "rum" från dem och, naturligtvis, utfärdar ett event:

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) Så vi kom till den "högsta" myndigheten - klassen Bot. I allmänhet handlar han om kopplingen mellan gateways (jag kan inte förstå hur roligt det ser ut på ryska) och affärslogiken i matchmaking. Boten hör händelsen och beordrar DiscordGateway att skicka en beredskapskontroll till alla användare.

Vi skriver matchmaking för Dota 2014

8) Om någon avvisar eller inte accepterar spelet inom 3 minuter, då returnerar vi dem INTE till kön. Vi lämnar tillbaka alla andra i kön och väntar tills det är 10 personer igen. Om alla spelare har accepterat spelet börjar den intressanta delen.

Dedikerad serverkonfiguration

Våra spel finns på VDS med Windows Server 2012. Av detta kan vi dra flera slutsatser:

  1. Det finns ingen hamnare på den, vilket slog mig i hjärtat
  2. Vi sparar på hyran

Uppgiften är att köra en process på VDS från en VPS på Linux. Jag skrev en enkel server i Flask. Ja, jag gillar inte Python, men vad kan du göra? Det är snabbare och enklare att skriva den här servern på den.

Den utför 3 funktioner:

  1. Starta en server med en konfiguration - välj en karta, antalet spelare som ska starta spelet och en uppsättning plugins. Jag kommer inte skriva om plugins nu - det är en annan historia med litervis av kaffe på natten blandat med tårar och slitet hår.
  2. Stoppa/starta om servern vid misslyckade anslutningar, vilket vi bara kan hantera manuellt.

Allt är enkelt här, kodexempel är inte ens lämpliga. 100 rader manus

Så när 10 personer samlades och accepterade spelet, lanserades servern och alla var ivriga att spela, en länk för att ansluta till spelet skickades i privata meddelanden.

Vi skriver matchmaking för Dota 2014

Genom att klicka på länken ansluter spelaren till spelservern, och sedan är det allt. Efter ~25 minuter rensas det virtuella "rummet" med spelare.

Jag ber på förhand om ursäkt för artikelns besvärlighet, jag har inte skrivit här på länge, och det finns för mycket kod för att lyfta fram viktiga avsnitt. Nudlar, kort sagt.

Om jag ser intresse för ämnet kommer det att finnas en andra del - den kommer att innehålla min plåga med plugins för srcds (Source dedicated server), och, förmodligen, ett betygssystem och mini-dotabuff, en sida med spelstatistik.

Några länkar:

  1. Vår webbplats (statistik, topplista, liten målsida och klientnedladdning)
  2. Discord server

Källa: will.com

Lägg en kommentar