Escribiendo emparejamiento para Dota 2014

Hola.



Esta primavera me encontré con un proyecto en el que los chicos aprendieron cómo ejecutar el servidor Dota 2 de la versión 2014 y, en consecuencia, jugar con él. Soy un gran fan de este juego y no podía dejar pasar la oportunidad única de sumergirme en mi infancia.



Me sumergí muy profundamente, y sucedió que escribí un bot de Discord, que es responsable de casi todas las funciones que no son compatibles con la versión anterior del juego, es decir, el emparejamiento.

Antes de todas las innovaciones con el bot, el lobby se creaba manualmente. Recopilamos 10 respuestas a un mensaje y ensamblamos manualmente un servidor o organizamos un lobby local.







Mi naturaleza como programador no soportaba tanto trabajo manual, y de la noche a la mañana esbocé la versión más simple del bot, que automáticamente activaba el servidor cuando se reclutaban 10 personas.



Decidí escribir de inmediato en nodejs, porque realmente no me gusta Python y me siento más cómodo en este entorno.



Esta es mi primera experiencia escribiendo un bot para Discord, pero resultó ser muy simple. El módulo oficial de npm discord.js proporciona una interfaz conveniente para trabajar con mensajes, recopilar reacciones, etc.



Descargo de responsabilidad: todos los ejemplos de código están "actualizados", lo que significa que han pasado por varias iteraciones de reescritura durante la noche.



El núcleo del emparejamiento es una "cola" donde los jugadores que quieren jugar son colocados y eliminados cuando no quieren o no encuentran un juego.



Así es como se ve la esencia del "jugador". Inicialmente, era solo una identificación de usuario en Discord, pero los planes incluyen un lanzador / búsqueda de un juego en el sitio, pero lo primero es lo primero.



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


Y aquí está la interfaz de cola. Aquí, en lugar de "jugadores", se utiliza una abstracción en forma de "grupo". Para un solo jugador, el grupo está formado por él mismo, y para los jugadores de un grupo, respectivamente, por todos los jugadores del grupo.



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
}


Decidí usar eventos para intercambiar contexto. Adecuado para casos: para el evento "encontré un juego para 10 personas", puede enviar el mensaje deseado a los jugadores en mensajes privados y ejecutar la lógica comercial principal: lanzar una tarea para verificar la preparación, preparar el lobby para el lanzamiento, etc.



Para IOC, estoy usando InversifyJS. Tengo una experiencia agradable con esta biblioteca. ¡Rapido y facil!



Tenemos varias colas en el servidor: hemos agregado modos 1x1, normal / calificación y un par de personalizados. Por lo tanto, existe un RoomService singleton que se encuentra entre el usuario y la búsqueda del juego.



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


(Codifique fideos para representar cómo se ven los procesos aproximadamente)



Aquí inicializo una cola para cada uno de los modos de juego implementados, y también escucho los cambios en los "grupos" para corregir las colas y evitar algunos conflictos.



Entonces, estoy genial, inserté fragmentos de código que no tienen nada que ver con el tema, y ​​ahora pasemos directamente a la creación de mastines.



Considere un caso:



1) El usuario quiere jugar.



2) Para iniciar la búsqueda, usa Gateway = Discord, es decir, pone una reacción al mensaje:







3) Este gateway va a RoomService, y dice "El usuario de la discordia quiere ingresar a la cola, modo: juego sin clasificar".



4) RoomService acepta la solicitud de la puerta de enlace y la coloca en la cola de usuarios deseada (más precisamente, el grupo de usuarios).



5) La cola comprueba cada cambio para ver si hay suficientes jugadores para jugar. Si es posible, emita un evento:



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


6) RoomService obviamente se complace en escuchar cada cola con ansiosa anticipación a este evento. En la entrada recibimos una lista de jugadores, formamos una "sala" virtual de ellos y, por supuesto, emitimos un evento:



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) Así que llegamos a la instancia "más alta": la clase Bot . En general, se ocupa de la conexión entre las puertas de enlace (qué ridículo se ve en ruso, no puedo) y la lógica empresarial del emparejamiento. El bot escucha a escondidas el evento y ordena a DiscordGateway que envíe un cheque de preparación a todos los usuarios.







8) Si alguien rechazó o no aceptó el juego en 3 minutos, NO lo devolveremos a la cola. Devolvemos a todos los demás a la cola y esperamos a que se recluten nuevamente 10 personas. Si todos los jugadores han aceptado el juego, entonces comienza la parte divertida.



Configuración de servidor dedicado



Nuestros juegos están alojados en VDS con servidor Windows 2012. De esto se pueden sacar varias conclusiones:



  1. No hay acoplador, lo que golpeó mi corazón
  2. Ahorramos en alquiler


La tarea es iniciar el proceso en VDS con VPS en Linux. Escribió un servidor simple en Flask. Sí, no me gusta Python, pero ¿qué puedo hacer? Escribir este servidor en él es más rápido y fácil.



Tiene 3 funciones:



  1. Inicio del servidor con configuración: selección de mapa, número de jugadores para iniciar el juego y un conjunto de complementos. No escribiré sobre complementos ahora; esta es una historia separada con litros de café por la noche mezclados con lágrimas y cabello desgarrado.
  2. Detención / reinicio del servidor en caso de conexiones fallidas, que solo podemos manejar manualmente.


Todo es simple aquí, los ejemplos de código son incluso inapropiados. Script para 100 líneas



Entonces, cuando 10 personas se reunieron y aceptaron el juego, el servidor está funcionando y todos están ansiosos por jugar, un enlace para conectarse al juego viene en mensajes privados.







Al hacer clic en el enlace, el jugador se conecta al servidor del juego y eso es todo. Después de ~ 25 minutos, se despeja la "sala" virtual con los jugadores.



Pido disculpas de antemano por la incomodidad del artículo, no he escrito aquí durante mucho tiempo y hay demasiado código para resaltar secciones importantes. Fideos, en resumen.



Si veo interés en el tema, habrá una segunda parte: contendrá mi tormento con complementos para srcds (servidor dedicado de origen) y, probablemente, un sistema de clasificación y mini-dotabuff, un sitio con estadísticas del juego.



Pocos enlaces:



  1. Nuestro sitio (estadísticas, clasificación, pequeños landos y descarga de clientes)
  2. Servidor de discordia



All Articles