We schrijven matchmaking voor Dota uit 2014

Hallo allemaal.

Deze lente stuitte ik op een project waarin de jongens geleerd hadden om een Dota 2-server van de versie 2014 op te zetten en er ook op te spelen. Ik ben een grote fan van dit spel en kon deze unieke kans om in mijn kindertijd te duiken niet voorbij laten gaan.

Ik dook er heel diep in en zo gebeurde het dat ik een Discord-bot schreef die praktisch alle functionaliteit dekte die niet werd ondersteund in de oude versie van het spel, namelijk matchmaking.
Voordat alle vernieuwde functies met de bot beschikbaar waren, werd de lobby handmatig aangemaakt. We verzamelden 10 reacties op een bericht en stelden handmatig de server samen of hostten een lokale lobby.

We schrijven matchmaking voor Dota uit 2014

Mijn programmeernatuur hield het niet vol om zoveel handwerk te doen, en in de nacht heb ik de eenvoudigste versie van de bot in elkaar gezet, die automatisch de server opstartte wanneer er 10 mensen aanwezig waren.

Ik besloot meteen om in nodejs te schrijven, omdat ik Python niet echt leuk vind, en ik me comfortabeler voel in deze omgeving.

Dit is mijn eerste ervaring met het schrijven van een bot voor Discord, maar het bleek eigenlijk heel eenvoudig te zijn. De officiƫle npm-module discord.js biedt een handige interface voor het werken met berichten, reacties verzamelen, enzovoort.

Disclaimer: alle voorbeeldcodes zijn 'actueel', wat betekent dat ze verschillende iteraties van herschrijven 's nachts hebben doorlopen.

De basis van matchmaking is een 'wachtrij' waarin spelers worden geplaatst die willen spelen, en ze worden verwijderd wanneer ze niet meer willen of een spel gevonden hebben.

Zo ziet de entiteit 'speler' eruit. Oorspronkelijk was dit gewoon een id van de gebruiker in Discord, maar er zijn plannen voor een launcher/spel zoeken vanaf de website, maar dat komt later.

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

En hier is de interface van de wachtrij. In plaats van 'spelers' wordt een abstractie in de vorm van een 'groep' gebruikt. Voor een enkele speler bestaat de groep uit henzelf, en voor spelers in een groep uit alle spelers in die groep.

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
}

Ik heb besloten om evenementen te gebruiken voor het uitwisselen van context. Dit past goed bij de cases - wanneer er een 'spel gevonden voor 10 mensen' evenement plaatsvindt, kan ik ook een bericht naar de persoonlijke berichten van de spelers sturen, en de belangrijkste bedrijfslogica uitvoeren - een taak starten om de gereedheid te controleren, de lobby voorbereiden voor de lancering, enzovoort.

Voor IOC gebruik ik InversifyJS. Ik heb een prettige ervaring met deze bibliotheek. Snel en eenvoudig!

We hebben meerdere wachtrijen op de server - de modi 1x1, standaard/ranglijst, en een paar custom games zijn toegevoegd. Daarom is er een singleton RoomService, die tussen de gebruiker en de game-zoekfunctie ligt.

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))) {
            // als de wachtrij deze partij heeft, voegen we de partij opnieuw toe
            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))) {
            // als de wachtrij deze partij heeft, verwijderen we de partij
            q.remove(event.party)
          }
        });
      }
    );
  }

(Codeomslag voor de voorstelling, hoe de processen eruitzien)

Hier initialiseert ik de wachtrij voor elk van de geĆÆmplementeerde spelmodus, en luister ik naar de veranderingen van 'groepen' om de wachtrijen aan te passen en sommige conflicten te vermijden.

Dus, goed gedaan, ik heb stukken code ingevoegd die niets met het onderwerp te maken hebben, laten we nu echt naar matchmaking gaan.

Laten we de case bekijken:

1) De gebruiker wil spelen.

2) Om te beginnen met zoeken, gebruikt hij Gateway=Discord, dat wil zeggen dat hij een reactie op een bericht plaatst:

We schrijven matchmaking voor Dota uit 2014

3) Deze gateway gaat naar RoomService, en zegt "De gebruiker uit Discord wil de wachtrij in, modus: casual game".

4) RoomService accepteert het verzoek van de gateway, en plaatst de gebruiker (precies gezegd, de groep van de gebruiker) in de juiste wachtrij.

5) De wachtrij controleert bij elke wijziging of er voldoende spelers zijn voor het spel. Als het kan - emit een gebeurtenis:

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

6) RoomService luistert natuurlijk met veel plezier naar elke wachtrij in afwachting van dit evenement. We ontvangen een lijst met spelers, vormen er een virtuele "kamer" van, en uiteraard stoten we het evenement uit:

queue.addListener("room-found", (event: RoomFoundEvent) => {
      console.log(
        `Room found mode: [${mode}]. Tijd om een gratis kamer voor deze jongens te krijgen`
      );
      const room = this.getFreeRoom(mode);
      room.fill(event.players);

      this.onRoomFormed(room);
    });

7) Hier zijn we dan bij de "hoogste" instantie — de klasse Bot. Over het algemeen houdt deze verband met de communicatie tussen gateways (hoe grappig dit ook in het Nederlands klinkt, kan ik niet helpen) en de businesslogica van matchmaking. De bot luistert naar het evenement en geeft DiscordGateway de opdracht om alle gebruikers een klaarheidscontrole te sturen.

We schrijven matchmaking voor Dota uit 2014

8) Als iemand de game binnen 3 minuten heeft afgewezen of niet heeft geaccepteerd, dan zetten we ze NIET terug in de wachtrij. Alle anderen worden terug in de wachtrij gezet en we wachten tot er opnieuw 10 personen zijn. Als alle spelers de game hebben geaccepteerd, begint het interessante gedeelte.

Configuratie van de dedicated server

Onze games worden gehost op VDS met Windows Server 2012. Hieruit kunnen we een paar conclusies trekken:

  1. Er is geen Docker op, wat me recht in het hart raakte.
  2. We besparen op huurkosten.

De taak is: een proces op VDS draaien vanaf VPS met Linux. Ik schreef een eenvoudige server in Flask. Ja, ik hou niet van Python, maar wat kan ik doen — het duurt sneller en eenvoudiger om deze server ermee te schrijven.

Het vervult 3 functies:

  1. Het starten van de server met configuratie — het kiezen van de kaart, het aantal spelers voor het starten van de game, en een set plugins. Over de plugins wil ik nu niets zeggen — dat is een apart verhaal met liters koffie die 's nachts vermengd zijn met tranen en uitgerukte haren.
  2. Stoppen/herstarten van de server in geval van mislukte verbindingen, die we alleen handmatig kunnen afhandelen.

Hier is alles eenvoudig, codevoorbeelden zijn zelfs niet passend. Het script is 100 regels lang.

Dus, wanneer 10 mensen samenkomen en de game hebben geaccepteerd, wordt de server gestart en is iedereen enthousiast om te spelen, ontvangt elke speler een link naar de game in hun privƩberichten.

We schrijven matchmaking voor Dota uit 2014

Bij het klikken op de link verbindt de speler met de game server, en daarna gaat alles van zelf. Na ongeveer 25 minuten wordt de virtuele "kamer" met spelers leeggemaakt.

Bij voorbaat excuses voor de onduidelijkheid van het artikel, ik heb hier al lange tijd niet meer geschreven, en er is te veel code om de belangrijke delen te benadrukken. Kortom, een grote chaos.

Als ik interesse zie in het onderwerp, komt er een tweede deel - daarin zullen mijn worstelingen met plugins voor srcds (Source dedicated server) worden besproken, en waarschijnlijk ook een ranking systeem en een mini-dotabuff, een site met speelnstatistieken.

Een paar links:

  1. Onze website (statistieken, ranglijst, een kleine landingspagina en download van de client)
  2. Discord server

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers šŸ”„ Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster