Всем привет.
Этой весной я наткнулся на проект, в котором ребята научились запускать Dota 2 сервер версии 2014 года и, соответственно, играть на нем. Я большой фанат этой игры, и не смог пройти мимо уникальной возможности окунуться в свое детство.
Окунулся я очень глубоко, и так вышло что я написал Discord бота, который отвечает практически за весь функционал, который не поддерживается в старой версии игры, а именно матчмейкинг.
До всех нововведений с ботом лобби создавалось вручную. Собирали 10 реакций на сообщение и вручную собирали сервер, либо хостили локальное лобби.
Моя натура программиста не выдержала такое количество ручной работы, и за ночь я набросал самую простую версию бота, которая автоматически поднимала сервер, когда набиралось 10 человек.
Писать сходу решил на nodejs, потому что не очень люблю питон, ну и комфортнее себя чувствую в этой среде.
Это мой первый опыт написания бота для Discord, но оказалось все очень даже просто. Официальный npm модуль discord.js предоставляет удобный интерфейс для работы с сообщениями, сбором реакций и т.д.
Дисклеймер: все примеры кода являются «актуальными», то есть прошли несколько итераций переписывания по ночам.
Основа матчмейкинга — это «очередь», в которую помещаются игроки, которые хотят играть, и убираются, когда расхотели или нашли игру.
Так выглядит сущность «игрока». Изначально это был просто id пользователя в Discord, но в планах лаунчер/поиск игры с сайта, но обо всем по порядку.
А вот интерфейс очереди. Тут вместо «игроков» используется абстракция в виде «группы». Для одиночного игрока группа состоит из него самого, а для игроков в группе, соответственно, из всех игроков группы.
Решил использовать события для обмена контекстом. Подходило под кейсы — по событию «найдена игра для 10 человек» можно и отправить в личные сообщения игрокам нужное сообщение, и выполнить основную бизнес логику — запустить таск для проверки готовности, подготовить лобби к запуску и так далее.
Для IOC я использую InversifyJS. Имею приятный опыт работы с этой библиотекой. Быстро и просто!
Очередей у нас на сервере несколько — добавились режими 1х1, обычный/рейтинговый, и пара кастомок. Поэтому есть singleton RoomService, который лежит между пользователем и поиском игры.
(Лапша кода для представления, как примерно выглядят процессы)
Здесь я инициализирую очередь под каждый из реализованных режимов игры, а так же слушаю изменения «групп», чтобы подкорректировать очереди и избежать некоторых конфликтов.
Так, я молодец, я вставил куски кода, которые никак не относятся к топику, а теперь перейдем уже непосредственно к мачтмейкингу.
Рассмотрим кейс:
1) Пользователь хочет поиграть.
2) Для того, чтобы начать поиск, он использует Gateway=Discord, то есть ставит реакцию на сообщение:
3) Этот гейтвей идет в RoomService, и говорит «Пользователь из дискорда хочет войти в очередь, режим: нерейтинговая игра».
4) RoomService принимает просьбу гейтвея, и пихает в нужную очередь пользователя(точнее, группу пользователя).
5) Очередь при каждом изменении проверяет, хватает ли игроков для игры. Если можно — эмиттим событие:
6) RoomService, очевидно, с радостью слушает каждую очередь в трепетном ожидании этого события. На вход мы получаем список игроков, формируем из них виртуальную «комнату», и, конечно же, эмиттим событие:
7) Вот мы и добрались до «высшей» инстанции — класса Bot. В целом он занимается связью между гейтвеями(как это смешно на русском выглядит я не могу) и бизнес логикой матчмейкинга. Бот подслушивает событие, и приказывает DiscordGateway отослать всем пользователям проверку на готовность.
8) Если кто-то отклонил или не принял игру за 3 минуты, то мы НЕ возвращаем их в очередь. Всех остальных возвращаем в очередь и ждем, когда снова наберется 10 человек. Если все игроки приняли игру, то начинается интересная часть.
У нас игры хостятся на VDS c Windows server 2012. Из этого можно сделать несколько выводов:
Стоит задача: с VPS на линуксе запускать процесс на VDS. Написал простой сервер на Flask. Да, не люблю питон, но что поделать — на нем написать этот сервер быстрее и проще.
Он выполняет 3 функции:
Тут все просто, примеры кода даже неуместны. Скрипт на 100 строчек
Итак, когда 10 человек собрались вместе и приняли игру, запущен сервер и все жаждут играть, в личные сообщения приходит ссылка на подключение к игре.
По нажатию ссылки игрока коннектит к игровому серверу, и дальше уже само все. Через ~25 минут виртуальная «комната» с игроками очищается.
Заранее извиняюсь за нескладность статьи, давно не писал сюда, да и кода слишком много, чтобы выделить важные участки. Лапша, короче.
Если увижу интерес к теме, то будет вторая часть — в ней будут мои мучения с плагинами для srcds(Source dedicated server), и, наверное, система рейтинга и мини-dotabuff, сайт со статистикой игр.
Немного ссылок:
Этой весной я наткнулся на проект, в котором ребята научились запускать 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, сайт со статистикой игр.
Немного ссылок:
Voiddancer
А зачем это, если дота бесплатна? Мне правда интересно, в чем смысл?
enchantinggg Автор
Что именно? Мы просто предоставляем возможность поиграть в старую доту с людьми. Она иначе не работает, игры ищутся с "актуальной версией клиента", поэтому либо сам находишь 10 человек поиграть, либо с ботами. Проект некоммерческий абсолютно, живет только за донаты на аренду серверов и рекламу.
Voiddancer
Зачем нужна старая дота?
enchantinggg Автор
Видимо для тех, кому не нравится новая.
inkvizitor68sl
Потому что в новой баланс крутят туда-сюда, подкручивая героев, на которые скоро выйдет дорогая шмотка (ну или просто платная, но первая на него), а потом закапывая их обратно.
В итоге баланс сейчас мягко говоря отсутствует, а единственный интересный режим игры — турбо, потому что там можно проиграть пик, но потом выиграть игру.
drdead
Ну, конечно же, вы можете подтвердить это, скажем, выборкой по героям, у которых сейчас вышли недавно Arcana или Persona и винрейтом, на https://www.dotabuff.com/heroes/meta, сортированном по divine+ рангу?
inkvizitor68sl
При том что сами divine+ говорят, что эта статистика — фигня? =) Нет, не могу.
Зато могу сказать, что квопа — перебафана и последний год получала только улучшения, а меньше чем через месяц после выхода арканы получила долгожданный фикс кинжала (ну какой-никакой).
Что WR бафается последний год и превратилась в гниль, с которой невозможно стоять на линии — берется на любую роль, контрить её теперь нечем (раньше хотя бы обратка была, не все игроки догадывались нажать hold), имеет бесконечный стан.
Что WK последний год бесконечно апали и сейчас он стал той ещё занозой в заднице — в последнем патче ему вернули старую ауру, например, не убрав все баффы, которые он получил, когда её забирали.
Что AMу убили все контрпики (сикера, купол войду), поменяли manaburn на проценты.
И что через 3-6 месяцев со следующим глобальным патчем эти герои снова залезут туда, откуда вылезли и все про них забудут.
Так же, как это было с огром, шейкером, и джаггером. Огр и шейкер были сильнейшими саппортами на момент выхода своих аркан, сейчас их в играх не встретишь. Огр ещё бывает, но страшным уже не смотрится. Шейкера вообще нет. Джагера c 2017 года люди вообще за героя не считали — только этим летом он как-то всплыл.
Ага, а виспу за 3 дня до релиза арканы добавили талант на стан с тизера вместо замедления.
И это я говорю только про арканы. На траксу вышел ultra rare лук — на тот момент она была default-carry, фармила на уровне алхимика, на ней играли вообще все — при этом продолжалось это около полугода. А через 2-3 недели с первым же патчем её закапывают обратно, убирая ваншот с древних крипов. Потом она с каким-то патчем месяц заиграла новыми красками, но и это быстро пофиксили.
ЦК и войда начали баффать после голосования за collectors cache ну и тыды.
Собственно, всё это — даже не секрет. И вольво не сильно скрывает, хотя официально это звучит как «мы режем слишком популярных геров и усиливаем непопулярных» (ага, земелю особенно).
inkvizitor68sl
Насчет статистики.
Broodmother — 59%. Кому это что даёт?
Lycan — 54, веник 55.
Да пойди выиграй ещё на них.
Я убедился в том, что дотабафф в абсолютных значениях очень сильно привирает ещё с тех пор, когда у найкса было 45%, а у спектры — 55. Только найкс при этом был достаточно сильным героем — пусть и с известными контрпиками, а спектра — героем, на котором невозможно выиграть. Ну хотя бы потому, что когда ты пикал спектру — вся команда переставала играть, потому что «автолуз же, зачем стараться» (но герой действительно был неиграбельным совершенно).
Вот относительные значения там смотреть хорошо. Хотя и доверять тоже не всегда стоит — у реворкнутых героев процент побед часто падает, хотя они и становятся намного сильнее. Или наоборот у значительно ослабленных героев сильно вырастает винрейт просто из-за стереотипов о старом (точно помню такое с вайпером, которого считали магом и собирали в мага, а у него на 7м уровне +192 урона на тычку — можно было в миде отстоять с CS 100/80 без проблем, собраться в phys dps и за 18 минут выиграть). Вот вайпера переделали в настоящего мага, у него стало 55% вместо 45% (а потом ещё сильнее порезали), но герой стал при этом чуть ли не в 2 раза слабее на глаз — стало 80 урона вместо 250+ на линии. В итоге на старом я выигрывал 90+%, а на новом еле-еле 50 (при том что весь аккаунт на тот момент игрался в 56%).
Sequoza
Хаха. Почитайте статью habr.com/en/post/488116.
TLDR идеальный баланс только в камень-ножницы-бумага, и то можно на реакции поймать.
Игры, в которых всё зависит от человека, относительно непопулярны и скучны.
inkvizitor68sl
Проблема в том, что valve создаёт искусственный дисбаланс, делая это крайне топорно и не аккуратно.
На практике это означает как раз то, что люди по полгода долбят одного героя (и двух запасных) — и это единственный способ повышать рейтинг. Потому что в каждый момент времени есть примерно 20 играбельных героев, 10 (теперь 12) из которых окажутся в бане.
И как только ты отходишь от подобной стратегии — ты перестаешь выигрывать, даже в 50% случаев, которые тебе система дарит в общем-то бесплатно с точки зрения «стараний».
При этом если ты долбишь «слишком сильно» — то против тебя попадаются всё более сильные игроки (твинки в основном) и система пытается привести тебя к тем самым 50% побед — что как бы тоже удовольствия не добавляет. Ну если уж человек играет сильно для своих цифр — пустите его вверх и всё.
Мы как-то додолбились (не в рейтинге, просто игр 30 выиграли на 3-4 поражения) связкой бара-найкс-сир и со средним рейтингом 2500 стали против титанов с 8000+ играть (см. terrorblade и shadow fiend, но там, конечно, не 2 игры).
inkvizitor68sl
> 20 играбельных героев
20 героев поделенных на 4 роли, если что, а не 20 на каждую роль.
Sequoza
Понимаю ваше разочарование. Я только хотел сказать, что добиться «баланса» очень сложно, если вообще возможно. Да и сомневаюсь, что у разработчиков работа с ним — главная задача.
inkvizitor68sl
Никто не ждет баланса от игры с подобным масштабом игровой механики =)
Речь именно об искуственном адском дисбалансе, который давно уже сделал из этой игры не игру, а какую-то корейскую гриндилку — многократно повторяешь одни и те же действия, иначе успеха не добьёшься.
Просто играть — тоже в общем-то не весело. Как только ты выиграешь больше одной игры подряд — получаешь пессимизацию в ММ и играешь с афк-ребятами и прочей нечистью (даже имея высший рейтинг поведения). И если в году где-то 2017 тебе просто давали слабых игроков и тебе могли сказать «сам не доигрываешь», то теперь система ММ даже не скрывает своего лица — 3 победы, человек идет в мид убиваться до начала игры, ещё 3 — тебе от бота прилетает «против вас играет читер», ещё 3 — против тебя люди с [anylvl.com booster sorry] в нике, играющие 20-0.
Досемерочная версия в этом плане была хотя бы честной — в ММ решали только цифры рейтинга, а персонажи балансировались «как получится», без всяких искусственных ротаций — поэтому после патча оставалось 30 старых героев, 20 новых добавлялось, а 20 шли на свалку истории — но в итоге в среднем всегда оставалось какое-то значимое количество. Сейчас на каждом «цифренном» патче — список изменений чуть ли не на каждого героя.
В этот раз уже не стали ничего придумывать и десяток героев (помимо остальных изменений) тупо откатили на состояние двух лет назад — всех этих героев объединяло то, что игроки реально уже забыли, как герои выглядят.
siziyman
Прекрасный тред, ваше нежелание мириться с объективной реальностью очень повеселило, спасибо.
inkvizitor68sl
Хм?
Что именно вы этим пытались сказать?
Sequoza
Меня лишь смутило высказывание.
Насчёт вашего негодования. Чем вам бустеры не угодили? Как по мне, так против них интереснее играть, т.к. заставляет вас думать, а не повторять одни и те же скрипты, тоже самое касается людей с ммром over 9000. Одно но, в доту, по крайней мере тогда, нужно было играть фул стаком, без исключений.
inkvizitor68sl
Сами по себе — ничем, они есть в каждой онлайн-игре по факту.
Меня расстраивает, что появляться они начинают ровно тогда, когда победишь 5 игр подряд. При этом они попадаются тебе потом ровно 5 игр.
При том что проблема с бустерами по факту давно решена (они получают +300 очков за игру и играют в 10 раз меньше игр) и вероятность встретить их в игре на самом деле близка к нулю, пока ММ тебя не балансит.
alhimik45
Это как так? Система оценивает их скилл относительно соперника?
inkvizitor68sl
Вроде того — если показатели на герое сильно выше средних на этом герое на этом рейтинге несколько игр подряд — дают больше очков за победу. И в обратную сторону тоже работает.
В итоге бустеры играют +300/-30, люди потом покупают аккаунты и играют +30/-300
ЗЫ — забавно, что бустеры на мипо почти перестали играть, потому что для мипо эта система не работала, т.к. средние показатели мипо на всех рейтингах как раз близки к средним показателям бустеров =)
(ну и в точных цифрах я могу ошибаться, у меня больше +60 не было, у команды было максимум +100).
IntActment
Поэтому лично я забил на обычную доту и ушел в режим Абилити Драфт: хоть балансом там и не пахнет, а пик вообще решает всё — режим интересен тем, что больше упор идет на комбинирование скиллов с разных персонажей и полученный эффект синергии, которого нельзя добиться в нормальной игре (из-за существования скиллов, которые работают только в пользу их владельца, то есть их эффект не может быть «одолжен» союзнику). Тут весь тимплей больше сконцентрирован на этапе пика (не допустить взятия важных скиллов командой противника в том числе) и помощи в фарме «везунчику» из своей (кому на этапе пика достались мощные комбинации). Жалко лишь то, что как Вальве, так и большинство игроков не считают достойным внимания любой режим, где нет рейтинга. А ведь из него можно много «фишек» почерпнуть, которые можно применять в «настоящей» доте…
inkvizitor68sl
Ну в турбо тоже баланса нет и в этом вся прелесть =)
Хотя ММ и расстраивает, строгие 50% в соло, но даже проигрывать там — весело.
При этом игроков в турбо всё больше и больше (игры ищутся по 2 секунды в прайм-тайм), а вольво продолжает увеличивать среднее время матча в all pick (+5 минут за год).