Hallo zusammen.
In diesem Frühling stieß ich auf ein Projekt, bei dem die Leute gelernt haben, einen Dota 2 Server der Version von 2014 zu starten und entsprechend darauf zu spielen. Ich bin ein großer Fan dieses Spiels und konnte an dieser einzigartigen Gelegenheit, in meine Kindheit einzutauchen, nicht vorbeigehen.
Ich tauchte sehr tief ein und schrieb fast über Nacht einen Discord-Bot, der für nahezu alle Funktionen verantwortlich ist, die in der alten Version des Spiels nicht unterstützt werden, nämlich das Matchmaking.
Vor allen Neuerungen wurde das Lobby-Setup manuell erstellt. Man sammelte 10 Reaktionen auf eine Nachricht und stellte manuell den Server zusammen oder hostete eine lokale Lobby.

Meine Programmiernatur hielt eine solche Menge an manueller Arbeit nicht aus, und so skizzierte ich über Nacht die einfachste Version des Bots, die automatisch den Server hochfuhr, wenn 10 Personen zusammenkamen.
Ich entschied mich sofort für nodejs, da ich Python nicht besonders mag und mich in dieser Umgebung wohler fühle.
Das ist meine erste Erfahrung mit dem Schreiben eines Bots für Discord, aber es stellte sich heraus, dass es sehr einfach ist. Das offizielle npm-Modul discord.js bietet eine praktische Schnittstelle für die Arbeit mit Nachrichten, dem Sammeln von Reaktionen usw.
Disclaimer: Alle Codebeispiele sind „aktuell“, das heißt, sie durchliefen mehrere Überarbeitungsiteration in der Nacht.
Die Grundlage des Matchmakings ist die „Warteschlange“, in die Spieler aufgenommen werden, die spielen wollen, und die entfernt werden, wenn sie nicht mehr wollen oder ein Spiel gefunden haben.
So sieht die Entität „Spieler“ aus. Ursprünglich war dies einfach die ID des Benutzers in Discord, aber geplant ist ein Launcher/Suchspiel von der Website, aber alles der Reihe nach.
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);
}
}
Hier ist die Schnittstelle der Warteschlange. Statt „Spieler“ wird hier eine Abstraktion in Form einer „Gruppe“ verwendet. Für einen einzelnen Spieler besteht die Gruppe aus ihm selbst, und für Spieler in einer Gruppe entsprechend 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 mich entschlossen, Ereignisse zum Austausch von Kontexten zu nutzen. Das passte zu den Anwendungsfällen - bei dem Ereignis „Spiel für 10 Personen gefunden“ kann man sowohl in persönlichen Nachrichten den Spielern die benötigte Mitteilung schicken als auch die grundlegende Geschäftslogik ausführen - einen Task zur Überprüfung der Bereitschaft starten, das Lobby für den Start vorbereiten und so weiter.
Für IOC verwende ich InversifyJS. Ich habe gute Erfahrungen mit dieser Bibliothek gemacht. Schnell und einfach!
Wir haben mehrere Warteschlangen auf dem Server - es wurden Modi 1x1, normal / rankend und ein paar benutzerdefinierte hinzugefügt. Daher gibt es einen Singleton RoomService, der zwischen dem Benutzer und der Spielsuche steht.
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))) {
// wenn die Warteschlange diese Gruppe hat, fügen wir die Gruppe erneut hinzu
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))) {
// wenn die Warteschlange diese Gruppe hat, entfernen wir die Gruppe
q.remove(event.party)
}
});
}
);
}
(Code-Nudeln zur Veranschaulichung, wie die Prozesse aussehen)
Hier initialisiere ich eine Warteschlange für jeden der implementierten Spielmodi und höre die Änderungen der „Gruppen“ an, um die Warteschlangen anzupassen und einige Konflikte zu vermeiden.
So, ich habe es geschafft, ich habe Codefragmente eingefügt, die nichts mit dem Thema zu tun haben, und jetzt kommen wir direkt zum Matchmaking.
Betrachten wir den Fall:
1) Der Benutzer möchte spielen.
2) Um die Suche zu starten, nutzt er Gateway=Discord, also reagiert er auf eine Nachricht:

3) Dieses Gateway geht zu RoomService und sagt: „Der Benutzer von Discord möchte der Warteschlange beitreten, Modus: unranked Spiel.“
4) RoomService nimmt die Anfrage des Gateways entgegen und fügt den Benutzer (genauer gesagt, die Gruppe des Benutzers) in die benötigte Warteschlange ein.
5) Die Warteschlange überprüft bei jeder Änderung, ob genug Spieler für das Spiel vorhanden sind. Wenn ja - emittieren wir das Ereignis:
private onRoomFound(players: Party[]) {
this.emit("room-found", {
players,
});
}
6) Der RoomService hört offensichtlich mit Freude jeder Warteschlange zu, in gespanntem Warten auf dieses Ereignis. Am Eingang erhalten wir eine Liste von Spielern, bilden daraus einen virtuellen "Raum" und emitten natürlich ein Ereignis:
queue.addListener("room-found", (event: RoomFoundEvent) => {
console.log(
`Raum gefunden Modus: [${mode}]. Zeit, um einen freien Raum für diese Jungs zu finden`
);
const room = this.getFreeRoom(mode);
room.fill(event.players);
this.onRoomFormed(room);
});
7) Hier sind wir nun bei der "höchsten" Instanz — der Klasse Bot. Insgesamt kümmert es sich um die Verbindung zwischen Gateways (wie das auf Russisch lustig klingt, kann ich nicht) und der Geschäftlogik des Matchmakings. Der Bot lauscht dem Ereignis und befiehlt DiscordGateway, allen Nutzern eine Bereitschaftsprüfung zu senden.

8) Wenn jemand das Spiel innerhalb von 3 Minuten abgelehnt oder nicht angenommen hat, setzen wir sie NICHT zurück in die Warteschlange. Alle anderen werden in die Warteschlange zurückgesetzt und wir warten, bis wieder 10 Personen zusammenkommen. Wenn alle Spieler das Spiel angenommen haben, beginnt der interessante Teil.
Konfiguration des dedizierten Servers
Unsere Spiele werden auf VDS mit Windows Server 2012 gehostet. Daraus lassen sich mehrere Schlussfolgerungen ziehen:
- Es gibt dort keinen Docker, was mich mitten ins Herz getroffen hat.
- Wir sparen bei der Miete.
Die Aufgabe besteht darin, den Prozess auf VDS mit VPS unter Linux zu starten. Ich habe einen einfachen Server in Flask geschrieben. Ja, ich mag Python nicht, aber was soll ich machen — es ist schneller und einfacher, diesen Server damit zu schreiben.
Er erfüllt 3 Funktionen:
- Starten des Servers mit der Konfiguration — Auswahl der Karte, Anzahl der Spieler für den Spielstart und Auswahl der Plugins. Über die Plugins möchte ich jetzt nicht schreiben — das ist eine separate Geschichte mit Litern Kaffee nachts, gemischt mit Tränen und herausgerissenen Haaren.
- Stoppen/Neustarten des Servers bei fehlgeschlagenen Verbindungen, die wir nur manuell bearbeiten können.
Hier ist alles einfach, Codebeispiele sind nicht einmal angebracht. Das Skript umfasst 100 Zeilen.
Also, wenn 10 Personen zusammengekommen sind und das Spiel angenommen haben, der Server gestartet ist und alle spielen möchten, kommt ein Link zur Verbindung mit dem Spiel in die persönlichen Nachrichten.

Beim Klicken auf den Link verbindet der Spieler sich mit dem Spielesever, und der Rest geschieht von selbst. Nach etwa 25 Minuten wird der virtuelle "Raum" mit den Spielern geleert.
Ich entschuldige mich im Voraus für die Unordnung des Artikels, ich habe lange nicht mehr hier geschrieben und es gibt zu viel Code, um die wichtigen Stellen herauszustellen. Kurz gesagt, es ist ein Wirrwarr.
Wenn ich Interesse an dem Thema sehe, wird es einen zweiten Teil geben – darin werden meine Qualen mit den Plugins für srcds (Source Dedicated Server) und vielleicht ein Bewertungssystem sowie ein Mini-Dotabuff, eine Website mit Spieldaten, behandelt.
Einige Links:
Quelle: habr.com
