Escriptura de matchmaking per a Dota 2014

Hola a tots

Aquesta primavera em vaig trobar amb un projecte en què els nois van aprendre a executar la versió del servidor Dota 2 2014 i, en conseqüència, jugar-hi. Sóc un gran fan d'aquest joc, i no podia deixar passar aquesta oportunitat única de submergir-me en la meva infància.

Em vaig submergir molt profundament, i va passar que vaig escriure un bot de Discord que és responsable de gairebé totes les funcionalitats que no s'admeten a la versió antiga del joc, és a dir, el matchmaking.
Abans de totes les innovacions amb el bot, el lobby es va crear manualment. Vam recollir 10 reaccions a un missatge i vam muntar manualment un servidor o vam allotjar un vestíbul local.

Escriptura de matchmaking per a Dota 2014

La meva naturalesa com a programador no podia suportar tant de treball manual, i d'un dia per l'altre vaig dibuixar la versió més senzilla del bot, que augmentava automàticament el servidor quan hi havia 10 persones.

De seguida vaig decidir escriure en nodejs, perquè no m'agrada molt Python i em sento més còmode en aquest entorn.

Aquesta és la meva primera experiència escrivint un bot per a Discord, però va resultar molt senzill. El mòdul oficial npm discord.js proporciona una interfície convenient per treballar amb missatges, recollir reaccions, etc.

Exempció de responsabilitat: tots els exemples de codi són "actuals", és a dir, han passat per diverses iteracions de reescriptura a la nit.

La base del matchmaking és una "cua" en què els jugadors que volen jugar són col·locats i eliminats quan no volen o troben un joc.

Així és l'essència d'un "jugador". Inicialment era només un identificador d'usuari a Discord, però hi ha plans per llançar/cercar jocs des del lloc, però primer és el primer.

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

I aquí teniu la interfície de la cua. Aquí, en lloc de "jugadors", s'utilitza una abstracció en forma de "grup". Per a un sol jugador, el grup està format per ell mateix, i per als jugadors d'un grup, respectivament, per tots els jugadors del grup.

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
}

Vaig decidir utilitzar esdeveniments per intercanviar context. Era adequat per a casos: quan es va trobar un joc per a 10 persones, podeu enviar el missatge necessari als jugadors en missatges privats i dur a terme la lògica empresarial bàsica: llançar una tasca per comprovar la preparació, preparar el vestíbul. per al llançament, etc.

Per a IOC faig servir InversifyJS. Tinc una experiència agradable treballant amb aquesta biblioteca. Ràpid i fàcil!

Tenim diverses cues al nostre servidor: hem afegit 1x1, normal/classificat i un parell de modes personalitzats. Per tant, hi ha un Singleton RoomService que es troba entre l'usuari i la cerca del joc.

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

(Codeu fideus per donar-vos una idea de com són aproximadament els processos)

Aquí inicialitzo la cua per a cadascun dels modes de joc implementats, i també escolto els canvis en "grups" per tal d'ajustar les cues i evitar alguns conflictes.

Així que, ben fet, he inserit fragments de codi que no tenen res a veure amb el tema, i ara passem directament al matchmaking.

Considerem el cas:

1) L'usuari vol jugar.

2) Per iniciar la cerca, utilitza Gateway=Discord, és a dir, posa una reacció al missatge:

Escriptura de matchmaking per a Dota 2014

3) Aquesta passarel·la va a RoomService i diu "Un usuari de Discord vol entrar a la cua, mode: joc sense classificació".

4) RoomService accepta la sol·licitud de la passarel·la i empeny l'usuari (més precisament, el grup d'usuaris) a la cua desitjada.

5) La cua es comprova cada cop que hi ha prou jugadors per jugar. Si és possible, emet un esdeveniment:

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

6) Òbviament, RoomService està escoltant feliçment totes les cues en espera d'aquest esdeveniment. Rebem una llista de jugadors com a entrada, formem una "sala" virtual d'ells i, per descomptat, emetem un esdeveniment:

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) Així que vam arribar a la "màxima" autoritat: la classe Bot. En general, tracta la connexió entre les passarel·les (no entenc el divertit que sembla en rus) i la lògica empresarial del matchmaking. El bot escolta l'esdeveniment i ordena a DiscordGateway que enviï un control de preparació a tots els usuaris.

Escriptura de matchmaking per a Dota 2014

8) Si algú rebutja o no accepta el joc en 3 minuts, NO el retornem a la cua. Tornem la resta a la cua i esperem fins que tornin a ser 10 persones. Si tots els jugadors han acceptat el joc, llavors comença la part interessant.

Configuració del servidor dedicat

Els nostres jocs estan allotjats a VDS amb Windows Server 2012. D'això en podem extreure diverses conclusions:

  1. No hi ha cap docker, que em va colpejar al cor
  2. Estalviem en lloguer

La tasca és executar un procés a VDS des d'un VPS a Linux. Vaig escriure un servidor senzill a Flask. Sí, no m'agrada Python, però què pots fer? És més ràpid i fàcil escriure aquest servidor en ell.

Fa 3 funcions:

  1. Iniciar un servidor amb una configuració: seleccionar un mapa, el nombre de jugadors per iniciar el joc i un conjunt de connectors. Ara no escriuré sobre connectors: aquesta és una història diferent amb litres de cafè a la nit barrejats amb llàgrimes i cabells esquinçats.
  2. Aturar/reiniciar el servidor en cas de connexions no reeixides, que només podem gestionar manualment.

Aquí tot és senzill, els exemples de codi ni tan sols són adequats. Guió de 100 línies

Així, quan 10 persones es van reunir i van acceptar el joc, es va posar en marxa el servidor i tothom tenia ganes de jugar, es va enviar un enllaç per connectar-se al joc en missatges privats.

Escriptura de matchmaking per a Dota 2014

En fer clic a l'enllaç, el jugador es connecta al servidor del joc i això és tot. Després de ~25 minuts, s'esborra la "habitació" virtual amb jugadors.

Demano disculpes per endavant per la incomoditat de l'article, fa molt de temps que no escric aquí i hi ha massa codi per destacar les seccions importants. Fideus, en definitiva.

Si veig interès en el tema, hi haurà una segona part: contindrà el meu turment amb connectors per a srcds (servidor dedicat a la font) i, probablement, un sistema de classificació i mini-dotabuff, un lloc amb estadístiques de joc.

Alguns enllaços:

  1. El nostre lloc web (estadístiques, classificació, petita pàgina de destinació i descàrrega del client)
  2. Servidor de Discord

Font: www.habr.com

Afegeix comentari