Création d'un système de matchmaking pour Dota 2014

Bonjour à tous.

Ce printemps, je suis tombé sur un projet où les gars ont réussi à lancer un serveur Dota 2 de 2014 et, par conséquent, à y jouer. Je suis un grand fan de ce jeu et je n'ai pas pu résister à l'opportunité unique de plonger dans mon enfance.

Je me suis plongé si profondément que j'ai fini par écrire un bot Discord qui gère pratiquement toutes les fonctionnalités non supportées dans l'ancienne version du jeu, à savoir le matchmaking.
Avant toutes les nouveautés avec le bot, les lobbys étaient créés manuellement. On rassemblait 10 réactions sur un message et constituait manuellement le serveur ou hébergeait un lobby local.

Création d'un système de matchmaking pour Dota 2014

Mon tempérament de programmeur n'a pas supporté une telle quantité de travail manuel, et en une nuit, j'ai esquissé la version la plus simple du bot, qui élève automatiquement le serveur lorsque 10 personnes sont réunies.

J'ai décidé d'écrire directement en nodejs, car je n'aime pas vraiment Python et je me sens plus à l'aise dans cet environnement.

C'est ma première expérience d'écriture d'un bot pour Discord, mais tout s'est avéré plutôt simple. Le module npm officiel discord.js offre une interface pratique pour travailler avec les messages, la collecte de réactions, etc.

Avertissement : tous les exemples de code sont « actuels », c'est-à-dire qu'ils ont subi plusieurs itérations de réécriture pendant la nuit.

La base du matchmaking est une « file d'attente » dans laquelle les joueurs qui veulent jouer sont placés, puis retirés lorsqu'ils ne le souhaitent plus ou ont trouvé une partie.

Voici à quoi ressemble l'entité « joueur ». Au départ, c'était juste l'ID de l'utilisateur dans Discord, mais il est prévu un lanceur/recherche de jeu depuis le site, mais faisons ça dans l'ordre.

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

Voici l'interface de la file d'attente. Ici, au lieu de « joueurs », une abstraction sous forme de « groupe » est utilisée. Pour un joueur individuel, le groupe est composé de lui-même, et pour les joueurs d'un groupe, il en va de même pour tous les membres du groupe.

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
}

J'ai décidé d'utiliser des événements pour échanger des contextes. Cela correspondait aux cas d'utilisation - lorsqu'un événement « jeu trouvé pour 10 personnes » se produit, il est possible d'envoyer le message approprié en messages privés aux joueurs et d'exécuter la logique métier principale - lancer une tâche pour vérifier la préparation, préparer le lobby au lancement, etc.

Pour l'IOC, j'utilise InversifyJS. J'ai une expérience agréable avec cette bibliothèque. C'est rapide et simple !

Nous avons plusieurs files d'attente sur le serveur - des modes 1v1 ont été ajoutés, ainsi qu'un mode normal/compétitif, et quelques personnalisés. C'est pourquoi il y a un singleton RoomService qui se trouve entre l'utilisateur et la recherche de jeu.

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))) {
            // si la file d'attente a cette fête, nous réajoutons la fête
            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))) {
            // si la file d'attente a cette fête, nous retirons la fête
            q.remove(event.party)
          }
        });
      }
    );
  }

(Code de présentation pour montrer à quoi ressemblent les processus)

Ici, j'initialise la file d'attente pour chacun des modes de jeu implémentés, et j'écoute également les changements de « groupes » pour ajuster les files d'attente et éviter certains conflits.

Eh bien, je suis malin, j'ai inséré des morceaux de code qui n'ont rien à voir avec le sujet, maintenant passons au matchmaking.

Considérons le cas :

1) L'utilisateur souhaite jouer.

2) Pour commencer la recherche, il utilise Gateway=Discord, c'est-à-dire qu'il réagit à un message :

Création d'un système de matchmaking pour Dota 2014

3) Cette passerelle va à RoomService et dit « Un utilisateur de Discord souhaite entrer dans la file d'attente, mode : jeu non classé ».

4) RoomService accepte la demande de la passerelle et place l'utilisateur (plutôt, le groupe de l'utilisateur) dans la file d'attente appropriée.

5) La file d'attente vérifie à chaque changement si le nombre de joueurs est suffisant pour jouer. Si oui, nous émettons l'événement :

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

6) RoomService, écoute avec impatience chaque file d'attente à l'affût de cet événement. Nous recevons une liste de joueurs, formons une « salle » virtuelle à partir d'eux et, bien sûr, émettons un événement :

queue.addListener("room-found", (event: RoomFoundEvent) => {
      console.log(
        `Mode de salle trouvé : [${mode}]. Il est temps d'obtenir une salle gratuite pour ces gars`
      );
      const room = this.getFreeRoom(mode);
      room.fill(event.players);

      this.onRoomFormed(room);
    });

7) Nous voici arrivés à la « plus haute » instance — la classe Bot.Dans l'ensemble, il s'occupe de la communication entre les passerelles (c'est drôle à quel point cela sonne en russe) et de la logique commerciale de la mise en relation. Le bot surveille l'événement et ordonne à DiscordGateway d'envoyer à tous les utilisateurs une vérification de leur disponibilité.

Création d'un système de matchmaking pour Dota 2014

8) Si quelqu'un refuse ou ne répond pas au jeu dans les 3 minutes, nous ne le remettons PAS dans la file d'attente. Tous les autres sont remis dans la file et nous attendons que 10 personnes soient à nouveau réunies. Si tous les joueurs acceptent le jeu, la partie intéressante commence.

Configuration du serveur dédié

Nos jeux sont hébergés sur un VDS avec Windows Server 2012. Cela permet de tirer plusieurs conclusions :

  1. Il n'y a pas de Docker, ce qui m'a profondément blessé.
  2. Nous économisons sur la location.

L'objectif est de lancer un processus sur le VDS depuis un VPS sous Linux. J'ai écrit un serveur simple en Flask. Oui, je n'aime pas Python, mais que faire — écrire ce serveur était plus rapide et plus simple avec ce langage.

Il remplit 3 fonctions :

  1. Lancer le serveur avec la configuration — choix de la carte, nombre de joueurs pour commencer le jeu, et jeux de plugins. Je ne vais pas parler des plugins maintenant — c'est une autre histoire avec des litres de café la nuit mélangés aux larmes et aux cheveux arrachés.
  2. Arrêter/redémarrer le serveur en cas de connexions échouées, que nous ne pouvons traiter qu'à la main.

Ici tout est simple, des exemples de code seraient même inappropriés. Un script de 100 lignes.

Donc, quand 10 personnes se sont réunies et ont accepté le jeu, le serveur est lancé et tout le monde a hâte de jouer, un lien de connexion au jeu arrive par message privé.

Création d'un système de matchmaking pour Dota 2014

En cliquant sur le lien, le joueur se connecte au serveur de jeu, et ensuite le reste se déroule tout seul. Après environ 25 minutes, la « salle » virtuelle avec les joueurs se vide.

Je m'excuse d'avance pour la maladresse de l'article, cela fait longtemps que je n'ai pas écrit ici et il y a trop de code pour mettre en évidence des parties importantes. C'est du n'importe quoi, en gros.

Si je vois un intérêt pour le sujet, il y aura une deuxième partie - elle présentera mes souffrances avec les plugins pour srcds (Serveur dédié Source), et probablement un système de classement et un mini-dotabuff, un site avec des statistiques de jeux.

Quelques liens :

  1. Notre site (statistiques, tableau des leaders, petite landing page et téléchargement du client)
  2. Serveur Discord

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster