Estamos escrevendo matchmaking para Dota 2014

Olá a todos.

Nesta primavera me deparei com um projeto no qual os caras aprenderam como rodar o servidor Dota 2 versão 2014 e, consequentemente, jogar nele. Sou um grande fã deste jogo e não poderia deixar passar esta oportunidade única de mergulhar na minha infância.

Mergulhei muito fundo e aconteceu que escrevi um bot Discord que é responsável por quase todas as funcionalidades que não são suportadas na versão antiga do jogo, nomeadamente matchmaking.
Antes de todas as inovações com o bot, o lobby era criado manualmente. Coletamos 10 reações a uma mensagem e montamos manualmente um servidor ou hospedamos um lobby local.

Estamos escrevendo matchmaking para Dota 2014

Minha natureza de programador não aguentava tanto trabalho manual, e da noite para o dia esbocei a versão mais simples do bot, que acionava automaticamente o servidor quando havia 10 pessoas.

Decidi imediatamente escrever em nodejs, porque não gosto muito de Python e me sinto mais confortável neste ambiente.

Esta é minha primeira experiência escrevendo um bot para Discord, mas acabou sendo muito simples. O módulo npm oficial discord.js fornece uma interface conveniente para trabalhar com mensagens, coletar reações, etc.

Isenção de responsabilidade: todos os exemplos de código são “atuais”, o que significa que passaram por várias iterações de reescrita durante a noite.

A base do matchmaking é uma “fila” na qual os jogadores que querem jogar são colocados e removidos quando não querem ou não encontram um jogo.

É assim que se parece a essência de um “jogador”. Inicialmente era apenas um ID de usuário no Discord, mas há planos para lançar/pesquisar jogos no site, mas primeiro o mais importante.

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

E aqui está a interface da fila. Aqui, em vez de “jogadores”, é usada uma abstração na forma de “grupo”. Para um único jogador, o grupo é composto por ele mesmo, e para os jogadores de um grupo, respectivamente, por todos os jogadores do 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
}

Decidi usar eventos para trocar contexto. Foi adequado para os casos - no evento “foi encontrado um jogo para 10 pessoas”, você pode enviar a mensagem necessária aos jogadores em mensagens privadas, e realizar a lógica básica de negócios - lançar uma tarefa para verificar a prontidão, preparar o lobby para lançamento e assim por diante.

Para IOC eu uso InversifyJS. Tenho uma experiência agradável trabalhando com esta biblioteca. Rápido e fácil!

Temos várias filas em nosso servidor - adicionamos 1x1, normal/classificado e alguns modos personalizados. Portanto, existe um RoomService singleton que fica entre o usuário e a busca do jogo.

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 macarrão para dar uma ideia de como são os processos)

Aqui inicializo a fila para cada um dos modos de jogo implementados, e também ouço mudanças nos “grupos” para ajustar as filas e evitar alguns conflitos.

Então, muito bem, inseri trechos de código que não têm nada a ver com o assunto e agora vamos direto para o matchmaking.

Vamos considerar o caso:

1) O usuário quer jogar.

2) Para iniciar a busca ele utiliza Gateway=Discord, ou seja, coloca uma reação à mensagem:

Estamos escrevendo matchmaking para Dota 2014

3) Este gateway vai para RoomService e diz “Um usuário do discord deseja entrar na fila, modo: jogo sem classificação”.

4) RoomService aceita a solicitação do gateway e empurra o usuário (mais precisamente, o grupo de usuários) para a fila desejada.

5) A fila verifica sempre que há jogadores suficientes para jogar. Se possível, emita um evento:

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

6) O RoomService obviamente está ouvindo com alegria todas as filas, aguardando ansiosamente esse evento. Recebemos uma lista de jogadores como entrada, formamos uma “sala” virtual deles e, claro, emitimos um 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) Então chegamos à autoridade “mais alta” - a classe Bot. Em geral, ele trata da conexão entre gateways (não consigo entender o quão engraçado isso parece em russo) e da lógica de negócios do matchmaking. O bot ouve o evento e ordena que o DiscordGateway envie uma verificação de prontidão a todos os usuários.

Estamos escrevendo matchmaking para Dota 2014

8) Se alguém rejeitar ou não aceitar o jogo em 3 minutos, NÃO o devolveremos à fila. Devolvemos todos os demais à fila e esperamos até que haja 10 pessoas novamente. Se todos os jogadores aceitarem o jogo, começa a parte interessante.

Configuração de servidor dedicado

Nossos jogos são hospedados em VDS com Windows server 2012. Disto podemos tirar várias conclusões:

  1. Não há janela de encaixe nele, o que me atingiu no coração
  2. Economizamos no aluguel

A tarefa é executar um processo no VDS a partir de um VPS no Linux. Eu escrevi um servidor simples em Flask. Sim, não gosto de Python, mas o que posso fazer? É mais rápido e fácil escrever este servidor nele.

Desempenha 3 funções:

  1. Iniciando um servidor com uma configuração - selecionando um mapa, o número de jogadores para iniciar o jogo e um conjunto de plugins. Não vou escrever sobre plugins agora - a história é diferente com litros de café à noite misturados com lágrimas e cabelos rasgados.
  2. Parar/reiniciar o servidor em caso de conexões malsucedidas, que só podemos tratar manualmente.

Tudo é simples aqui, exemplos de código nem são apropriados. roteiro de 100 linhas

Assim, quando 10 pessoas se reuniram e aceitaram o jogo, o servidor foi lançado e todos ficaram ansiosos para jogar, um link para se conectar ao jogo foi enviado em mensagens privadas.

Estamos escrevendo matchmaking para Dota 2014

Ao clicar no link, o jogador se conecta ao servidor do jogo e pronto. Após cerca de 25 minutos, a “sala” virtual com jogadores é esvaziada.

Peço desculpas antecipadamente pela estranheza do artigo, não escrevo aqui há muito tempo e há muito código para destacar seções importantes. Macarrão, em suma.

Se eu perceber interesse no tema, haverá uma segunda parte - ela conterá meu tormento com plugins para srcds (servidor dedicado de origem), e, provavelmente, um sistema de classificação e mini-dotabuff, um site com estatísticas de jogos.

Alguns links:

  1. Nosso site (estatísticas, tabela de classificação, pequena landing page e download do cliente)
  2. Servidor de discórdia

Fonte: habr.com

Adicionar um comentário