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.
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:
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.
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:
- There is no docker on it, which hit me in the heart
- 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:
- 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.
- 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.
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:
Source: habr.com