Scriviamo un matchmaking per Dota 2014

Ciao a tutti.

Questa primavera mi sono imbattuto in un progetto in cui ragazzi sono riusciti a lanciare un server di Dota 2 della versione 2014 e, di conseguenza, a giocare. Sono un grande fan di questo gioco e non ho potuto resistere a questa unica opportunità di rivivere la mia infanzia.

Mi sono immerso così profondamente che ho scritto un bot per Discord, che gestisce praticamente tutta la funzionalità non supportata nella vecchia versione del gioco, ovvero il matchmaking.
Prima di tutti gli aggiornamenti, con il bot i lobby venivano creati manualmente. Raccoglievamo 10 reazioni a un messaggio e poi costituivamo il server o ospitavamo un lobby locale.

Scriviamo un matchmaking per Dota 2014

La mia natura da programmatore non ha retto un tale carico di lavoro manuale, e in una notte ho abbozzato la versione più semplice del bot, che alzava automaticamente il server quando si raggiungevano 10 persone.

Ho scelto di scrivere in nodejs, perché non amo molto Python e mi sento più a mio agio in questo ambiente.

Questo è il mio primo tentativo di scrivere un bot per Discord, ma è risultato tutto molto semplice. Il modulo npm ufficiale discord.js offre un'interfaccia comoda per gestire i messaggi, la raccolta delle reazioni, e così via.

Disclaimer: tutti gli esempi di codice sono "attuali", cioè sono stati riscritti in più iterazioni durante le notti.

La base del matchmaking è la "coda", in cui vengono inseriti i giocatori che vogliono giocare e rimossi quando cambiano idea o trovano una partita.

Ecco come appare l'entità "giocatore". Inizialmente era solo l'id dell'utente su Discord, ma sono previsti un launcher/ricerca partita dal sito, ma andiamo con ordine.

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

Ecco l'interfaccia della coda. Qui, invece dei "giocatori", viene usata un'astrazione in forma di "gruppo". Per un singolo giocatore, il gruppo è costituito da lui stesso, mentre per i giocatori in gruppo, essenzialmente da tutti i membri del gruppo.

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
}

Ho deciso di usare eventi per lo scambio di contesto. Si adattava ai casi: per l'evento "partita trovata per 10 persone" è possibile inviare un messaggio privato ai giocatori e anche eseguire la logica di business principale: avviare un task per verificare la preparazione, preparare il lobby per l'avvio, e così via.

Per l'IOC utilizzo InversifyJS. Ho avuto una piacevole esperienza di lavoro con questa libreria. Veloce e semplice!

Abbiamo diverse code sul server — sono stati aggiunti i modi 1v1, normale/ranked e un paio di custom. Perciò esiste un singleton RoomService, che si colloca tra l'utente e la ricerca della partita.

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))) {
            // se la coda ha questo party, lo riaggiungiamo
            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))) {
            // se la coda ha questo party, lo rimuoviamo
            q.remove(event.party)
          }
        });
      }
    );
  }

(Frammenti di codice per illustrare come funzionano i processi)

Qui inizializzo la coda per ciascuno dei modi di gioco implementati e ascolto i cambiamenti delle "gruppi", per correggere le code ed evitare alcuni conflitti.

Quindi, ho fatto bene, ho inserito pezzi di codice che non hanno nulla a che vedere con l'argomento, e ora passiamo al matchmaking.

Diamo un'occhiata a questo caso:

1) L'utente vuole giocare.

2) Per iniziare a cercare, utilizza il Gateway=Discord, cioè mette una reazione a un messaggio:

Scriviamo un matchmaking per Dota 2014

3) Questo gateway va a RoomService e dice "L'utente da Discord vuole entrare in coda, modalità: gioco non classato".

4) RoomService accetta la richiesta del gateway e inserisce l'utente (più precisamente, il gruppo dell'utente) nella coda corretta.

5) La coda verifica ad ogni cambiamento se ci sono abbastanza giocatori per il gioco. Se sì, emettiamo l'evento:

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

6) RoomService, ovviamente, ascolta ogni coda in attesa con trepidazione di questo evento. In ingresso riceviamo l'elenco dei giocatori, formiamo una "stanza" virtuale e, naturalmente, emettiamo l'evento:

queue.addListener("room-found", (event: RoomFoundEvent) => {
      console.log(
        `Modalità sala trovata: [${mode}]. È il momento di ottenere una sala gratuita per questi ragazzi`
      );
      const room = this.getFreeRoom(mode);
      room.fill(event.players);

      this.onRoomFormed(room);
    });

7) Siamo arrivati all'«istanza» finale — la classe Bot. In generale, si occupa della comunicazione tra i gateway (è divertente come suoni in russo) e della logica di business del matchmaking. Il bot ascolta l'evento e ordina a DiscordGateway di inviare a tutti gli utenti una verifica di prontezza.

Scriviamo un matchmaking per Dota 2014

8) Se qualcuno rifiuta o non accetta la partita entro 3 minuti, NON li riportiamo in coda. Riportiamo in coda tutti gli altri e aspettiamo che si raggiungano di nuovo 10 persone. Se tutti i giocatori hanno accettato la partita, inizia la parte interessante.

Configurazione del server dedicato

I nostri giochi sono ospitati su VDS con Windows Server 2012. Da ciò possiamo trarre alcune conclusioni:

  1. Non c'è Docker, il che mi ha colpito al cuore
  2. Stiamo risparmiando sull'affitto

L'obiettivo è: avviare un processo su VDS a partire da VPS su Linux. Ho scritto un semplice server su Flask. Sì, non amo Python, ma che posso farci — scrivere questo server è stato più veloce e semplice.

Esso svolge 3 funzioni:

  1. Avvio del server con configurazione — scelta della mappa, numero di giocatori per l'inizio della partita, e insieme di plugin. Non scriverò di plugin adesso — è una storia a parte con litri di caffè mescolati a lacrime e capelli strappati.
  2. Arresto/riavvio del server in caso di connessioni fallite, che possiamo gestire solo manualmente.

Qui tutto è semplice, esempi di codice sarebbero fuori luogo. Uno script di 100 righe

Quindi, quando 10 persone si sono riunite e hanno accettato la partita, il server è attivato e tutti sono desiderosi di giocare, nei messaggi privati arriva un link per connettersi al gioco.

Scriviamo un matchmaking per Dota 2014

Cliccando sul link, il giocatore si connette al server di gioco, e poi il resto avviene automaticamente. Dopo ~25 minuti la «stanza» virtuale con i giocatori si svuota.

Mi scuso in anticipo per la mancanza di coerenza dell'articolo, è passato del tempo dall'ultima volta che ho scritto qui, e ci sono troppi codici per evidenziare le parti importanti. Una sorta di spaghetti, insomma.

Se vedo interesse per l'argomento, ci sarà una seconda parte — in essa parlerò delle mie sofferenze con i plugin per srcds (Source Dedicated Server), e probabilmente del sistema di rating e un mini-dotabuff, un sito con le statistiche delle partite.

Alcuni link:

  1. Il nostro sito (statistiche, classifica, una piccola landing page e download del client)
  2. Server Discord

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS | ProHoster