Ciao a tutti.
Questa primavera ho scoperto un progetto in cui i ragazzi hanno imparato a far partire un server di Dota 2 della versione 2014 e, di conseguenza, a giocarci. Sono un grande fan di questo gioco e non potevo perdere l'opportunità unica di tornare alla mia infanzia.
Mi sono immerso profondamente e così ho scritto un bot per Discord che gestisce praticamente tutte le funzioni non supportate nella vecchia versione del gioco, in particolare il matchmaking.
Prima di tutte le novità, il bot creava le lobby manualmente. Raccoglievamo 10 reazioni a un messaggio e assemblevamo manualmente il server, oppure ospitavamo una lobby locale.

La mia natura di programmatore non ha retto a un tale numero di lavori manuali e in una notte ho abbozzato la versione più semplice del bot, che attivava automaticamente il server quando si raggiungevano 10 persone.
Ho deciso di scrivere subito in nodejs, perché non mi piace molto Python e mi sento più a mio agio in questo ambiente.
È la mia prima esperienza nella scrittura di un bot per Discord, ma si è rivelato tutto molto semplice. Il modulo npm ufficiale discord.js fornisce un'interfaccia comoda per lavorare con i messaggi, la raccolta delle reazioni, ecc.
Dichiarazione: tutti i codici esempi sono "attuali", ovvero sono stati riscritti dopo diverse iterazioni notturne.
La base del matchmaking è la "coda", in cui vengono inseriti i giocatori che vogliono giocare e vengono rimossi quando non vogliono più giocare o hanno trovato una partita.
Ecco come appare l'entità "giocatore". Inizialmente era semplicemente l'ID dell'utente su Discord, ma ci sono piani per un launcher/reperimento di gioco dal sito, ma andiamo con ordine.
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);
}
}
Ecco l'interfaccia della coda. Qui invece dei "giocatori" si utilizza un'astrazione sotto forma di "gruppo". Per un giocatore singolo, il gruppo è composto da se stesso, mentre per i giocatori in gruppo, ovviamente, da tutti i membri del gruppo.
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
}
Ho deciso di utilizzare eventi per lo scambio di contesto. Si adattava ai casi: con l'evento "trovata partita per 10 persone" posso sia inviare un messaggio personale ai giocatori sia eseguire la logica di base del business: avviare un task per controllare la prontezza, preparare il lobby per l'avvio e così via.
Per IOC utilizzo InversifyJS. Ho una piacevole esperienza di lavoro con questa libreria. Veloce e semplice!
Abbiamo diverse code sul server: si sono aggiunti i modelli 1vs1, normale/competitivo, e un paio di personalizzate. Quindi c'è il singleton RoomService, che si interpone tra l'utente e la ricerca del gioco.
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))) {
// se la coda ha questo gruppo, riaggiungiamo il gruppo
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))) {
// se la coda ha questo gruppo, rimuoviamo il gruppo
q.remove(event.party)
}
});
}
);
}
(Codice dimostrativo per illustrare come appaiono i processi)
Qui inizio l'inizializzazione della coda per ciascuno dei modelli di gioco implementati, oltre a monitorare le modifiche ai "gruppi" per correggere le code e evitare alcuni conflitti.
Quindi, sono bravo, ho inserito pezzi di codice che non sono rilevanti per l'argomento, e ora passiamo direttamente al matchmaking.
Consideriamo il caso:
1) L'utente vuole giocare.
2) Per avviare la ricerca, utilizza Gateway=Discord, cioè reagisce a un messaggio:

3) Questo gateway va a RoomService e dice "L'utente da Discord vuole entrare in coda, modalità: partita non classificata".
4) RoomService accetta la richiesta del gateway e inserisce l'utente (più precisamente, il gruppo dell'utente) nella coda appropriata.
5) La coda controlla ad ogni cambiamento se ci sono abbastanza giocatori per il gioco. Se sì, emettiamo un evento:
private onRoomFound(players: Party[]) {
this.emit("room-found", {
players,
});
}
6) RoomService, ovviamente, ascolta con gioia ogni coda in trepidante attesa di questo evento. All'ingresso riceviamo un elenco di giocatori, formiamo da essi una "stanza" virtuale e, naturalmente, emettiamo l'evento:
queue.addListener("room-found", (event: RoomFoundEvent) => {
console.log(
`Modalità stanza trovata: [${mode}]. È il momento di trovare una stanza libera per questi ragazzi`
);
const room = this.getFreeRoom(mode);
room.fill(event.players);
this.onRoomFormed(room);
});
7) Ecco che siamo arrivati all'"istanza suprema" — classe Bot. Fondamentalmente si occupa della comunicazione tra i gateway (quanto è divertente che in russo si presenti così) e la logica di business del matchmaking. Il bot ascolta l'evento e ordina a DiscordGateway di inviare a tutti gli utenti un controllo di disponibilità.

8) Se qualcuno rifiuta o non accetta il gioco entro 3 minuti, NON li rimettiamo in coda. Gli altri vengono rimessi in coda e aspettiamo che si raggiunga di nuovo un gruppo di 10 persone. Se tutti i giocatori accettano il gioco, inizia la parte interessante.
Configurazione del server dedicato
I nostri giochi sono ospitati su VDS con Windows Server 2012. Da ciò possiamo trarre alcune conclusioni:
- Non c'è Docker, il che mi ha colpito dritto al cuore.
- Stiamo risparmiando sull'affitto.
C'è un compito: lanciare un processo su VDS da VPS su Linux. Ho scritto un semplice server in Flask. Sì, non amo Python, ma che ci posso fare — scrivere questo server in lui è più veloce e semplice.
Esegue 3 funzioni:
- L'avvio del server con configurazione — scelta della mappa, numero di giocatori per iniziare il gioco e set di plugin. Non parlerò ora dei plugin — è tutta un'altra storia con litri di caffè e notti miste a lacrime e capelli strappati.
- Arresto/riavvio del server in caso di connessioni non riuscite, che possiamo gestire solo manualmente.
Qui tutto è semplice, esempi di codice sarebbero superflui. Uno script di 100 righe.
Quindi, quando 10 persone si riuniscono e accettano il gioco, il server è avviato e tutti desiderano giocare, arriva un link diretto nei messaggi privati per connettersi al gioco.

Cliccando sul link, il giocatore si collega al server di gioco, e poi il tutto prosegue automaticamente. Dopo circa 25 minuti la "stanza" virtuale con i giocatori si svuota.
Mi scuso in anticipo per la disorganicità dell'articolo, non scrivo qui da tempo e ci sono troppi codici per evidenziare le parti importanti. In sintesi, pasta.
Se vedo interesse per l'argomento, ci sarà una seconda parte: in essa parlerò delle mie sofferenze con i plugin per srcds (Source dedicated server) e, probabilmente, del sistema di ranking e mini-dotabuff, un sito con le statistiche delle partite.
Un po' di link:
Fonte: habr.com
