Hola a todos.
Esta primavera me encontré con un proyecto en el que los chicos aprendieron a lanzar un servidor de Dota 2 de la versión de 2014 y, por lo tanto, jugar en él. Soy un gran fanático de este juego y no pude resistir la oportunidad única de sumergirme en mi infancia.
Me sumergí muy profundamente, y así fue como escribí un bot de Discord que responde prácticamente por toda la funcionalidad que no se admite en la antigua versión del juego, a saber, el emparejamiento.
Antes de todas las innovaciones con el bot, el lobby se creaba manualmente. Se recogían 10 reacciones en un mensaje y se reunía el servidor manualmente, o se alojaba un lobby local.

Mi naturaleza de programador no soportó tal cantidad de trabajo manual, y en una noche esbocé la versión más simple del bot, que levantaba automáticamente el servidor cuando se reunían 10 personas.
Decidí escribirlo rápidamente en nodejs, porque no me gusta mucho 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 sencillo. El módulo oficial npm discord.js proporciona una interfaz cómoda para trabajar con mensajes, recolección de reacciones, etc.
Descargo de responsabilidad: todos los ejemplos de código son «actuales», es decir, han pasado por varias iteraciones de reescritura nocturna.
La base del emparejamiento es una «cola», en la que se colocan los jugadores que quieren jugar, y se eliminan cuando ya no quieren o han encontrado juego.
Así es como se ve la entidad «jugador». Inicialmente, era simplemente el id del usuario en Discord, pero planeo un lanzador/búsqueda de juego desde el sitio, pero todo a su debido tiempo.
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 la cola. Aquí en lugar de «jugadores» se utiliza una abstracción en forma de «grupo». Para un jugador individual, el grupo consiste en él mismo, y para los jugadores en un grupo, por lo tanto, en 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í utilizar eventos para intercambiar contexto. Se adaptaba a los casos: al evento "se encontró un juego para 10 personas", se puede enviar un mensaje privado a los jugadores necesario, y ejecutar la lógica empresarial principal: iniciar una tarea para verificar la preparación, preparar el lobby para el lanzamiento, y así sucesivamente.
Para IOC utilizo InversifyJS. Tengo una buena experiencia trabajando con esta biblioteca. ¡Rápido y fácil!
Tenemos varias colas en nuestro servidor: se agregaron modos 1x1, normal/ranking, y un par de personalizados. Por lo tanto, hay un singleton RoomService que se sitúa entre el usuario y la búsqueda de 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))) {
// si la cola tiene este partido, lo volvemos a agregar
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))) {
// si la cola tiene este partido, lo eliminamos
q.remove(event.party)
}
});
}
);
}
(Código de ejemplo para ilustrar cómo se ven los procesos)
Aquí inicializo la cola para cada uno de los modos de juego implementados, así como escucho los cambios en los "grupos" para ajustar las colas y evitar algunos conflictos.
Así que, soy genial, inserté fragmentos de código que no tienen nada que ver con el tema, y ahora pasemos directamente al matchmaking.
Consideremos el caso:
1) El usuario quiere jugar.
2) Para comenzar la búsqueda, utiliza Gateway=Discord, es decir, reacciona a un mensaje:

3) Este gateway va a RoomService y dice: "El usuario de Discord quiere unirse a la cola, modo: juego no clasificado".
4) RoomService acepta la solicitud del gateway y coloca al usuario (más exactamente, al grupo del usuario) en la cola adecuada.
5) La cola verifica con cada cambio si hay suficientes jugadores para jugar. Si es posible, emitimos el evento:
private onRoomFound(players: Party[]) {
this.emit("room-found", {
players,
});
}
6) RoomService, evidentemente, escucha con gusto cada cola en ansiosa espera de este evento. Al comienzo, recibimos una lista de jugadores, formamos una "habitación" virtual y, por supuesto, emitimos el evento:
queue.addListener("room-found", (event: RoomFoundEvent) => {
console.log(
`Modo de sala encontrada: [${mode}]. Es hora de conseguir una sala gratuita para estos chicos`
);
const room = this.getFreeRoom(mode);
room.fill(event.players);
this.onRoomFormed(room);
});
7) Aquí llegamos a la "instancia superior" — la clase Bot. En general, se encarga de la comunicación entre los gateways (qué gracioso suena esto en español) y la lógica empresarial del emparejamiento. El bot escucha el evento y ordena a DiscordGateway que envíe a todos los usuarios una verificación de disponibilidad.

8) Si alguien rechaza o no acepta el juego en 3 minutos, NO los devolvemos a la cola. A todos los demás los devolvemos a la cola y esperamos a que se vuelvan a reunir 10 personas. Si todos los jugadores aceptan el juego, comienza la parte interesante.
Configuración del servidor dedicado
Nuestros juegos se hospedan en un VDS con Windows Server 2012. De esto se pueden hacer varias conclusiones:
- No hay docker en él, lo cual me golpó en el corazón
- Estamos ahorrando en el alquiler
La tarea es: ejecutar un proceso en el VDS desde un VPS en Linux. Escribí un servidor simple en Flask. Sí, no me gusta Python, pero qué se le va a hacer — escribir este servidor fue más rápido y fácil.
Realiza 3 funciones:
- Inicio del servidor con configuración — selección del mapa, número de jugadores para iniciar el juego y conjunto de plugins. No hablaré de los plugins ahora, es una historia aparte con litros de café de noche mezclados con lágrimas y pelos arrancados.
- Detención/reinicio del servidor en caso de conexiones fallidas, que solo podemos manejar manualmente.
Aquí todo es simple, ejemplos de código incluso son inapropiados. Un script de 100 líneas
Así que, cuando 10 personas se reúnen y aceptan el juego, el servidor se inicia y todos están ansiosos por jugar, les llega un enlace de conexión al juego por mensaje privado.

Al hacer clic en el enlace, el jugador se conecta al servidor del juego, y a partir de ahí todo fluye. Después de aproximadamente 25 minutos, la "habitación" virtual con los jugadores se vacía.
Pido disculpas de antemano por la falta de coherencia del artículo, no he escrito aquí desde hace tiempo, y hay demasiado código para destacar las secciones importantes. Una mezcla de ideas, en resumen.
Si veo interés en el tema, habrá una segunda parte, donde compartiré mis sufrimientos con los plugins para srcds (Servidor dedicado de Source), y probablemente un sistema de clasificación y un mini-dotabuff, un sitio con estadísticas de juegos.
Algunos enlaces:
Fuente: habr.com
