Writing matchmaking for Dota 2014

Hello.

This spring, I came across a project in which the guys learned how to run a Dota 2 server of the 2014 version and, accordingly, play on it. I am a big fan of this game, and could not pass by a unique opportunity to plunge into my childhood.

I plunged very deeply, and it so happened that I wrote a Discord bot, which is responsible for almost all the functionality that is not supported in the old version of the game, namely matchmaking.
Before all the innovations with the bot, the lobby was created manually. We collected 10 reactions to the message and manually assembled the server, or hosted a local lobby.

Writing matchmaking for Dota 2014

My nature as a programmer could not stand this amount of manual work, and overnight I sketched the simplest version of the bot, which automatically brought up the server when 10 people were recruited.

I immediately decided to write on nodejs, because I don’t really like python, and I feel more comfortable in this environment.

This is my first time writing a bot for Discord, but it turned out to be very simple. The official discord.js npm module provides a convenient interface for working with messages, collecting reactions, and more.

Disclaimer: All code examples are "up to date", which means they went through several iterations of rewriting at night.

The basis of matchmaking is the β€œqueue”, where players who want to play are placed and removed when they don’t want to or find a game.

This is what the essence of the "player" looks like. Initially, it was just a user id in Discord, but the launcher / search for a game from the site is in the plans, but first things first.

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

And here is the queue interface. Here, instead of "players", an abstraction in the form of a "group" is used. For a single player, the group consists of himself, and for players in a group, respectively, of all the players in the group.

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
}

Decided to use events to exchange context. Suitable for cases - on the event β€œa game for 10 people was found”, you can send the necessary message to players in private messages, and execute the main business logic - launch a task to check readiness, prepare a lobby for launch, and so on.

For IOC I use InversifyJS. I have a pleasant experience with this library. Fast and easy!

We have several queues on the server - we added 1x1 modes, normal / rating, and a couple of custom ones. Therefore, there is a singleton RoomService that lies between the user and the game search.

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

(Code noodles to show what processes roughly look like)

Here I initialize the queue for each of the implemented game modes, and also listen for changes in "groups" in order to correct the queues and avoid some conflicts.

So, well done, I inserted pieces of code that have nothing to do with the topic, and now let's move on directly to mastmaking.

Consider a case:

1) The user wants to play.

2) In order to start the search, he uses Gateway=Discord, that is, puts a reaction to the message:

Writing matchmaking for Dota 2014

3) This gateway goes to RoomService and says "Discord user wants to queue, mode: unranked game".

4) RoomService accepts the request of the gateway, and pushes the user into the required queue (more precisely, the user's group).

5) The queue at each change checks if there are enough players for the game. If possible, emit an event:

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

6) RoomService is obviously happily listening to each queue in tremulous anticipation of this event. At the input, we receive a list of players, form a virtual β€œroom” from them, and, of course, emit an event:

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) So we got to the "highest" instance - the class Boots. In general, he deals with the connection between gateways (how funny it looks in Russian, I can’t) and the business logic of matchmaking. The bot eavesdrops on the event and tells DiscordGateway to send a readiness check to all users.

Writing matchmaking for Dota 2014

8) If someone rejected or did not accept the game in 3 minutes, then we will NOT return them to the queue. We return all the rest to the queue and wait until 10 people are recruited again. If all players have accepted the game, then the interesting part begins.

Dedicated server configuration

Our games are hosted on VDS with Windows server 2012. Several conclusions can be drawn from this:

  1. There is no docker on it, which hit me in the heart
  2. We save on rent

There is a task: from VPS on Linux to launch process on VDS. Wrote a simple server in Flask. Yes, I don’t like python, but what can you do - it’s faster and easier to write this server on it.

It performs 3 functions:

  1. Launching a server with a configuration - choosing a map, the number of players to start the game, and a set of plugins. I won’t write about plugins now - this is a separate story with liters of coffee at night mixed with tears and torn hair.
  2. Stop/restart the server in case of failed connections, which we can only handle manually.

Everything is simple here, code examples are even inappropriate. 100 line script

So, when 10 people got together and accepted the game, the server is launched and everyone is eager to play, a link to join the game comes in private messages.

Writing matchmaking for Dota 2014

By clicking the link, the player connects to the game server, and then that's it. After ~25 minutes, the virtual "room" with the players is cleared.

I apologize in advance for the clumsiness of the article, I haven’t written here for a long time, and there is too much code to highlight important sections. Noodles, in short.

If I see interest in the topic, then there will be a second part - it will contain my torment with plugins for srcds (Source dedicated server), and, probably, a rating system and mini-dotabuff, a site with game statistics.

Few links:

  1. Our website (statistics, leaderboard, small landing page and client download)
  2. Discord server

Source: habr.com

Add a comment