Rédaction de matchmaking pour Dota 2014

Bonjour.

Ce printemps, je suis tombé sur un projet dans lequel les gars ont appris à exécuter le serveur Dota 2 version 2014 et, par conséquent, à y jouer. Je suis un grand fan de ce jeu et je ne pouvais pas laisser passer cette occasion unique de me replonger dans mon enfance.

J'ai plongé très profondément, et il se trouve que j'ai écrit un robot Discord qui est responsable de presque toutes les fonctionnalités qui ne sont pas prises en charge dans l'ancienne version du jeu, à savoir le matchmaking.
Avant toutes les innovations du bot, le lobby était créé manuellement. Nous avons collecté 10 réactions à un message et assemblé manuellement un serveur ou hébergé un lobby local.

Rédaction de matchmaking pour Dota 2014

Ma nature de programmeur ne pouvait pas supporter autant de travail manuel, et du jour au lendemain, j'ai esquissé la version la plus simple du bot, qui faisait automatiquement monter le serveur lorsqu'il y avait 10 personnes.

J'ai tout de suite décidé d'écrire 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 cela s'est avéré très simple. Le module officiel npm discord.js fournit une interface pratique pour travailler avec des messages, collecter des réactions, etc.

Avertissement : tous les exemples de code sont « à jour », ce qui signifie qu'ils ont subi plusieurs itérations de réécriture la nuit.

La base du matchmaking est une « file d’attente » dans laquelle les joueurs qui veulent jouer sont placés et retirés lorsqu’ils ne veulent pas ou ne trouvent pas de partie.

Voilà à quoi ressemble l’essence d’un « joueur ». Au départ, il s'agissait simplement d'un identifiant d'utilisateur dans Discord, mais il est prévu de lancer/rechercher des jeux à partir du site, mais commençons par le commencement.

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

Et voici l'interface de file d'attente. Ici, au lieu de « joueurs », une abstraction sous la forme d'un « groupe » est utilisée. Pour un seul joueur, le groupe est constitué de lui-même, et pour les joueurs d'un groupe, respectivement, de tous les joueurs 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 du contexte. Cela convenait aux cas - lors de l'événement "un jeu pour 10 personnes a été trouvé", vous pouvez envoyer le message nécessaire aux joueurs dans des messages privés et exécuter la logique métier de base - lancer une tâche pour vérifier l'état de préparation, préparer le lobby pour le lancement, etc.

Pour IOC, j'utilise InversifyJS. J'ai une expérience agréable en travaillant avec cette bibliothèque. Rapide et facile!

Nous avons plusieurs files d'attente sur notre serveur - nous avons ajouté 1x1, normal/noté et quelques modes personnalisés. Par conséquent, il existe un RoomService singleton qui se situe 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))) {
            // 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)
          }
        });
      }
    );
  }

(Codez les nouilles pour donner une idée de ce à quoi ressemblent grossièrement les processus)

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

Alors bravo, j'ai inséré des bouts de code qui n'ont rien à voir avec le sujet, et maintenant passons directement au matchmaking.

Considérons le cas :

1) L'utilisateur veut jouer.

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

Rédaction de matchmaking pour Dota 2014

3) Cette passerelle accède à 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 pousse l'utilisateur (plus précisément, le groupe d'utilisateurs) dans la file d'attente souhaitée.

5) La file d'attente vérifie chaque fois qu'il y a suffisamment de joueurs pour jouer. Si possible, émettez un événement :

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

6) RoomService écoute évidemment avec plaisir chaque file d'attente en attendant avec impatience cet événement. Nous recevons une liste de joueurs en entrée, formons une « salle » virtuelle à partir d'eux et, bien sûr, organisons un événement :

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) Nous sommes donc arrivés à l'autorité « la plus élevée » - la classe Bot. En général, il traite du lien entre les passerelles (je ne comprends pas à quel point ça a l'air drôle en russe) et la logique commerciale du matchmaking. Le robot surprend l'événement et ordonne à DiscordGateway d'envoyer un contrôle de préparation à tous les utilisateurs.

Rédaction de matchmaking pour Dota 2014

8) Si quelqu'un rejette ou n'accepte pas le jeu dans les 3 minutes, nous ne le remettons PAS dans la file d'attente. Nous remettons tout le monde dans la file d'attente et attendons qu'il y ait à nouveau 10 personnes. Si tous les joueurs ont accepté le jeu, alors la partie intéressante commence.

Configuration du serveur dédié

Nos jeux sont hébergés sur VDS avec serveur Windows 2012. De là, nous pouvons tirer plusieurs conclusions :

  1. Il n'y a pas de docker dessus, ce qui m'a frappé au cœur
  2. Nous économisons sur le loyer

La tâche consiste à exécuter un processus sur VDS à partir d'un VPS sous Linux. J'ai écrit un simple serveur dans Flask. Oui, je n'aime pas Python, mais que puis-je faire ? C'est plus rapide et plus facile d'écrire ce serveur dessus.

Il remplit 3 fonctions :

  1. Démarrage d'un serveur avec une configuration - sélection d'une carte, du nombre de joueurs pour démarrer le jeu et d'un ensemble de plugins. Je n'écrirai pas sur les plugins maintenant - c'est une autre histoire avec des litres de café le soir mélangés à des larmes et des cheveux arrachés.
  2. Arrêt/redémarrage du serveur en cas d'échec de connexion, que nous ne pouvons gérer que manuellement.

Tout est simple ici, les exemples de code ne sont même pas appropriés. script de 100 lignes

Ainsi, lorsque 10 personnes se sont réunies et ont accepté le jeu, le serveur a été lancé et tout le monde avait hâte de jouer, un lien pour se connecter au jeu a été envoyé en messages privés.

Rédaction de matchmaking pour Dota 2014

En cliquant sur le lien, le joueur se connecte au serveur de jeu, et c'est tout. Après environ 25 minutes, la « salle » virtuelle avec les joueurs est vidée.

Je m'excuse par avance pour la maladresse de l'article, je n'ai pas écrit ici depuis longtemps et il y a trop de code pour mettre en évidence les sections importantes. Des nouilles, en bref.

Si je vois de l'intérêt pour le sujet, il y aura une deuxième partie - elle contiendra mon tourment avec des plugins pour srcds (serveur dédié Source), et, probablement, un système de notation et un mini-dotabuff, un site avec des statistiques de jeu.

Quelques liens :

  1. Notre site Web (statistiques, classement, petite page d'accueil et téléchargement client)
  2. Serveur Discorde

Source: habr.com

Ajouter un commentaire