Schreiben von Matchmaking für Dota 2014

Hallo.

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

Ich habe mich sehr intensiv damit beschäftigt, und so kam es, dass ich einen Discord-Bot geschrieben habe, der für fast alle Funktionen verantwortlich ist, die in der alten Version des Spiels nicht unterstützt werden, nämlich das Matchmaking.
Vor allen Neuerungen mit dem Bot wurde die Lobby manuell erstellt. Wir haben 10 Reaktionen auf eine Nachricht gesammelt und manuell einen Server zusammengestellt oder eine lokale Lobby gehostet.

Schreiben von Matchmaking für Dota 2014

Meine Natur als Programmierer konnte so viel Handarbeit nicht aushalten, und über Nacht habe ich die einfachste Version des Bots entworfen, der den Server automatisch hochfährt, wenn 10 Leute da sind.

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

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

Haftungsausschluss: Alle Codebeispiele sind „aktuell“, was bedeutet, dass sie nachts mehrere Iterationen des Umschreibens durchlaufen haben.

Die Grundlage des Matchmakings ist eine „Warteschlange“, in die Spieler, die spielen möchten, eingefügt und entfernt werden, wenn sie kein Spiel finden oder nicht möchten.

So sieht das Wesen eines „Spielers“ aus. Ursprünglich war es nur eine Benutzer-ID in Discord, aber es gibt Pläne, Spiele über die Website zu starten/zu suchen, 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. Anstelle von „Spielern“ wird hier eine Abstraktion in Form einer „Gruppe“ verwendet. Bei einem einzelnen Spieler besteht die Gruppe aus ihm selbst, bei Spielern einer Gruppe jeweils aus allen Spielern 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
}

Ich habe beschlossen, Veranstaltungen zum Austausch von Kontexten zu nutzen. Es war für Fälle geeignet – bei dem Ereignis „Ein Spiel für 10 Personen wurde gefunden“, können Sie die erforderliche Nachricht in privaten Nachrichten an die Spieler senden und die grundlegende Geschäftslogik ausführen – eine Aufgabe starten, um die Bereitschaft zu überprüfen, die Lobby vorbereiten zum Start und so weiter.

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

Wir haben mehrere Warteschlangen auf unserem Server – wir haben 1x1, normal/bewertet und einige benutzerdefinierte Modi 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)
          }
        });
      }
    );
  }

(Codenudeln, um eine Vorstellung davon zu geben, wie die Prozesse ungefähr aussehen)

Hier initialisiere ich die Warteschlange für jeden der implementierten Spielmodi und lausche auch auf Änderungen in „Gruppen“, um die Warteschlangen anzupassen und einige Konflikte zu vermeiden.

Also, gut gemacht, ich habe Codeteile eingefügt, die nichts mit dem Thema zu tun haben, und jetzt gehen wir direkt zum Matchmaking über.

Betrachten wir den Fall:

1) Der Benutzer möchte spielen.

2) Um die Suche zu starten, verwendet er Gateway=Discord, setzt also eine Reaktion auf die Nachricht:

Schreiben von Matchmaking für Dota 2014

3) Dieses Gateway geht zu RoomService und sagt „Ein Benutzer von Discord möchte in die Warteschlange kommen, Modus: Spiel ohne Bewertung.“

4) RoomService nimmt die Anfrage des Gateways an und schiebt den Benutzer (genauer gesagt die Benutzergruppe) in die gewünschte Warteschlange.

5) Die Warteschlange überprüft jedes Mal, ob genügend Spieler zum Spielen vorhanden sind. Wenn möglich, geben Sie ein Ereignis aus:

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

6) RoomService hört offensichtlich jeder Schlange in gespannter Erwartung auf dieses Ereignis zu. Wir erhalten eine Liste von Spielern als Eingabe, bilden daraus einen virtuellen „Raum“ und geben 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) So kamen wir zur „höchsten“ Autorität – der Klasse Wander- und Outdoorschuhen. Im Allgemeinen beschäftigt er sich mit dem Zusammenhang zwischen Gateways (ich kann nicht verstehen, wie lustig das auf Russisch aussieht) und der Geschäftslogik des Matchmaking. Der Bot hört das Ereignis mit und weist DiscordGateway an, eine Bereitschaftsprüfung an alle Benutzer zu senden.

Schreiben von Matchmaking für Dota 2014

8) Wenn jemand das Spiel ablehnt oder nicht innerhalb von 3 Minuten annimmt, ordnen wir ihn NICHT wieder in die Warteschlange ein. Wir stellen alle anderen wieder in die Warteschlange und warten, bis wieder 10 Personen da sind. Wenn alle Spieler das Spiel angenommen haben, beginnt der interessante Teil.

Dedizierte Serverkonfiguration

Unsere Spiele werden auf VDS mit Windows Server 2012 gehostet. Daraus können wir mehrere Schlussfolgerungen ziehen:

  1. Es ist kein Docker drauf, was mich ins Herz getroffen hat
  2. Wir sparen Miete

Die Aufgabe besteht darin, einen Prozess auf VDS von einem VPS unter Linux auszuführen. Ich habe einen einfachen Server in Flask geschrieben. Ja, ich mag Python nicht, aber was können Sie tun? Es ist schneller und einfacher, diesen Server darauf zu schreiben.

Es erfüllt 3 Funktionen:

  1. Starten eines Servers mit einer Konfiguration – Auswahl einer Karte, der Anzahl der Spieler, die das Spiel starten sollen, und einer Reihe von Plugins. Ich werde jetzt nicht über Plugins schreiben – das ist eine andere Geschichte mit literweise Kaffee am Abend, gemischt mit Tränen und zerrissenen Haaren.
  2. Stoppen/Neustarten des Servers bei fehlgeschlagenen Verbindungen, die wir nur manuell beheben können.

Hier ist alles einfach, Codebeispiele sind nicht einmal angemessen. 100-Zeilen-Skript

Als also 10 Leute zusammenkamen und das Spiel annahmen, wurde der Server gestartet und alle wollten unbedingt spielen. Ein Link zum Herstellen einer Verbindung zum Spiel wurde in privaten Nachrichten gesendet.

Schreiben von Matchmaking für Dota 2014

Durch Klicken auf den Link verbindet sich der Spieler mit dem Spieleserver und das war’s. Nach ca. 25 Minuten ist der virtuelle „Raum“ mit den Spielern geräumt.

Ich entschuldige mich im Voraus für die Unbeholfenheit des Artikels. Ich habe hier schon lange nicht mehr geschrieben 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 wird meine Qual mit Plugins für srcds (Quelle dedizierter Server) und wahrscheinlich ein Bewertungssystem und Mini-Dotabuff, eine Seite mit Spielstatistiken, enthalten.

Einige Links:

  1. Unsere Website (Statistiken, Bestenliste, kleine Landingpage und Client-Download)
  2. Discord-Server

Source: habr.com

Kommentar hinzufügen