- Лучшие функции, необходимые для потоковой передачи. Попробуйте сами.
- Но подождите, это еще не все.
- Наложение и команда победы и проигрыша
- Значок MMR и отслеживание
- Обнаружение смурфиков
- Дотабоду есть что сказать
- Игровые медали
- Фиксированные цены, без платы за управление.
- Почему имя Дотабод?
- Как мне объяснить жене, какие деньги я снимаю с нашего общего банковского счета за использование Dotabod?
- Как мне начать?
- Will my viewers like this?
- Why is Dotabod based?
- Is there any age limit to using Dotabod?
- How does it work?
- Isn’t this against TOS?
- Will this make me a better streamer?
- С чего всё начиналось
- Данные, которые не дают преимущества
- UI, Оповещения, Electron
- Обогащаем данные
- Узнаём предпочтения игрока
- Задача
- Турнир The International
- Уязвимости бота
- Инфраструктура
- Пять на пять
Лучшие функции, необходимые для потоковой передачи. Попробуйте сами.
Dotabod был создан для таких стримеров, как вы, которые играют по своим правилам и не позволят ничему помешать своей мечте. Если другие потоковые инструменты боятся его создавать, то у Dotabod он есть.
Но подождите, это еще не все.
В процессе активной разработки и общения с несколькими личностями Dota 2 функции добавляются по мере их запроса.
Наложение и команда победы и проигрыша
Расскажите всем, кто смотрит, какой у вас текущий рекорд побед и поражений. Автоматически отображает рейтинг или отсутствие рейтинга, или и то, и другое одновременно!
Значок MMR и отслеживание
Покажите свой текущий рейтинг или положение в таблице лидеров на трансляции.
Обнаружение смурфиков
Показывает общее количество игр, сыгранных каждым игроком в матче.
Игры за всю жизнь: Viper: 408 · Doom: 657 · Hoodwink: 2,243 · Lina: 2,735 · Sniper: 2,850 · Drow Ranger: 3,136 · Clinkz: 3,384 · Tusk: 4,202 · Pugna: 4,466 · Dazzle: 6626
Дотабоду есть что сказать
Но только тогда, когда условия игры соответствуют правильным параметрам.
Пудж закурил!
Пудж погиб от пассивного огня фейри
Кто поставил игру на паузу?
Мы переключали ступени 6 раз, чтобы сэкономить в общей сложности 284 маны в этом матче.
Устали копировать три часа времени? Дотабод знает, когда Роша убивают или когда берут Эгиду. Таймер будет отображаться для просмотра вашими зрителями!
Рошан убит! Следующий рошан между 30:27 и 33:27 · Смерти Роша: 1 · Следующий дроп: осколок Ага. · Инвокер взял эгиду!
Пудж подобрал эгиду!
Игровые медали
Возвращает рейтинги каждого игрока в игре.
Legion Commander: #856 · Dark Willow: #402 · Crystal Maiden: #321 · Weaver: #553 · Storm Spirit: #794 · Doom: #536 · Rubick: #524 · Dawnbreaker: # 946 · Веномант: #631 · Похититель жизни: #294
Фиксированные цены, без платы за управление.
Независимо от того, являетесь ли вы одним человеком, пытающимся добиться успеха, или крупной фирмой, пытающейся захватить мир, у нас есть план для вас.
Вы уже какое-то время ведете стрим. Больше транслируйте с Dotabod и увеличивайте свое игровое время.
- Это буквально бесплатно
- Сменщик сцен OBS
Ты хороший человек. Вы знаете, что Dotabod бесплатен, но все равно хотите поддержать проект ❤.
- Всё на бесплатном уровне
- Эксклюзивный значок Discord
- Логотип или имя, видимые на Dotabod.com
- Логотип или имя видны на Github проекта
- Доступ к предварительным версиям
- Отдавайте приоритет отчетам об ошибках
- Доступ к личному Discord для разработчиков DM
Поддержать проект
Почему имя Дотабод?
Потому что в каждом из нас есть немного дота-тела
. Но еще и потому, что дотабота взяли.Как мне объяснить жене, какие деньги я снимаю с нашего общего банковского счета за использование Dotabod?
Такое ощущение, что это на сто процентов твоя проблема. Dotabod не несет никакой ответственности за ваши семейные обиды. Но вы также можете просто сказать ей, что Дотабод свободен.
Как мне начать?
Войдите в свою учетную запись Twitch, и вы сможете сразу же начать ее использовать. Появится экран настройки, который проведет вас через весь процесс.
Will my viewers like this?
Why is Dotabod based?
Let’s just say it’s because stream snipers will never find us.
Is there any age limit to using Dotabod?
You can use Dotabod even if you’re 9 years old. Or a dog. But there might be age limits to streaming on Twitch. So ask a parent first.
How does it work?
We use the Dota 2 gamestate integration API, and give you a browser source overlay to use with OBS.
Isn’t this against TOS?
Not at all. In fact, Valve themselves sanction and allow this practice officially. Valve provides Gamestate Integration for Dota 2 so that any thirdparty apps, like Dotabod, can receive live data.
Will this make me a better streamer?
Here’s the thing: yes.
Привет, в этой статье будут рассматриваться легальные
способы получить преимущество перед противником с помощью таких простых средств, как NodeJS, Electron и React, при этом обходя бан стороной. На эксперименты меня вдохновила другая статья Визуализация времени возрождения Рошана
и желание автоматизировать часть рутины. Стоит заметить что сейчас будут рассматриваться инструменты не модифицирующие каким либо нечестным способом игру — все API открыты, данные получены честным путём, никакого вмешательства в процесс игры не происходит. Под катом будет несколько картинок и немного кода.

Весь исходный код расположен на Github
, с ним можно ознакомится, лайкнуть, форкнуть, предложить изменения. Писал его левой пяткой правой ноги, прямо во время игры, поэтому просьба не ругаться сильно за стилистику.
Если честно, то я ничего нового не придумал, уже всё до меня придумали и даже есть готовые приложения, которые примерно тоже самое умеют.
Дальнейшими знаниями можно пользоваться, как во имя добра — делать инструменты для студий аналитики, киберспорта, стримов Twitch, тренировок команд и т.д., так и во имя зла — написания читов, выбор за вами.
Disclaimer: Автор не несёт ответственности за применение вами знаний полученных в данной статье или ущерб в результате их использования. Вся информация здесь изложена только в познавательных целях. Особенно для компаний разрабатывающих MOBA, чтобы помочь им бороться с читерами. И, естественно, автор статьи ботовод, читер и всегда им был.
В итоге созданные инструменты умеют:
Отслеживать игровое время
Воспроизводить звуки до начала важных событий
Отображать текущие показатели золота (GPM)
Отображать статистику по герою из открытого источника OpenDota.com
Отслеживать время возрождения рошана
Собирать данные о любимых героях противника
Какие ещё можно сделать улучшения:
Отображать историю средних показателей противника
Добавить ретроспективный анализ игры по её окончанию
Добавить больше звуковых/визуальных уведомлений
Дать возможность отслеживать «ультимейты»
Добавить больше визуальных данных во время просмотра киберспортивных игр
Добавить подробнейшие руководства прямо во время игры

С чего всё начиналось
У Dota 2 неожиданно есть GSI ( Game State Integration
), который придумали для интеграции сторонних приложений / оверлеев (наложение картинки поверх игры) и синхронизации этих самых оверлеев с игрой в реальном времени. Это говорит о том, что можно получать состояние игры и дальше что — то с ним делать. Для интеграции с NodeJS есть готовое решение в виде библиотеки
. Для работы GSI сервера, в первую очередь, нужно создать файл конфигурации в «Steam\steamapps\common\dota 2 beta\game\dota\cfg», в этом файле прописываются настройки, например, такие:
"dota2-gsi Configuration"
{ "uri" "http://localhost:3001/" "timeout" "5.0" "buffer" "0.1" "throttle" "0.1" "heartbeat" "30.0" "data" { "buildings" "1" "provider" "1" "map" "1" "player" "1" "hero" "1" "abilities" "1" "items" "1" "draft" "1" "wearables" "1" }
}
После перезапуска игры, подтягиваются новые настройки и запускается сервер GSI, который будет отправлять данные по HTTP на localhost:3001, на котором и поднимается NodeJS сервер
:
var server = app.listen(3001, () => { console.log("Dota 2 GSI listening on port " + server.address().port);
});
Собственно, после запускается код слушателя
, который как раз и позаимствован из сторонней библиотеки NodeJS
Сервер получения данных об игровом мире готов.
Данные, которые не дают преимущества
Во время рейтингового матча в Dota 2, GSI отдаёт обрезанные данные, из полезного доступно
Прошедшее количество секунд с начала игры
Игровое время в секундах
Пример данных (в игре запущена карта с демо режимом)
{ "ip": "::ffff:127.0.0.1", "gamestate": { "buildings": { "radiant": { "dota_goodguys_tower1_top": { "health": 1800, "max_health": 1800 }, "dota_goodguys_tower2_top": { "health": 2500, "max_health": 2500 }, "dota_goodguys_tower3_top": { "health": 2500, "max_health": 2500 }, "dota_goodguys_tower1_mid": { "health": 1800, "max_health": 1800 }, "dota_goodguys_tower2_mid": { "health": 2500, "max_health": 2500 }, "dota_goodguys_tower3_mid": { "health": 2500, "max_health": 2500 }, "dota_goodguys_tower1_bot": { "health": 1800, "max_health": 1800 }, "dota_goodguys_tower2_bot": { "health": 2500, "max_health": 2500 }, "dota_goodguys_tower3_bot": { "health": 2500, "max_health": 2500 }, "dota_goodguys_tower4_top": { "health": 2600, "max_health": 2600 }, "dota_goodguys_tower4_bot": { "health": 2600, "max_health": 2600 }, "good_rax_melee_top": { "health": 2200, "max_health": 2200 }, "good_rax_range_top": { "health": 1300, "max_health": 1300 }, "good_rax_melee_mid": { "health": 2200, "max_health": 2200 }, "good_rax_range_mid": { "health": 1300, "max_health": 1300 }, "good_rax_melee_bot": { "health": 2200, "max_health": 2200 }, "good_rax_range_bot": { "health": 1300, "max_health": 1300 }, "dota_goodguys_fort": { "health": 4500, "max_health": 4500 } } }, "provider": { "name": "Dota 2", "appid": 570, "version": 47, "timestamp": 1613780229 }, "map": { "name": "dota", "matchid": "0", "game_time": 2, "clock_time": 1, "daytime": true, "nightstalker_night": false, "game_state": "DOTA_GAMERULES_STATE_GAME_IN_PROGRESS", "paused": false, "win_team": "none", "customgamename": "C:\\Program Files (x86)\\Steam\\steamapps\\common\\dota 2 beta\\game\\dota_addons\\hero_demo", "ward_purchase_cooldown": 0 }, "player": { "steamid": "76561198282999022", "name": "D1rty F0x", "activity": "playing", "kills": 0, "deaths": 0, "assists": 0, "last_hits": 0, "denies": 0, "kill_streak": 0, "commands_issued": 0, "kill_list": {}, "team_name": "radiant", "gold": 99999, "gold_reliable": 0, "gold_unreliable": 99999, "gold_from_hero_kills": 0, "gold_from_creep_kills": 0, "gold_from_income": 2, "gold_from_shared": 0, "gpm": 3913086, "xpm": 0 }, "hero": { "xpos": -6700, "ypos": -6700, "id": 6, "name": "npc_dota_hero_drow_ranger", "level": 1, "alive": true, "respawn_seconds": 0, "buyback_cost": 8540, "buyback_cooldown": 0, "health": 560, "max_health": 560, "health_percent": 100, "mana": 255, "max_mana": 255, "mana_percent": 100, "silenced": false, "stunned": false, "disarmed": false, "magicimmune": false, "hexed": false, "muted": false, "break": false, "smoked": false, "has_debuff": false, "talent_1": false, "talent_2": false, "talent_3": false, "talent_4": false, "talent_5": false, "talent_6": false, "talent_7": false, "talent_8": false }, "abilities": { "ability0": { "name": "drow_ranger_frost_arrows", "level": 0, "can_cast": false, "passive": false, "ability_active": true, "cooldown": 0, "ultimate": false }, "ability1": { "name": "drow_ranger_wave_of_silence", "level": 0, "can_cast": false, "passive": false, "ability_active": true, "cooldown": 0, "ultimate": false }, "ability2": { "name": "drow_ranger_multishot", "level": 0, "can_cast": false, "passive": false, "ability_active": true, "cooldown": 0, "ultimate": false }, "ability3": { "name": "drow_ranger_marksmanship", "level": 0, "can_cast": false, "passive": true, "ability_active": true, "cooldown": 0, "ultimate": true } }, "items": { "slot0": { "name": "empty" }, "slot1": { "name": "empty" }, "slot2": { "name": "empty" }, "slot3": { "name": "empty" }, "slot4": { "name": "empty" }, "slot5": { "name": "empty" }, "slot6": { "name": "empty" }, "slot7": { "name": "empty" }, "slot8": { "name": "empty" }, "stash0": { "name": "empty" }, "stash1": { "name": "empty" }, "stash2": { "name": "empty" }, "stash3": { "name": "empty" }, "stash4": { "name": "empty" }, "stash5": { "name": "empty" } }, "draft": {}, "wearables": { "wearable0": 77, "wearable1": 76, "wearable2": 5841, "wearable3": 80, "wearable4": 78, "wearable5": 267, "wearable6": 79, "wearable7": 8632, "wearable8": 737, "wearable9": 14912 }, "previously": { "player": { "gpm": 5000054 } } }
}
Если просматривать реплей или чужую игру, то доступно гораздо больше информации — вся она описана тут
. Что-ж, самая важная информация нам уже доступна — GPM, игровое время, Id героя.
После получения информации мы должны с ней что — то поделать, например, отрисовать или предупредить о наступившем моменте в игре.
UI, Оповещения, Electron
Для UI решено было использовать Electron и внутри этого электрона запускать React. Идея заключается в том, чтобы рисовать Electron приложение поверх игры (оверлей). Исходник оверлея можно найти тут
, немного задержимся на нём — есть пару особенностей.
Для начала нужно настроить окно, в котором будет всё отображаться:
const win = new BrowserWindow({ width: 210, height: 200, // Окно должно без рамки frame: false, // Окно может быть прозрачным transparent: true, webPreferences: { // Фикс багов связанных с импортами React nodeIntegration: true, },
});
// Окно должно всегда быть поверх остальных
win.setAlwaysOnTop(true, "screen-saver");
Сначала у меня не получалось поверх доты что — то вывести, пока не наткнулся на настройку в файле machine_convars.vcfg (Dota 2) под названием «dota_mouse_window_lock», которую нужно выставить в «0», а в самой игре (либо в тех же файлах конфигурации) настроить режим отображения в окне без рамки.
UI написан с использованием React, поэтому решено его было загружать прямо с dev сервера разработки (да, я ленивый):
function loadWindow() { setTimeout(() => { win.loadURL("http://localhost:3000").catch(loadWindow); }, 3000);
}
loadWindow();
Если dev сервер не успел загрузиться, то мы попробуем ещё разок через 3 секунды, вот для этого и нужен setTimeout.
Всё, с overlay закончили, теперь UI часть.

В UI был выбран мой любимый стек: TS, CRA (Styled только для одного/двух классов использовался — рисовать то особо нечего). После того, как GSI Dota2 отправил данные на express сервер, их нужно передать на фронт. Пишется простая GET ручка для отдачи данных
. Затем на фронте пишется хук, который раз в секунду запрашивает эти данные и дальше они попадают сразу во все остальные хуки. То есть в приложении каждую секунду запускаются все хуки — это важный факт, ведь иногда понадобится хранить время запуска хука, чтобы случайно его не запустить несколько раз (если этого не делать, то у вас произойдёт в лучшем случае два оповещения подряд, в худшем случае взрыв из оповещений). Логика получения данных:
import { State } from "../state/state";
import { useState } from "react";
import { useInterval } from "./useInterval";
const SERVER_URL = "http://localhost:3001/time";
const UPDATE_FREQUENTLY = Number(process.env.REACT_APP_SERVER_UPDATE_FREQUENTLY);
export function useServerState(): State { const [state, setState] = useState<State>({}); useInterval(async () => { try { const data = await (await fetch(SERVER_URL)).json(); setState(data); } catch {} }, UPDATE_FREQUENTLY); return state;
}
Теперь, когда есть все данные на фронте, можно написать хук для звуковых оповещений
, что пора бы пойти (за 30 секунд до начала оповещает) забрать руны богатства, появляющиеся на каждой минуте кратной пяти (5, 10, 15, 20 минута):
export function useBountyRunes(state: State) { const clockTime = clockTimeSelector(state); const [play] = useSound(bountiesMp3, { volume: 0.25 }); const [lastIntervalPlay, setLastIntervalPlay] = useState<number>(-1); useEffect(() => { // Проверка, что время пришло корректное if (!clockTime || isNegative(clockTime)) { return; } // Проверка кратности минуты на 4,5 и проверка от двойного оповещения if (isNeedToPlay(clockTime, lastIntervalPlay)) { // Проигрываем звук play(); // Записываем когда последний раз было оповещение setLastIntervalPlay(getInterval(clockTime + ALARM_BEFORE)); } }, [clockTime, lastIntervalPlay, play]);
}
Запись о последнем воспроизведении (setLastIntervalPlay) нужна чтобы не повторить оповещение случайно дважды.
И вот уже в игре одно преимущество, может быть оно несущественное, но как мне кажется неплохо управляет вниманием команды. Что — ж можно пойти дальше и сделать такую же кнопку возрождения рошана
, как из прошлой статьи:
Хук useRoshanSpawn для кнопки
export function useRoshanSpawn(state: State) { const currentGameTime = gameTimeSelector(state) || 0; const [play] = useSound(roshanRespawnMp3, { volume: 0.25 }); const [roshanStopwatch, setRoshanStopwatch] = useState<RoshanStopwatch>({ isActive: false, time: 0, isPlayedSound: false, }); // Фиксация смерти роши function handleDead() { setRoshanStopwatch({ time: currentGameTime, isActive: true, isPlayedSound: false, }); } // Сброс таймера function handleReset() { setRoshanStopwatch({ time: 0, isActive: false, isPlayedSound: false, }); } // Проверяем роша жив, жив/мёртв, и сколько времени прошло с момента смерти const isDead = roshanIsDead(roshanStopwatch, currentGameTime); const isDeadOrLive = schrodingerRoshan(roshanStopwatch, currentGameTime); const timeToSpawn = roshanTimeDeadSelector(roshanStopwatch, currentGameTime); useEffect(() => { // Оповещаем звуком за 30 секунд до возможного возрождения if (needToPlaySound(roshanStopwatch, currentGameTime)) { play(); setRoshanStopwatch({ ...roshanStopwatch, isPlayedSound: true, }); } }, [roshanStopwatch, currentGameTime, setRoshanStopwatch]); return { handleDead, handleReset, isDead, isDeadOrLive, timeToSpawn };
}
С рошаном всё немного запутаннее, чем с рунами — он может возрождаться в интервале от 9 до 12 минут. То есть у него есть состояния:
Точно мёртв (прошло до 9 минут с момента смерти)
Он жив или мёртв (прошло от 9 до 12 минут с момента смерти)
Он точно жив (прошло свыше 12 минут с момента смерти или это начало игры)
Поэтому у таймера есть три визуальных состояния:
Кнопка — для запуска таймера
Таймер тикает и сообщает о том что роша точно мёртв
Таймер тикает и сообщает о том что роша возможно жив, а возможно мёртв
И одно звуковое оповещение: Рошан будет в состоянии Шредингера через 30 секунд (то есть, и жив, и мёртв одновременно — пока не проверишь, не узнаешь). Также есть возможность сбросить таймер, ведь если мы проверили и узнали, что он жив — то таймер больше не нужен, а нужна кнопка о том чтобы сообщить о новой смерти рошана. Из минусов — иногда забываешь запускать таймер, было бы здорово в будущем это тоже автоматизировать.
Обогащаем данные
Ещё есть информация о том, на каком герое мы играем, поэтому пускай клиент запрашивает бенчмарки с сайта OpenDota.com
и отображаем их, чтобы было понятно, на сколько мы отстаём от ритма игры. Я взял перцентиль 99%, то есть мне интересно, с какими показателями отыгрывается 1% лучших игр на том или ином герое.

Вся логика описана в хуке useBenchmark
:
export function useBenchmarks(state: State): State { const [localState, setLocalState] = useState<Benchmarks>(); const updateBenchmarksForHero = useCallback( async function (id: number) { try { // Запросили данные по герою const response = await fetch(`${BENCHMARKS_URL}?hero_id=${id}`); const benchmarks = (await response.json()) as Benchmarks; // Сохранили бенчмарк setLocalState(benchmarks); } catch (error) { setLocalState({ error, }); } }, [setLocalState] ); useEffect(() => { const heroId = heroIdSelector(state); const benchmarksHeroId = localState?.hero_id; // Запрашиваем бенчмарк, при появлении информации о герое if (heroId && heroId !== benchmarksHeroId && !localState?.error) { updateBenchmarksForHero(heroId); } }, [state, localState, updateBenchmarksForHero]); // Возвращаем данные о бенчмарке return { ...state, benchmarks: localState };
}
Узнаём предпочтения игрока
Было бы здорово получать информацию о том, на каких героях вероятнее всего будет играть противник, чтобы забанить, отобрать, законтрить их у него. Для этого нужно считывать память
файл игры: «server_log.txt» и дальше распарсить его регуляркой, найти там ID ваших оппонентов, затем запросить историю игр в OpenDota или Dotabuff. У этого способа есть минусы — если оппоненты сделал свой игровой профиль скрытым в Dota 2, то никакой информации о нём вы не получите. Есть ещё один момент, который я забыл учесть — данные могут быть устаревшими, но в коде это легко исправляется добавлением фильтра по времени.
Attention: код по ссылке
может совершить BSoD ваших глаз.
import fs from "fs";
const DEFAULT_FILE = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\dota 2 beta\\game\\dota\\server_log.txt";
// Парсим все ID в строке
const getDotaIdsFromLine = (line) => { let playersRegex = /\d:(\[U:\d:(\d+)])/g; let playersMatch; let dotaIds = []; while ((playersMatch = playersRegex.exec(line))) { dotaIds.push(playersMatch[2]); } return dotaIds;
};
const getState = () => { return new Promise((res) => { // ищем последнюю строку со словом Lobby fs.readFile(DEFAULT_FILE, (err, data) => { const rowString = data.toString(); const startIndex = rowString.lastIndexOf("Lobby"); const finishIndex = rowString.indexOf("\n", startIndex); const lobbyString = rowString.slice(startIndex, finishIndex); res(getDotaIdsFromLine(lobbyString)); }); });
};
export const getSteamIDs = () => { return getState();
};
После, будет здорово это тоже вывести, поэтому решено было сделать отдельное React приложение на localhost:3002. Без дизайна выглядит оно совсем по страшному, но это уже был просто спортивный интерес и вообще я им не пользуюсь. В нём есть информация о прошлых десяти играх. Кнопочка «Ban this id», чтобы убирать друзей, с которыми играешь, из этой статистики и ссылка на Dotabuff профиль, если вдруг хочется подробностей.

Это приложение тоже можно было бы красиво оформить в виде Electron оверлея и запускать его на стадии выбора героев, но кажется я слишком много играю в игры и мало уделяю времени действительно полезным вещам 🙂
Что ещё пробовалось:
Пробовался DLL Injection из прошлой статьи и чтение памяти с помощью Rust, но там был большой изъян в том, что все найденные указатели на структуры данных жили до обновления игры, поэтому эта идея была заброшена.
Попытка создать сервис подбора героев на основе ML обучения по выгрузке игр из OpenDota.com или с тех же серверов Valve (провал — хотя мне кажется я просто не сумел правильно приготовить ML часть)
Парсинг Dota 2 реплеев — там не сложно, используется Protobuff и все структуры легко находятся на гитхабе. Вот только что дальше с этим огромным объёмом данных делать?
Вывод: интегрироваться с Dota2 не вызывает труда, можно делать быстрый анализ прямо во время игры, при просмотре киберспортивных игр можно сделать огромное количество красивого оверлея для Twitch стрима, также можно развивать эту тему в сторону ретроспективного анализа из реплеев, что скорее всего будет полезно профессионалам.
Надеюсь вам было интересно почитать про то, как я собрал на коленке читы (на самом деле хороший вопрос — читы это или нет?), да ещё и на JS, если есть орфографические или лексические ошибки, то пишите пожалуйста в ЛС, спасибо за внимание.
Изменение рейтинга TrueSkill
(схожего с рейтингом Эло
для шахмат) нашего бота со временем, подсчитанная при помощи симуляции игр между ботами.
Проект развивался следующим образом. Рейтинг 15% игроков находится ниже отметки 1,5К по шкале MMR
; у 58% игроков он ниже 3К; у 99,99% игроков ниже 7,5К.
• 1 мая: первые результаты
обучения с подкреплением
в простом Dota-окружении, где Drow Ranger учится сражаться с жёстко запрограммированным Earthshaker.
• 8 мая: тестировщик с MMR
в 1,5K говорит, что его результаты улучшаются быстрее, чем у бота.
• Начало июня: выиграл у тестировщика с MMR 1,5K
• 30 июня: выиграл большую часть игр у тестировщика с MMR 3000.
• 8 июля: впервые с небольшим отрывом выиграл
у полупрофессионального тестировщика с MMR 7,5К.
• 7 августа: победил Blitz
(6,2К, бывший профессионал) со счётом 3-0, Pajkatt
(8,5К, профессионал) 2-1, и CC&C
(8,9К, профессионал) 3-0. Все они согласились, что SumaiL придумает, как его обыграть.
• 9 августа: победил Arteezy (10К, профессионал, один из лучших игроков) 10-0. Он сказал, что SumaiL сможет справиться с этим ботом.
• 10 августа: победил SumaiL (8,3К, профессионал, лучший игрок 1 на 1) 6-0. Игрок заявил, что бота победить нельзя. Сыграл с версией бота от 9 августа, победил 2-1.
• 11 августа: победил Dendi (7,3К, профессионал, бывший чемпион мира) 2-0. На 60% больше побед, чем у версии от 10 августа.
Игра против SumaiL
Задача
В полной версии игры сражаются игроки 5 на 5, но в некоторых
турнирах бывают
и игры 1 на 1. Наш бот играл по стандартным турнирным правилам – мы не добавляли специальные упрощения для ИИ.
Бот работал со следующими интерфейсами:
• Наблюдение: API, разработанные так, чтобы у него были те же возможности, что и у живых игроков, касающиеся героев, других персонажей игры и поверхности рядом с героем. Игра частично наблюдаема.
• Действия: доступные через API, с частотой, сравнимой с человеческой, включая движение к определённому месту, атаку и использование предметов.
• Обратная связь: бот получает вознаграждения за победу, а также простые параметры, такие, как здоровье и ластхиты
.
Мы выбрали несколько десятков предметов, доступных для бота, и выбрали из них один для изучения. Также мы отдельно тренировали блокирование крипов при помощи традиционных техник обучения с подкреплением, поскольку это происходит до того, как появляется соперник.
Бот играет против Arteezy
Турнир The International
Наш подход, комбинирующий игру с самим собой и обучение извне, позволил нам значительно усилить игру нашего бота с понедельника по четверг, пока шёл турнир. Вечером в понедельник Pajkatt выиграл, используя необычную сборку предметов. Мы добавили эту сборку в список доступных предметов.
В районе часа дня в среду мы протестировали последнюю версию бота. Бот терял очень много здоровья после первой волны. Мы решили, что нужно откатиться, но затем заметили, что последующая игра была потрясающей, и поведение в первой волне было всего лишь приманкой для других ботов. Последующие игры с самим собой решили проблему, когда бот выучился противостоять стратегии с приманкой. А мы совместили это с понедельничным ботом только для первой волны, и закончили всего за 20 минут до того, как появился Arteezy.
Arteezy сыграл матч с нашим тестировщиком уровня 7,5К. Arteezy выигрывал игру, но наш тестировщик сумел удивить его при помощи стратегии, подсмотренной у бота. Arteezy позже заметил, что эту стратегию против него однажды использовал Paparazi, и что к ней довольно редко прибегают.
Pajkatt выигрывает у понедельничного бота. Он заманивает бота, а потом использует регенерацию.
Уязвимости бота
Хотя SumaiL назвал бота «непобедимым», он всё ещё может запутаться в ситуациях, слишком отличающихся от того, что он видел. Мы запустили его на одном из мероприятий, проходивших на турнире, где игроки играли более 1000 игр с целью победить бота всеми возможными способами.
Удачные уязвимости попали в три категории:
• Перетягивание крипов. Можно постоянно заставлять крипов с линии гнаться за вами сразу после их появления. В результате за вами по всей карте будет бегать несколько десятков крипов, и вражеские крипы уничтожат башню бота.
• Orb of venom + wind lace: дают вам преимущество в скорости передвижения над ботом на первом уровне и позволяют быстро нанести урон.
• Raze на первом уровне: требует навыков, но несколько игроков класса 6-7K смогли убить бота на первом уровне, удачно выполнив 3-5 заклинаний за короткое время.
Исправление проблем для матчей один на один будет похоже на исправление бага с Pajkatt. Но для матчей 5 на 5 эти проблемы уязвимостями не являются, и нам будет нужна система, способная справиться со странными ситуациями, которые она не видела до этого раньше.
Инфраструктура
Мы пока не готовы обсуждать внутренние особенности бота – команда работает над решением задачи с игрой 5 на 5.
Первым шагом проекта было понять, как запустить Dota 2 в облаке на физическом GPU. Игра выдавала непонятную ошибку в таких случаях. Но при запуске на GPU на десктопе Грега (во время шоу этот десктоп выносили на сцену) мы заметили, что Dota загружается с подключенным монитором, и выдаёт то же самое сообщение без монитора. Поэтому мы настроили наши виртуалки так, чтобы они притворялись, будто к ним подключен физический монитор.
В то время Dota не поддерживала выделенные серверы, то есть запуск с масштабированием и без GPU был возможен только в варианте с очень медленным софтовым рендером. Затем мы создали заглушку для большей части вызовов OpenGL, кроме тех, что нужны были для загрузки.
Одновременно мы написали бота на скриптах – в качестве эталона для сравнения (в частности потому, что встроенные боты плохо работают в режиме 1 на 1) и чтобы понять семантику API для ботов
. Скриптовый бот доходит до 70 ластхитов за 10 минут на пустом пути, но всё равно проигрывает достаточно хорошо играющим людям. Наш лучший бот играющий 1 на 1, доходит до отметки порядка 97 (башню он уничтожает раньше, так что мы можем только экстраполировать), а теоретический максимум – 101.
Бот играет против SirActionSlacks. Стратегия отвлечения бота толпой курьеров не сработала
Пять на пять
Игра 1 на 1 – сложная задача, но 5 на 5 – это океан сложности. Нам нужно будет расширить пределы возможности ИИ, чтобы он смог с ней справится.
Привычным образом мы начнём с копирования поведения. В Dota проходит порядка миллиона публичных игр в день. Записи матчей хранятся на серверах Valve две недели. Мы скачиваем каждую запись игры на экспертном уровне с прошлого ноября, и набрали набор данных объёмом в 5,8 млн игр (каждая игра – примерно 45 минут с 10 игроками). Мы используем OpenDota
для поиска записей и перечислили им $12000 (что в десять раз больше того, сколько они хотели собирать за год) для поддержки проекта.
У нас ещё много идей, и мы нанимаем
программистов (интересующихся машинным обучением, но не обязательно экспертов) и исследователей нам в помощь. Мы благодарим Microsoft Azure и Valve за поддержку в нашей работе.