Hej alla.
I vÄras snubblade jag över ett projekt dÀr grabbarna hade lÀrt sig att starta en Dota 2-server av version 2014 och dÀrmed spela pÄ den. Jag Àr ett stort fan av detta spel och kunde inte motstÄ den unika möjligheten att Äteruppleva min barndom.
Jag dök djupt ner i det och det visade sig att jag skrev en Discord-bot som hanterar nÀstan all funktionalitet som inte stöds i den gamla versionen av spelet, nÀmligen matchmaking.
Innan alla nyheter med boten skapades lobbyn manuellt. Vi samlade in 10 reaktioner pÄ meddelandet och skapade servern manuellt, eller hostade en lokal lobby.

Min programmerartemperament klarade inte av sÄ mycket manuellt arbete, sÄ jag skissade ihop den enklaste versionen av boten pÄ en natt som automatiskt startade servern nÀr 10 personer hade samlats.
Jag bestÀmde mig för att skriva i Node.js eftersom jag inte riktigt gillar Python, och jag kÀnner mig mer bekvÀm i den miljön.
Detta Àr min första erfarenhet av att skriva en bot för Discord, men det visade sig vara vÀldigt enkelt. Den officiella npm-modulen discord.js erbjuder ett bekvÀmt grÀnssnitt för att arbeta med meddelanden, samla reaktioner osv.
Disklaimer: alla kodexempel Àr 'aktuella', dvs. de har genomgÄtt flera iterationer av omskrivningar pÄ nÀtterna.
Grunden för matchmaking Àr en 'kö' dÀr spelare som vill spela placeras och tas bort nÀr de tappar intresset eller hittar ett spel.
SÄ hÀr ser entiteten 'spelare' ut. FrÄn början var detta bara id:t av anvÀndaren i Discord, men planen Àr att ha en launcher/spel sökt frÄn webbplatsen, men allt i tur och ordning.
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);
}
}
HÀr Àr grÀnssnittet för kön. IstÀllet för 'spelare' anvÀnds en abstraktion i form av en 'grupp'. För en enskild spelare bestÄr gruppen av honom eller henne, och för spelarna i en grupp, förstÄs, av alla spelare i gruppen.
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
}
Jag bestĂ€mde mig för att anvĂ€nda hĂ€ndelser för att dela kontext. Det passade in i fallen â vid hĂ€ndelsen "spel för 10 personer hittat" kan man bĂ„de skicka meddelandet till spelarna i privata meddelanden och genomföra den huvudsakliga affĂ€rslogiken â starta en uppgift för att kontrollera beredskap, förbereda lobbyn för start och sĂ„ vidare.
För IOC anvÀnder jag InversifyJS. Jag har en trevlig erfarenhet av att arbeta med detta bibliotek. Snabbt och enkelt!
Vi har flera köer pĂ„ servern â det har tillkommit lĂ€gen 1x1, vanlig/rankad, samt ett par custom-lĂ€gen. DĂ€rför finns det en singleton RoomService, som ligger mellan anvĂ€ndaren och spellet.
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))) {
// om kön har denna grupp, lÀgg till gruppen igen
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))) {
// om kön har denna grupp, ta bort gruppen
q.remove(event.party)
}
});
}
);
}
(Kodsnutt för att visa hur processerna ser ut ungefÀr)
HÀr initierar jag kön för varje av de implementerade spelsÀtten, och lyssnar ocksÄ pÄ förÀndringar i "grupper" för att justera köerna och undvika vissa konflikter.
SÄ, jag Àr duktig, jag har klistrat in kodsnuttar som inte har nÄgot att göra med Àmnet, och nu gÄr vi vidare till matchmaking.
LÄt oss betrakta fallet:
1) AnvÀndaren vill spela.
2) För att börja sökningen anvÀnder han Gateway=Discord, det vill sÀga han sÀtter en reaktion pÄ meddelandet:

3) Denna gateway gÄr till RoomService och sÀger "AnvÀndaren frÄn Discord vill gÄ med i kön, lÀge: icke-rankad spel".
4) RoomService tar emot gatewayens begÀran och lÀgger anvÀndaren i den rÀtta kön (eller snarare, anvÀndarens grupp).
5) Kön kontrollerar vid varje förĂ€ndring om det finns tillrĂ€ckligt med spelare för att spela. Om det Ă€r möjligt â emitterar vi en hĂ€ndelse:
private onRoomFound(players: Party[]) {
this.emit("room-found", {
players,
});
}
6) RoomService lyssnar uppenbarligen pÄ varje kö med förvÀntan inför detta tillfÀlle. Vi fÄr en lista över spelare, skapar ett virtuellt "rum" av dem, och sjÀlvklart emitterar vi en hÀndelse:
queue.addListener("room-found", (event: RoomFoundEvent) => {
console.log(
`Rum funnet lÀge: [${mode}]. Dags att fÄ gratis rum för dessa killar`
);
const room = this.getFreeRoom(mode);
room.fill(event.players);
this.onRoomFormed(room);
});
7) HĂ€r Ă€r vi vid den "högsta" instansen â klassen Bot. Generellt hanterar den kommunikationen mellan gateways (hur lustigt det ser ut pĂ„ ryska kan jag inte förstĂ„) och affĂ€rslogiken för matchmaking. Botten lyssnar pĂ„ hĂ€ndelsen och beordrar DiscordGateway att skicka en kontroll till alla anvĂ€ndare för att se om de Ă€r redo.

8) Om nÄgon avböjer eller inte accepterar spelet inom 3 minuter, sÄ Äterför vi INTE dem till kön. Alla andra Äterförs till kön, och vi vÀntar tills vi Äterigen har 10 personer. Om alla spelare accepterar spelet börjar den intressanta delen.
Konfiguration av dedikerad server
Vi hostar spel pÄ VDS med Windows Server 2012. Av detta kan vi dra flera slutsatser:
- Det finns ingen docker, vilket slog mig rakt i hjÀrtat.
- Vi sparar pÄ hyran.
Uppgiften Ă€r att köra en process pĂ„ VDS frĂ„n VPS med Linux. Jag skrev en enkel server pĂ„ Flask. Ja, jag gillar inte Python, men vad ska man göra â att skriva denna server var snabbare och enklare pĂ„ det sprĂ„ket.
Den utför 3 funktioner:
- Starta servern med konfigurationen â val av karta, antal spelare för att starta spelet, och uppsĂ€ttning av plugins. Jag kommer inte gĂ„ in pĂ„ plugins just nu â det Ă€r en egen historia med liter av kaffe pĂ„ nĂ€tterna, blandat med tĂ„rar och utdragna hĂ„r.
- Stoppa/omstarta servern vid misslyckade anslutningar, som vi endast kan hantera manuellt.
HÀr Àr allt enkelt, kodexempel Àr knappt relevanta. Skriptet Àr pÄ 100 rader.
SÄ snart 10 personer har samlats och accepterat spelet, Àr servern igÄng och alla Àr sugna pÄ att spela, fÄr de en lÀnk till att ansluta till spelet i sina privata meddelanden.

NÀr spelaren klickar pÄ lÀnken kopplas de till spelservern, och sedan Àr allting klart. Efter ungefÀr 25 minuter rensas det virtuella "rummet" med spelarna.
Jag ber om ursÀkt pÄ förhand för artikels oordning, jag har inte skrivit hÀr pÄ lÀnge, och det finns för mycket kod för att lyfta fram viktiga delar. Koka ner det till nudlar.
Om jag ser intresse för Ă€mnet kommer jag att göra en andra del â dĂ€r kommer mina plĂ„gor med plugins för srcds (Source Dedicated Server) att finnas, och kanske Ă€ven ett ranking-system och en mini-dotabuff, en webbplats för spelstatistik.
NÄgra lÀnkar:
KĂ€lla: habr.com
