Усім прывітанне.
Гэтай вясной я натыкнуўся на праект, у якім хлопцы навучыліся запускаць Dota 2 сервер версіі 2014 года і, адпаведна, гуляць на ім. Я вялікі фанат гэтай гульні, і не змог прайсці міма ўнікальнай магчымасці акунуцца ў сваё дзяцінства.
Акунуўся я вельмі глыбока, і так выйшла што я напісаў Discord робата, які адказвае практычна за ўвесь функцыянал, які не падтрымліваецца ў старой версіі гульні, а менавіта матчмэйкінг.
Да ўсіх новаўвядзенняў з ботам лобі стваралася ўручную. Збіралі 10 рэакцый на паведамленне і ўручную збіралі сервер, альбо хосцілі лакальнае лобі.
Мая натура праграміста не вытрымала такую колькасць ручной працы, і за ноч я накідаў самую простую версію бота, якая аўтаматычна паднімала сервер, калі набіралася 10 чалавек.
Пісаць адразу вырашыў на nodejs, так як не вельмі люблю пітон, ды і зручней сябе адчуваю ў гэтым асяроддзі.
Гэта мой першы вопыт напісання бота для Discord, але аказалася ўсё вельмі нават проста. Афіцыйны npm модуль discord.js падае зручны інтэрфейс для працы з паведамленнямі, зборам рэакцый і т.д.
Дысклеймер: усе прыклады кода з'яўляюцца "актуальнымі", гэта значыць прайшлі некалькі ітэрацый перапісвання па начах.
Аснова матчмэйкінгу - гэта «чарга», у якую змяшчаюцца гульцы, якія хочуць гуляць, і прыбіраюцца, калі расхацелі або знайшлі гульню.
Так выглядае сутнасць "гульца". Першапачаткова гэта быў проста id карыстальніка ў Discord, але ў планах лаўнчар/пошук гульні з сайта, але пра ўсё па парадку.
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);
}
}
А вось інтэрфейс чаргі. Тут замест "гульцоў" выкарыстоўваецца абстракцыя ў выглядзе "групы". Для адзіночнага гульца група складаецца з яго самога, а для гульцоў у групе адпаведна з усіх гульцоў групы.
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
}
Вырашыў выкарыстоўваць падзеі для абмену кантэкстам. Падыходзіла пад кейсы - па падзеі "знойдзена гульня для 10 чалавек" можна і адправіць у асабістыя паведамленні гульцам патрэбнае паведамленне, і выканаць асноўную бізнес логіку - запусціць цягак для праверкі гатоўнасці, падрыхтаваць лобі да запуску і гэтак далей.
Для IOC я выкарыстоўваю InversifyJS. Маю прыемны досвед працы з гэтай бібліятэкай. Хутка і проста!
Чаргаў у нас на серверы некалькі - дадаліся рэжымы 1х1, звычайны/рэйтынгавы, і пару кастомок. Таму ёсць singleton RoomService, які ляжыць паміж карыстачом і пошукам гульні.
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)
}
});
}
);
}
(Локшына кода для прадстаўлення, як прыкладна выглядаюць працэсы)
Тут я ініцыялізую чаргу пад кожны з рэалізаваных рэжымаў гульні, а таксама слухаю змены «груп», каб падкарэктаваць чэргі і пазбегнуць некаторых канфліктаў.
Так, я малайчына, я ўставіў кавалкі кода, якія ніяк не ставяцца да топіка, а зараз пяройдзем ужо непасрэдна да мачтмэйкінгу.
Разгледзім кейс:
1) Карыстальнік хоча пагуляць.
2) Для таго, каб пачаць пошук, ён выкарыстоўвае Gateway=Discord, гэта значыць ставіць рэакцыю на паведамленне:
3) Гэты гейтвей ідзе ў RoomService, і кажа "Карыстальнік з дыскорду хоча ўвайсці ў чаргу, рэжым: нерэйтынгавая гульня".
4) RoomService прымае просьбу гейтвея, і піхае ў патрэбную чаргу карыстальніка (дакладней, групу карыстальніка).
5) Чарга пры кожнай змене правярае, ці хапае гульцоў для гульні. Калі можна – эмітым падзея:
private onRoomFound(players: Party[]) {
this.emit("room-found", {
players,
});
}
6) RoomService, відавочна, з радасцю слухае кожную чаргу ў трапяткім чаканні гэтай падзеі. На ўваход мы атрымліваем спіс гульцоў, фармуем з іх віртуальную «пакой», і, вядома ж, эмітым падзея:
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) Вось мы і дабраліся да «вышэйшай» інстанцыі - класа Bot. У цэлым ён займаецца сувяззю паміж гейтвеямі (як гэта смешна на рускай выглядае я не магу) і бізнес логікай матчмейкінга. Бот падслухоўвае падзею, і загадвае DiscordGateway адправіць усім карыстальнікам праверку на гатоўнасць.
8) Калі нехта адхіліў ці не прыняў гульню за 3 хвіліны, то мы НЕ вяртаем іх у чаргу. Усіх астатніх вяртаем у чаргу і чакаем, калі зноў набярэцца 10 чалавек. Калі ўсе гульцы прынялі гульню, то пачынаецца цікавая частка.
Канфігурацыя выдзеленага сервера
У нас гульні хосцяцца на VDS c Windows server 2012. З гэтага можна зрабіць некалькі высноў:
- На яго няма докера, што стукнула мяне ў самае сэрца
- Мы эканомім на арэндзе
Стаіць задача: з VPS на лінуксе запускаць працэс на VDS. Напісаў просты сервер на Flask. Так, не кахаю пітон, але што парабіць - на ім напісаць гэты сервер хутчэй і прасцей.
Ён выконвае 3 функцыі:
- Запуск сервера з канфігурацыяй - выбар карты, колькасці гульцоў для старту гульні, і набор убудоў. Пра плагіны зараз не буду пісаць — гэта асобная гісторыя з літрамі кавы па начах уперамешку са слязамі і вырванымі валасамі.
- Прыпынак/перазапуск сервера ў выпадку няўдалых падлучэнняў, якія мы можам апрацаваць толькі ўручную.
Тут усё проста, прыклады кода нават недарэчныя. Скрыпт на 100 радкоў
Такім чынам, калі 10 чалавек сабраліся разам і прынялі гульню, запушчаны сервер і ўсё прагнуць гуляць, у асабістыя паведамленні прыходзіць спасылка на падлучэнне да гульні.
Па націску спасылкі гульца канэктыт да гульнявога сервера, і далей ужо само ўсё. Праз ~25 хвілін віртуальная «пакой» з гульцамі чысціцца.
Загадзя прашу прабачэння за няскладнасць артыкула, даўно не пісаў сюды, ды і кода занадта шмат, каб вылучыць важныя ўчасткі. Локшына, карацей.
Калі ўбачу цікавасць да тэмы, то будзе другая частка - у ёй будуць мае пакуты з убудовамі для srcds (Source dedicated server), і, мусіць, сістэма рэйтынгу і міні-dotabuff, сайт са статыстыкай гульняў.
Трохі спасылак:
Крыніца: habr.com