Matchmaking für Dota 2014 schreiben

Hallo.



In diesem Frühjahr bin ich auf ein Projekt gestoßen, in dem die Jungs gelernt haben, wie man den Dota 2-Server der Version 2014 ausführt und dementsprechend darauf spielt. Ich bin ein großer Fan dieses Spiels, und ich konnte mir die einmalige Gelegenheit nicht entgehen lassen, in meine Kindheit einzutauchen.



Ich stürzte sehr tief und so kam es, dass ich einen Discord-Bot schrieb, der für fast alle Funktionen verantwortlich ist, die in der alten Version des Spiels nicht unterstützt werden, nämlich Matchmaking.

Vor allen Neuerungen mit dem Bot wurde die Lobby manuell erstellt. Wir haben 10 Antworten auf eine Nachricht gesammelt und manuell einen Server zusammengestellt oder eine lokale Lobby gehostet.







Meine Natur als Programmierer konnte nicht so viel manuelle Arbeit aushalten, und über Nacht skizzierte ich die einfachste Version des Bots, die den Server automatisch hochfuhr, wenn 10 Personen eingestellt wurden.



Ich habe mich entschlossen, sofort in nodejs zu schreiben, weil ich Python nicht wirklich mag und mich in dieser Umgebung wohler fühle.



Dies ist meine erste Erfahrung mit dem Schreiben eines Bots für Discord, aber es stellte sich als sehr einfach heraus. Das offizielle npm-Modul discord.js bietet eine praktische Schnittstelle zum Arbeiten mit Nachrichten, Sammeln von Reaktionen usw.



Haftungsausschluss: Alle Codebeispiele sind "aktuell", dh sie wurden über Nacht mehrfach umgeschrieben.



Der Kern des Matchmaking ist die "Warteschlange", in der Spieler, die spielen möchten, platziert und entfernt werden, wenn sie kein Spiel wollen oder finden.



So sieht die Essenz des "Spielers" aus. Anfangs war es nur eine Benutzer-ID in Discord, aber die Pläne beinhalten einen Launcher / eine Suche nach einem Spiel von der Site, aber das Wichtigste zuerst.



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


Und hier ist die Warteschlangenschnittstelle. Hier wird anstelle von "Spielern" eine Abstraktion in Form einer "Gruppe" verwendet. Für einen einzelnen Spieler besteht die Gruppe aus sich selbst und für Spieler in einer Gruppe aus allen Spielern in der Gruppe.



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
}


Beschlossen, Ereignisse zum Austausch von Kontext zu verwenden. Geeignet für Fälle - für das Ereignis "Ein Spiel für 10 Personen gefunden" können Sie die gewünschte Nachricht in privaten Nachrichten an die Spieler senden und die Hauptgeschäftslogik ausführen - Starten Sie eine Aufgabe, um die Bereitschaft zu überprüfen, die Lobby für den Start vorzubereiten usw.



Für IOC verwende ich InversifyJS. Ich habe eine angenehme Erfahrung mit dieser Bibliothek. Schnell und einfach!



Wir haben mehrere Warteschlangen auf dem Server - wir haben 1x1-Modi, reguläre / Bewertung und einige benutzerdefinierte hinzugefügt. Daher gibt es einen Singleton RoomService, der zwischen dem Benutzer und der Spielsuche liegt.



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-Nudeln für eine Vorstellung davon, wie die Prozesse aussehen)



Hier initialisiere ich eine Warteschlange für jeden der implementierten Spielmodi und höre auch Änderungen in den "Gruppen", um die Warteschlangen zu korrigieren und einige Konflikte zu vermeiden.



Also, ich bin großartig, ich habe Codeteile eingefügt, die nichts mit dem Thema zu tun haben, und jetzt gehen wir direkt zum Mastmaking über.



Stellen Sie sich einen Fall vor:



1) Der Benutzer möchte spielen.



2) Um die Suche zu starten, verwendet er Gateway = Discord,







dh er reagiert auf die Nachricht: 3) Dieses Gateway geht zu RoomService und sagt: "Der Benutzer aus der Zwietracht möchte die Warteschlange betreten, Modus: Spiel ohne Rating."



4) RoomService akzeptiert die Anforderung des Gateways und verschiebt sie in die gewünschte Warteschlange des Benutzers (genauer gesagt der Benutzergruppe).



5) Die Warteschlange prüft, ob bei jeder Änderung genügend Spieler vorhanden sind. Wenn möglich, geben Sie ein Ereignis aus:



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


6) RoomService ist offensichtlich froh, jede Warteschlange in gespannter Erwartung dieses Ereignisses anzuhören. Am Eingang erhalten wir eine Liste von Spielern, bilden von ihnen einen virtuellen "Raum" und senden natürlich ein Ereignis aus:



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) Also kamen wir zur "höchsten" Instanz - der Bot- Klasse . Im Allgemeinen befasst er sich mit der Verbindung zwischen Gateways (wie lächerlich es auf Russisch aussieht, kann ich nicht) und der Geschäftslogik des Matchmaking. Der Bot belauscht das Ereignis und befiehlt DiscordGateway, eine Bereitschaftsprüfung an alle Benutzer zu senden.







8) Wenn jemand das Spiel innerhalb von 3 Minuten ablehnt oder nicht akzeptiert, bringen wir ihn NICHT in die Warteschlange zurück. Wir stellen alle anderen wieder in die Warteschlange und warten, bis 10 Personen wieder eingestellt werden. Wenn alle Spieler das Spiel akzeptiert haben, beginnt der lustige Teil.



Dedizierte Serverkonfiguration



Unsere Spiele werden auf VDS mit Windows Server 2012 gehostet. Daraus lassen sich mehrere Schlussfolgerungen ziehen:



  1. Es gibt keinen Hafenarbeiter, der mein Herz getroffen hat
  2. Wir sparen Miete


Die Aufgabe besteht darin, den Prozess auf VDS mit VPS unter Linux zu starten. Schrieb einen einfachen Server in Flask. Ja, ich mag Python nicht, aber was kann ich tun - diesen Server darauf zu schreiben ist schneller und einfacher.



Es führt 3 Funktionen aus:



  1. Serverstart mit Konfiguration - Kartenauswahl, Anzahl der Spieler, die das Spiel starten sollen, und eine Reihe von Plugins. Ich werde jetzt nicht über Plugins schreiben - dies ist eine separate Geschichte mit Litern Kaffee in der Nacht, gemischt mit Tränen und zerrissenen Haaren.
  2. Stoppen / Neustarten des Servers bei erfolglosen Verbindungen, die nur manuell durchgeführt werden können.


Hier ist alles einfach, Codebeispiele sind sogar unangemessen. Skript für 100 Zeilen



Wenn also 10 Personen zusammengekommen sind und das Spiel akzeptiert haben, läuft der Server und jeder ist spielfreudig. Ein Link zum Herstellen einer Verbindung zum Spiel wird in privaten Nachrichten angezeigt.







Durch Klicken auf den Link stellt der Spieler eine Verbindung zum Spieleserver her, und das ist alles. Nach ~ 25 Minuten wird der virtuelle "Raum" mit den Spielern geräumt.



Ich entschuldige mich im Voraus für die Unbeholfenheit des Artikels, den ich hier schon lange nicht mehr geschrieben habe, und es gibt zu viel Code, um wichtige Abschnitte hervorzuheben. Kurz gesagt, Nudeln.



Wenn ich Interesse an dem Thema sehe, wird es einen zweiten Teil geben - er enthält meine Qualen mit Plugins für srcds (Source Dedicated Server) und wahrscheinlich ein Bewertungssystem und Mini-Dotabuff, eine Site mit Spielstatistiken.



Einige Links:



  1. Unsere Seite (Statistiken, Rangliste, kleine Landos und Client-Download)
  2. Discord-Server



All Articles