Tere kõigile.
Sel kevadel sattusin projekti, kus noored õpivad käivitama Dota 2 serverit 2014. aasta versioonis ja seega ka mängima selle peal. Olen selle mängu suur fänn ja ei saanud mööda minna ainulaadsest võimalusest sukelduda oma lapsepõlve.
Sukeldusin väga sügavale ja nii juhtus, et kirjutasin Discordi boti, mis vastutab praktiliselt kogu funktsionaalsuse eest, mida vana mängu versioon ei toeta, nimelt matchmaking.
Kõikide botiga seotud uuenduste eel koondati lobby käsitsi. Kogusime 10 reaktsiooni sõnumile ja kogusime serveri käsitsi või korraldasime kohalikku lobby.

Minu programmeerija loomus ei pidanud vastu nii suurtele käsitööde hulkadele ja öö jooksul viskasin kokku lihtsama botiversiooni, mis automaatselt tõstis serveri üles, kui 10 inimest oli koos.
Otsustasin kirjutada nodejs'i, sest ma ei armasta pythonit väga ning tunnen end selles keskkonnas mugavamalt.
See on minu esimene kogemus Discordi boti kirjutamisel, kuid osutus, et kõik on väga lihtne. Ametlik npm moodul discord.js pakub mugavat liidest sõnumitega töötamiseks, reaktsioonide kogumiseks jne.
Märkus: kõik koodi näited on "aktuaalsed", mis tähendab, et need on läbinud mitu öist ümberkirjutamise ringi.
Matchmakingu aluseks on "järjekord", kuhu paigutatakse mängijad, kes soovivad mängida, ja eemaldatakse, kui nad enam ei soovi või on mängu leidnud.
Nii näeb välja "mängija" entiteet. Alguses oli see lihtsalt Discordi kasutaja ID, kuid plaanis on käivitaja/mängu otsimine saidilt, aga sellest kõigest hiljem.
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);
}
}
Ja siin on järjekorra liides. Siin kasutatakse "mängijate" asemel abstraktsiooni nimega "grupp". Üksikmängija jaoks koosneb grupp temast endast, ning grupi mängijate jaoks vastavalt kõigist grupi mängijatest.
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
}
Otsustasin kasutada sündmusi konteksti vahetamiseks. See sobib juhtumitega - sündmuse "mäng leitud 10 inimesele" korral saab saata isiklikele sõnumitele mängijatele vajaliku sõnumi ning teostada peamist äriloogikat - käivitada ülesanne valmisoleku kontrollimiseks, valmistada lobby avamiseks ja nii edasi.
IOC jaoks kasutan InversifyJS-i. Mul on selle teegiga meeldiv kogemus. Kiire ja lihtne!
Meie serveris on mitu järjekorda - lisandusid režiimid 1x1, tavaline/reitinguline ja paar kohandatud. Seetõttu on olemas singleton RoomService, mis asub kasutaja ja mängu otsingu vahel.
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))) {
// kui järjekorras on see partei, siis me lisame partei uuesti
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))) {
// kui järjekorras on see partei, siis me eemaldame partei
q.remove(event.party)
}
});
}
);
}
(Koodinäide protsesside visualiseerimiseks)
Siin initsialiseerin järjekorra iga realiseeritud mängurežiimi jaoks ja kuulan "gruppide" muudatusi, et kohandada järjekordi ja vältida mõningaid konflikte.
Nii, rõhutan, ma lisasin kooditükke, mis ei seotud teemaga, nüüd aga liigume muthkemiseks.
Vaadakem juhtumit:
1) Kasutaja soovib mängida.
2) Otsingu alustamiseks kasutab ta Gateway=Discord, st reageerib sõnumile:

3) See värav läheb RoomService'i ja ütleb: "Discordi kasutaja soovib järjekorda astuda, režiim: mitte-reitinguline mäng."
4) RoomService võtab värava taotluse vastu ja suunab kasutaja (täpsemalt, kasutaja grupi) vajalikku järjekorda.
5) Iga järjekorra muudatuse korral kontrollitakse, kas mängimiseks on piisavalt mängijaid. Kui on võimalik - väljastame sündmuse:
private onRoomFound(players: Party[]) {
this.emit("room-found", {
players,
});
}
6) RoomService kuulab ilmselt igat järjekorda põnevil ootusärevuses. Saame sisendiks mängijate nimekirja, loome neist virtuaalse "toa" ja muidugi saadame sündmuse:
queue.addListener("room-found", (event: RoomFoundEvent) => {
console.log(
`Toa leidmise režiim: [${mode}]. Aeg saada tasuta tuba nende jaoks`
);
const room = this.getFreeRoom(mode);
room.fill(event.players);
this.onRoomFormed(room);
});
7) Nüüd oleme jõudnud "kõrgema" instantsi – klassini Bot. Üldiselt tegeleb ta väravate (kui naljakalt see vene keeles kõlab) ja mängude sobitamise äriloogika vahelise ühenduse loomisega. Bot kuulab sündmust ja käsib DiscordGateway'l saata kõigile kasutajatele valmisoleku kontrolli.

8) Kui keegi lükkab mängu tagasi või ei nõustu sellega kolme minuti jooksul, siis me EI tagasta neid järjekorda. Kõik teised tagastatakse järjekorda ja ootame, kuni meid on taas 10 inimest. Kui kõik mängijad on mänguga nõustunud, algab huvitav osa.
Pühendatud serveri konfiguratsioon
Meie mängud on hostitud VDS-iga, millel on Windows server 2012. Sellest võib teha mitu järeldust:
- Seal ei ole dockerit, mis lõi mu südamesse tõsise kahjustuse
- Me säästame rendilt
Eesmärk on: käivitada protsess VPS-l Linuxis VDS-is. Kirjutasin lihtsa serveri Flask'is. Jah, ma ei armasta Pythonit, aga mis sa teed – selle serveri kirjutamine on kiire ja lihtne.
See täidab 3 funktsiooni:
- Serveri käivitamine konfiguratsiooniga – kaardi valik, mängijate arvu määramine mängu alustamiseks ja pluginamoodulite valimine. Pluginatest ei räägi ma hetkel – see on eraldi lugu, kus on litreid kohvi öösel, segatuna pisarate ja väljatõmmatud juustega.
- Serveri peatamine/taaskäivitamine juhul, kui me saame ühenduse ebaõnnestumise käsitleda ainult käsitsi.
Siin on kõik lihtne, koodinäidised ei ole asjakohased. Skript 100 rida
Nii, kui 10 inimest on koos ja on nõustunud mänguga, on server käivitatud ja kõik tahtsid mängida, saadetakse neile isiklikesse sõnumitesse link mängusse ühendamiseks.

Linki vajutamisel ühendab mängija mänguserveriga ja edasi läheb kõik iseenesest. Umbes 25 minuti pärast tühjendatakse virtuaalne "tuba" mängijatega.
Vabandan ette, et artikkel on kohmakas, pole ammu siia kirjutanud ja koodi on liiga palju, et eristada olulisi osi. Lihtsalt nuud.
Kui näen teemas huvi, siis tuleb teine osa — seal on mu piinad srcds (Source dedicated server) pluginatega, ja tõenäoliselt ka reitingusüsteem ja mini-dotabuff, mängude statistika leht.
Mõned lingid:
Allikas: habr.com
