Бильярдный бот: история создания

Привет, хабрахабр! О чём эта статья? Эта статья посвящена подробному описанию процесса создания биллиардного бота, который без участия человека играет в игру pool billiard и принимает решения, зарабатывая очки. Статья будет полезна и интересна людям, увлекающимся созданием ботов и программированием.Предисловие afa8dbad600aadece36a0aee09868a58.jpgУ всех нас есть любимые игры и виды спорта. Здорово, когда первое совпадает со вторым. Помимо своих увлечений спортом и спортивными проектами, я люблю также и некоторые компьютерные игры. Одна из моих любимейших игр, и вживую, и виртуально — это, конечно же, бильярд. Бильярд, пул, снукер… как угодно, — я люблю их все! Я разделяю мнение многих о том, что, например, снукер — это «недискретные» шахматы. Мало просто забивать последовательность определённых шаров в лузы, там ведётся ещё и невероятная стратегическая борьба. Борьба за снукеры, за позиции…, а какой фантастической техникой обладают профессиональные бильярдисты — просто молчу в тряпочку.cbea92962aea55e179250d468b5d4109.jpgДостоинства этой несомненно аристократической игры можно перичислять очень долго. Но перейдём к сути статьи. Моя самая любимая игра в бильярд вот уже пять лет и по сегодняшний день — это «Pool Billiard» на Facebook. Она классно сделана не только эстетически, но и технически. Невооруженным глазом видны классно написаный физический движок, продуманный геймплей, клиент-серверная валидация действий, обработка ошибок, дизайн, система статистики, магазин, чат в конце концов. Игру явно делали профи, да и она в топах. В неё очень приятно играть… и выигрывать! Я достаточно долго играл в неё, пока в голову не пришла мысль: «Ба! Да она же идеально подходит для создания под неё игрового бота!» Выигрывать приятно, а выигрывать своим роботом, автоматически — вдвойне! Выигрывать у платных игроков, понакупивших систему навигации и подкручивания битка, демонстрируя им фантастические по технике и красоте удары, оставляя их с отвисшими челюстями — втройне приятно! Плюс автоматический набор очков опыта и монет: оставил робота на ночь, под утро ты лучший! Кроме того, я даже как зритель обажаю часами смотреть на игру в бильярд.В общем, да, я решился! Добро пожаловать под кат! :)Создание Перед началом создания я, имея на тот момент уже довольно большой опыт игры в Pool Billiard, всё тщательно продумал с карандашом и листочком в руках. Конечно, всё предугадать невозможно, но «скелет» был создан. Внесите скелет! Для создания бота я использовал C++. Написание и отладка кода в факультативном режиме по вечерам заняли у меня почти четыре месяца. Согласитесь, сложно отлаживать что-то на закрытой системе, особенно когда необходимо оттестировать некоторый редкий или исключительный случай, который случается в одной из ста партий. Я ведь не могу фривольно расставить шары и смоделировать ту или иную ситуацию на столе.e60b9f130ed3438e73be098631aed0d1.jpg Как робот взаимодействует с игрой? f227544a9e3fb4a05c8bf8262167ac21.jpgЯ понял, что бот будет взаимодейтсовать с браузером посредством полной имитации человека: распознавания образов и имитации человеческого ввода: мышь, клавиатура. Слава богу, перед каждым ударом не надо вводить капчу ;))) Так я и сделал: люблю системы имитации поведения человека, от которого браузеры так беззащитны! Значит, нужно уметь находить на экране регион с игрой, уметь взаимодействовать с ним, получать от игры некие сигналы и обрабатывать их, и, что самое сложное, уметь правильно осуществлять удары. Я составил список необходимых действий, которые должен уметь осуществлять бот (например, зайти в определённую игру, отправить в чат определённое сообщение, поставить шар в определённую точку, проверить количество очков, нанести удар определённой силы под определённым углом, разбить пирамиду и так далее) и список определённых сигналов, которые бот должен уметь распознавать (например, начался наш ход, раунд внезапно закончился, противник сдался, денег мало и так далее). Всего получилось около сорока ситуаций; некоторые получились асинхронными, некоторые — нет. Я запрограммировал и насколько это возможно тщательно оттестировал каждую из них (об этом отдельно), после чего строительные кубики бота были готовы.Какова общая логика работы? Я решил, что общая логика работы будет, разумеется, state-машина. Вначале бот калибруется на игру, потом вычисляет количество имеющихся денег, потом решает какую ставку сделать. Далее следует сам игровой процесс, после которого бот вновь решает как лучше войти в новую игру. Чем проще логика, тем надёжней и предсказуемей работает система. И проще ловить баги.5519698ca6f016fd26b2cabe1e8c0ae2.jpg Какова детальная логика работы? Детальная логика сурова и бородата, она содержит целую кучу ветвлений и возможных ситуаций. Начинается всё с того, что запускается игра и сам бот. Игра (окно браузера со стартовым экраном игры) находится на экране. Её координаты записываются и это служит отправной точкой для всех последующих действий ввода мышью и распознавания ситуации.803876ca76d74354af30313f747ae4d9.jpgДалее бот определяет количество моих денег, чтобы понять, какие ставки можно делать. После выбора размера ставки бот заходит в соответствующую комнату и начинается игра. Она может закончиться в любой момент, так как уже с этой секунды оппонент может закрыть браузер, и, спустя некий таймаут, наш раунд признают недействительным. Оппонент может также сам выйти из раунда в любой момент: в этом случае победа будет присуждена мне. Распознавание всех этих ситуаций начинается прямо тут.Далее сервер случайно определяет, кто начнёт игру: я или оппонент. Бот ожидает начала своего хода. На весь ход даётся только 20 секунд. За это время надо успеть определить, что ход уже мой, считать со стола шары, очки, правильно их распознать, найти нужный удар и успеть его осуществить.Бот сам определяет, будет ли он разбивать пирамиду, и на этот случай у него припасена парочка просто отпадных вариантов. Если разбивать пирамиду не надо, бот распознаёт координаты шаров на столе, какие шары он может забивать (цветные или полосатые или любые_кроме_чёрного или только_чёрный (судя по ситуации в игре)), количество очков и всю-всю-всю остальную канитель.После этого бот рассчитывает и наносит удар. (самый короткий, но самый важный абзац)Бот, помимо основных занятий и не в ущерб игре, любит также початиться с оппонентами. Обычно, такое временное окошко возникает сразу после осуществелния удара. Вот тут-то бот и пишет некоторое чат-сообщение оппоненту.После проведения удара либо ход переходит к сопернику, либо продолжаем играть, либо мы победили, либо мы проиграли. Бот ждёт признак одной из этих ситуаций.В случае поражения либо победы бот закрывает текущее окно игры и сводное окно результатов и переходит вновь к распознаванию количества денег.State-машина в общем примерно такая. 672991f1b85497ef55f033418731b9d1.gif Как игра находится на экране? Я читаю контекст всего экрана в C++. Игра находится на экране очень просто. Так как дизайн игры не «резиновый» (она не тянется), а фиксированный, то все элементы постоянно находятся на одном и том же расстоянии друг от друга. Значит, необходимо найти на экране с игрой некий статичный элемент, который никогда визуально (графически) не меняется, и вычислить координаты начала и конца окна относительно него. Сделав пару скриншотов и обработав всё в фотошопе, я так и сделал. Я сделал простой и надёжный маркер распознавания стола. Я решил отказаться от идеи распознавать координаты окна перед каждым ударом. Это напряжно и отнимает драгоценные моменты рассчёта идеального удара. Поэтому бот определяет позицию игры только один раз — при запуске. После того, как бот нашёл окно игры, двигать окно браузера даже на один пиксель строго запрещено: в процессе игры на бильярдном столе неточность даже в один пиксель при длинных ударах накапливает чудовищно огромную погрешность. Отсюда все удары будут неверными.Маркер игры — это неизменяемая часть изображения игрового окна. 44b6606fca1e6d35ba278c8573e84b6d.jpg Как определяется количество денег? Тут мне просто повезло. Мне не пришлось ставить снифер на браузер и слушать трансляцию игры для определения количества денег или распознавать символы с экрана. Всё прозаично: дело в том, что в игре не всё сделано на флеше. Сам элемент с суммой моих монет — простое текстовое поле, легко выделяемое мышью. Я просто имитирую следующее: подвожу мышь к началу поля, зажимаю ЛКМ, провожу вправо до конца поля, отпускаю ЛКМ. В результате бот попросту выделяет поле с деньгами. Потом я копирую его в буфер обмена, удаляю спецсимволы типа пробелов, запятых и точек… и всё! Сумма монет у меня в кармане;)Игровые монеты — это просто выделяемое поле. 1e32ef511a7a009794f9b8b1d6300706.jpg Как выбирается комната для игры? Есть несколько комнат для игры, каждая из которых содержит определённую ставку (плату за вход). Два оппонента заходят в комнату, ставя каждый, например, по 100 монет. Победитель получает выигрыш в 140 монет (не 200, как это можно было ожидать (это сделано гейм-дизайнерами для того, чтобы постоянно из игры утекала масса денег, и люди были вынуждены пользоваться магазином и докупать себе монет за реальные деньги и играть, либо ждать пока ежедневное колесо фортуны не принесёт им монет для игры)). Проигравший уходит ни с чем. Логика проста.5de2ece60357244fe1ed9edb4e565750.jpgВ дешёвых комнатах тусуется вечно всякий сброд: новички, лузеры и так далее. Ради прикола туда иногда заходят и профи. Вероятность выиграть там очень высока.В комнатах подороже можно встретить игроков средней руки, иногда новички заходят туда по глупости, иногда суперпрофи спускаются туда (видимо, чтобы порвать в клочья кого-то). Вероятность выиграть там примерно 50%.В дорогих комнатах тусуется элита: либо суперпрофи, либо платники с купленной системой навигации, подсказок и так далее. Скорей всего, там светит проигрыш.У бота есть несколько режимов выбора ставки: принудительные режимы (когда бот всегда ставит ту сумму, которую я ему указал) и авторежим. Авторежим сделан очень интересным образом. Я собрал статистику выигрыша ботом в комнатах разных ставок и на её основе написал весьма интересную программу. Она всего лишь выдаёт на выход несколько чисел, но очень важных чисел. Это так называемые пороговые количества монет. Например, если у нас 300 монет, бот имеет право играть только в первой комнате, а вот после преодоления порогового количества в 740 монет бот уже имеет право играть в первой и во второй комнате. Разумеется, бот стремится играть в самой дорогой комнате.Так вот, программа интересна тем, что она автоматически подбирает эти самые пороговые значения по некоторому алгоритму, который она же сама дальше и использует для предсказания рисков последующего проигрыша. Она имитирует миллионы партий и принятий решения об игре в той или иной комнате (обстрел Монте-Карло) и выдаёт правильные пограничные количества монет. Причём, я заметил, алгоритм — сходящийся! Непосредственно вход в комнату осуществляется нажатием на определённый фрагмент экрана, где расположена кнопка входа и осуществления определённой ставки.После выбора комнаты жмём на соответствующую кнопку. f40dd91d795d73a7be84bf24af0fe75f.jpg Как определяется возможность хода? Тут на помощь вновь приходят маркеры. На игровом экране всё так же статично, как и на главном, и все элементы находятся всегда на одних и тех же местах. Это панацея. Аватарка моего оппонента, а также статистика, окно сообщений и прочие элементы, всегда находятся слева. Мои — справа. Я так же создал простой и надёжный маркер определения моего хода. Как только бот читает его, он переходит к следующей фазе.d2460a38c9a5f02d6715eca4850733fc.jpg Как распознаются шары? Вот тут целая эпопея в трёх актах. Не буду описывать всех своих двухмесячных страданий по этому вопросу, опишу происходящее только в кратце. Из сфотографированного на момент распознавания шаров изображения стола вычитается изображение чистого стола, тщательно подготовленного мною в фотошопе. Далее находятся высококонтрастные зоны, на которых находятся пятна шаров. По определённым специфическим световым маркерам с учётом недопустимости overlap’a находятся центры шаров. Далее они сортируются по контрастности ярковыраженности, чтобы не допустить попадание мусора с экрана (кий, надписи на столе). После чего положение шаров уточняется проверкой тени вокруг них. Наконец, получили список нужного количества центров шаров на изображении.0ae5c5a206827dc5b2fecdc5ed6923fa.jpgКак только я не пытался по-другому искать шары! И с помощью определения теней, и с помощью метода границ Канни, и с помощью хитроумных классификаторов: все эти и многие другие методы были очень низкорезультативны и неточны по сравнению с методом, написанным лично мной. Я даже натравливал на статистику Wolfram Mathematica, но особо чёткого ответа это мне не дало. Может, плохо натравливал.bc1551cf95cffd461f5473c3278d4176.jpgНо мало узнать центры шаров, надо ещё определить где полосатый, где сплошной, где белый (биток), а где — чёрный (восьмёрка) шары. Тут безграничной фантазии не было границ, потому что надо было унифицированно решить целую кучу проблем. Например, зелёный шар очень сильно сливается с сукном, а если полосатый жёлтый шар остановится так, что его белый полюс будет направлен строго в камеру, то он будет почти настолько же белый, насколько таковым является биток. Тонкостей всплыло целый миллиард, и со всеми ними я вынужден был бороться.Я перебрал целое море решений, отбрасывая их из-за недостаточной точности, и, наконец, сам написал самый точный из них. У меня получилось решающее дерево, в каждом узле которого — самописный классификатор. Для каждого найденного шара я определяю цветовые характеристики: средний {R, G, B}, средний {H, S, B}, самый яркий и самый тёмный цвет без учёта цветовых пятен. Потом я однозначно нахожу биток как самый белый шар. Далее пытаюсь определить восьмёрку как самый тёмный шар. После всего, необходимо разделить цветные от полосатых. Но непросто ларчик открывался: я составлял трёхмерные таблицы выборок, фотографировал по тысяче раз шары в попытках обучить сеть, но всё же мне так и не удалось построить однозначно разделяющую кластеры сплошных и полосатых шаров гиперплоскость. Всё равно ошибки встречаются. Для этого случая я сделал небольшую коррекцию-ошибок-на-лету. Об этом — чуть ниже.Тут в ход пошли разные высосанные из пальца ортонормированные базисы поворотов, статистики кластеров с предустановленными цветами шаров и прочая математическая основа, но всё было не то… и не то… перебрал я очень много вариантов.В конце-концов, некий набор шаров с их достаточно точными характеристиками у нашего бота есть: обязательно есть белый и чёрный шары, и наборы сплошных/полосатых. Поверьте, проделана не малая работа.Как определяется возможность разбития пирамиды? Тут я было расслабился: создаём простой классификатор позиции пирамиды (каждый шар стоит в точке пирамиды) и если наш ход и наш ход — первый, то разбивам. Но не тут то было! Что-то не срабатывало! Оказывается, хитрые программисты добавили некоторый шум в начальные позиции шаров в пирамиде. Умно! А иначе можно было бы записать «идеальную партию» — безошибочную серию ударов, ведущую к победе, и, постоянно воспроизводя её, выигрывать. Но они предусмотрели такую вот защиту. Чуть подкорректировав классификатор, я достиг успеха в распознавании момента разбития пирамиды.Когда надо разбивать пирамиду, бот выбирает один из нескольких намертво запрограммированных в него роскошных вариантов разбития и реализует его. Почти все из вышеописанных вариантов заставляют один-два шара сразу же залететь в лузы. Ну, или накрайняк очень сильно разбрасывают шары по столу, что моему алгоритму только на руку.5f48fa1876651fd5f19f538f8eedbaad.jpg Как определяется количество очков на столе? Возле моей аватарки и аватарки оппонента есть стек из забитых шаров. Он представляет из себя полосу с помещёнными туда забитыми шарами чуть меньшего радиуса, чем на столе. Однако, как быть? Одного распознавания шаров на столе мало, чтобы понять, какую серию я играю — полосатых или сплошных. Значит, надо распознать не только количество шаров в стеках, но ещё и сами шары! Пфффф… алгоритм «вычитания стола» тут не подходит, да и размеры не те.…невероятным усилием воли я заставил себя написать ещё один распознаватель…и вторым невероятным усилием — объединить оба этих распознавателя в одну универсальную функцию распознавания. Фактически, пришлось всё писать с нуля. Вот она — проблема недостаточного планирования. Но зато теперь я указываю регион и размер шаров, а на выходе получаю координаты и характеристики каждого шара! Победа разума! Определив количество и тип забитых мною и противником шаров, ситуация на столе становится полностью детерминистичной. Теперь бот знает, что и как ему необходимо забивать.63c8e91bb209146a5395a2ff4d81f4aa.jpg Как определяется какие шары бить? Тут запрограммирован строго детерминистичный алгоритм. Если мы в начале партии и разбиваем пирамиду, то можно бить в любой шар (всё равно в чёрный мы не попадём — он спрятан внутри пирамиды). Если первый ход уже был осуществлён — смотрим на количество забитых шаров. Если их нет — можно бить любой, кроме чёрного. Если есть только у противника — взять противоположный тип и бить. Если есть у меня (и не только у меня) — бить такой же тип. Наконец, если мною забиты все шары моей серии кроме чёрного — забивать чёрный. Всё! ;)d5c8a579f916bbfe7386a6a57be2171a.jpg Как определяются фолы противника? Иногда оппонент косячит. В этом случае правила игры обязывают меня к свободному удару: я должен сам поставить биток в любую точку стола и нанести удар. Итак, есть две проблемы: распознавание фола противника и правильная постановка шара под удар.Первая проблема решается относительно просто. В отдельном потоке некий отслеживатель пытается найти такую ситуацию: сейчас ход противника и появился маркер «Fault!» на столе. В этом случае он сообщает главному потоку о том, что следующий удар необходимо делать с постановкой.Вторая проблема — проблема постановки — решается чуть сложнее. Хотя, признаюсь вам, друзья, тут я схалтурил. Я не пытаюсь поставить биток по некоторой линии забития одного из шаров своей серии. Нет. Я просто пытаюсь поставить шар универсально — максимально близко к центру стола, где нет препятствий. Сначала я распознаю ситуацию на столе, потом вычисляю ближайшую незакрытую к центру стола точку и ставлю туда шар. Пока происходит постановка, я уже почти наверняка вычислю идеальный удар.3ff9f7e5a8634074fba42243ccb43071.jpg Как вычисляется угол и сила удара? Итак, передо мной возникла задача за заданное время при заданной конфигурации шаров на столе и заданном правиле нанесения ударов рассчитать все возможные результативные удары и выбрать среди них лучший. Возможные значения заданного времени: {20 секунд}. Возможные конфигурации шаров: их?… Возможные правила нанесения ударов: {любые кроме чёрного, только сплошные, только полосатые, только чёрный}.Забивание шара Прежде чем влезать в дебри бильярда, необходимо вначале понять физику происходящего. Что нужно, чтобы забить шар? 31376ffb7be172ccc26b71de44976b24.jpg Для этого необходимо провести воображаемый отрезок от центра лузы до центра шара и чуть-чуть продлить его дальше до пересечения с границей шара. Точка пересечения и есть точка удара. Если ударить шар по ней, он полетит в лузу.04e5187319855dff84446774dcb3171a.jpg Чтобы биток ударил в эту точку, его надо направить отнюдь не в неё, а в точку, отстоящую от точки удара на радиус битка далее по той самой воображаемой линии.00edb6bac9132e9b6aaed42eb7ec303e.jpg Ну, а далее, я думаю, вы уже уловили. Чтобы сам биток прилетел куда надо, нужно также построить линию от целевой точки через центр битка и пересечь её с дальним краем окружности битка.71b48772eabea463a6f0ced0aef9cb54.jpg Итак, отсюда рождается общее правило: чтобы направить любой шар в заданную точку А, необходимо соединить воображаемой линией точку А и центр этого шара и продлить её до пересечения с дальней границей окружности шара. Сюда и надо ударить шар. Я решил сделать стратегию игры бота такой же, как и у людей: бот выбирает один шар под удар и забивает его. Всё. Чем проще система, тем она надёжнее и проще её отлаживать.Лузы Лузы разные. И попадание в них подчинено разным законам. Например, в угловую лузу можно попасть разными способами: 04517d3ae93acbd934dae13d54bb57a7.jpg Но для каждой лузы мне удалось найти универсальную точку, качение шара к которой из любого места влечёт попадание в лузу: 245ecfea4e8d3520418ae521e98ca666.jpg Я запрограммировал бот как раз целиться в эти точки. Единственное ограничение, которое налагают на себя лузы длинных бортов: залетание в них шаров возможно только в створке 70 градусов. Это также заложено в код.Моделируемое пространство Я решил отказаться от рикошетов. Нет нет, не пугайтесь, не в том смысле, конечно же;) Построить такое моделируемое пространство, чтобы вообще забыть о рикошетах. Они в игре есть и остаются, а вот в пространстве их не будет. Над решением этой задачи я работал лишь вечер. Я понял: соударение шара радиуса R с бортом толщиной 0 — это то же, что и соударение материальной точки радиуса 0 с бортом толщиной R: f304c344817fc314c33c06faf6a891fe.jpg Значит, можно существенно упростить рассчёт физики: в плане траекторий будем работать не с шарами, а с материальными точками. Далее: по законам жанра, угол падения шара на борт равен углу отражения. Как и в случае световых лучей и зеркальной поверхности. Вы спросите: причём тут это? Отвечу. Соударение материальной точки с бортом (рикошет о борт) — это то же самое, что и продолжение движения материальной точки сквозь борт в пространство, зеркально отражённое от этого борта. То есть если бы шар мог «видеть» вперёд по направлению своего движения, а борта были бы зеркалами, то получилось бы вот что: 31d32f7e94adab3363ffe704bca1d4f6.jpg В таком «зеркальнобортовом» пространстве мне уже не надо задумываться о рикошетах. Они вытекают сами собой. Плюс подключаем правило движения материальной точки, и получается вообще роскошно.В моделируемом пространстве все удары — прямые линии, рикошетов нет. А вот на столе они уже есть. c262e7d84733c3fc33ab32a87c3ec2e2.jpg Максимальные сила удара и путь битка Отлично, масса работы проделана! Но есть ещё чрезвычайно важный вопрос, которого мы вообще не коснулись: какой максимальный путь может пройти биток? Какая в нём энергия? Я не хочу каждый раз лупасить шары со всей силы; это будет подвергать биток риску случайного скатывания в лузу. Я хочу очень аккуратно и точно рассчитывать силу удара. Для этого мне нужно знать при какой оттяжке кия от битка в пикселях происходит минимальный удар и какой он длины, и при какой оттяжке кия от битка в пикселях происходит максимальный удар и какой он длины. Также мне надо знать какая энергия отбирается у шара при ударе о борт. Теперь осталось всё это выяснить. С минимальными ударами всё прозаично: это меряется в любой момент. С максимальными гораздо, гораздо сложнее. Диагонали стола не хватит даже для среднего удара, поэтому я явно померять длину его пути не могу. Я придумал разложить это на два случая: горизонтальный и вертикальный. Произведя потом соответствующие замеры и приравняв уравнения вместе я бы получил полную длину максимальной траектории шара и коэффициент гашения энергии о борт. Так я и сделал. Я выждал моменты, когда противник получил фол, чтобы аккуратно поставить биток прямо к краешкам бортов, да так, чтобы горизонтальному и вертикальному движению битка ничего не мешало. И дважды ударил со всей силы: один раз горизонтально, другой — вертикально. Снял показатели: при вертикальном ударе биток отскочил от длинных бортов 8 раз и, докатившись на остатке энергии ещё какой-то путь, остановился. При горизонтальном ударе число отскоков составило 5 с хвостиком. Отсюда, зная размеры стола, нетрудно получить уравнения, численно решив которые мы получим и полную длину максимального пути, и коэффициент гашения.Пусть x — максимальная длина пути качения шара без ударов о борты в пикселях, а p x —— в начальный момент времени самого сильного удара шар обладает полной энергией качения на максимальную длину пути x; x — w —— такой энергией и, соответственно, запасом пути обладает шар, прокатившийся по горизонтали от борта до борта на ширину стола w; p · (x — w) —— ударившись о борт, шар потерял энергию и, следовательно, запас пути согласно коэффициенту p; p · (x — w) — w —— прокатившись снова от борта до борта шар ещё раз потерял запас пути, равный ширине стола; p · (p · (x — w) — w) —— шар снова ударился о борт и потерял энергию согласно p; p · (p · (x — w) — w) — w —— опять качение от борта до борта, уже третье; p · (p · (p · (x — w) — w) — w) —— очередной удар о борт и потеря энергии; p · (p · (p · (x — w) — w) — w) — w —— четвёртая потеря энергии и запаса пути при качении от борта до борта; p · (p · (p · (p · (x — w) — w) — w) — w) —— удар о борт; p · (p · (p · (p · (x — w) — w) — w) — w) — 388 —— пятое, последнее качение от борта: шар полностью потерял всю энергию и запас пути и останавился на расстоянии 388 пикселей от края борта. Приравниваем последнее уравнение к нулю. Аналонично получаем уравнение для вертикального случая. Решая эту нелинейную систему из двух уравнений с двумя неизвестными численно, я получил искомые x и p.Цепные правила Разумеется, глубина удара может быть любой, лишь бы энергии хватило. Но из-за накопления погрешностей я решил остановиться на числе 4, то есть максимальное количество задействованых объектов (включая лузы) равно 4. Мой биток может ударить шар А, который ударит шар Б, который упадёт в лузу. И то (!) точность теряется настолько сильно, что я ввёл целый ряд ограничений на подобные удары. Они должны быть без рикошетов о борты, расстояния между шарами и лузой должны быть примерно равны и углы атаки близки к упругим ударам. А вот такая фантастика мне пока не светит: 8a5bc4a40d0494f82dbb117c12832076.jpg Откат битка Все страдания и рассчёты будут напрасны, если даже после феерически шедеврального удара наш биток откатится и упадёт в лузу. Даже через рикошет. Поэтому я сразу выбрасываю из рассмотрения такие удары, после которых боту может светить провал. Для рассчёта отката битка вычисляются сила и направления отката после удара битка о целевой шар. Я думал всё будет просто: вычисляем остаток энергии после упругого/неупругого удара, на какое расстояние и в каком направлении откатится биток, направление — с помощью косинуса относительного угла атаки, и смотрим, не проходит ли траектория через лузы. Но нет, подводный камень ждал меня и тут. После проведения серии тестов я заметил, что фактический откат существенно отличается от моделируемого.И система навигации игры (жётлая) и здравый смысл с рассчётами показывают, что биток откатится по зелёной линии. Однако, после удара он катится по красной: 0f3dcea14f46ab1ddd48b8f2962e9c5d.jpg Вот это западлянка. Оказывается, в игре учитывается ищё и инерция кручения шара и его направление. Пришлось почти четыре вечера писать и тестировать нелинейную функцию рассчёта траектории отката. Такие мелочи бесят, но в конце оказывается, что это вовсе и не мелочи. Более менее точную функцию мне определить удалось только методом проб и ошибок. Но я сделал это! Теперь я могу смело выбрасывать удары, проводящие биток через лузы в результате отката.Функция поиска лучшего удара В результате анализа конфигурации стола я получал коллекцию большого количества возможных ударов. Но как выбрать лучший из них? Надо включить мозги и оценить каждый параметр удара. Подумаем логически, чего бы нам хотелось? Число рикошетов пути от битка до целевого шара желательно бы свести к минимуму: каждый рикошет вносит маленькую, но погрешность. Расстояние от битка до целевого шара должно быть как можно меньше, но не совсем маленьким, чтобы была возможность качественно прицелиться. Как ни странно, это два разных условия. Подумайте над этим. Далее. Расстояние от целевого шара до лузы, в которую он должен упасть, должно быть как можно меньше. Безусловно. И этот путь должен содержать минимум рикошетов, желательно — ноль. Удар желателен средней силы, не слишком сильный (погрешности кручения и физического движка игры) и не слишком слабый (погрешности уже моего бота). Удар битка о целевой шар должен быть максимально «прямым» и упругим: касательные — зло! Отсюда автоматически вытекает минимальная длина отката. Из моего продолжительного подводного плавания и анализа подводных камней вытекли ещё дополнительные условия (не столь важные, но всё же значимые): надо, чтобы целевой шар в угловые лузы летел под углом, максимально близким к 45 градусам, а в лузы длинных бортов — под углом не более 35 градусов. Есть ещё некоторые дополнительные условия, но о них умолчу.b36a07f310a0833509d1fae40eb9ef53.jpg 26f653a9e483befe74c34cf2d596a6fc.jpg b8ee38bd7db82d963a92b88d33662f5c.jpg 1524edfcb9dea095e96bd05cf2f489b2.jpg a17fcc9c04bbd0d722e4d7c5f515705e.jpg 20d8b204d8536abf5fe6cd594967c23d.jpg 4cfa4b671fea86804e4940ac13559d2c.jpg d51bd91e72b824002ad6e154212a5c91.jpg Но по самой сути все эти вещи далеко не идеально сравнимы и друг с другом, и между собой. Например, если угол атаки (упругость удара) ещё как-то сравним с другим углом атаки (скажем, 0 градусов лучше, чем 50), то такая характеристика, как сила удара…? Для разных длин траекторий нужны разные силы, ведь там и расстояние разное, и резка. Если же перевести силу удара в некоторый относительный показатель, то что делать с резкой? Ведь для разных ударов она — своя! А как сравнивать несравнимые категории, такие, например, как число рикошетов (число от 0 до, скажем, 10) и угол влёта шара в лузу длинного борта? Что важнее? Пффф… Передо мной возникла ещё одна существенная проблема. Необходимо было привести все эти характеристики к некоторой консистентности. Скажем, к числовому диапазону от 0 до 1, где 0 — чудовищно плохо или невозможно, а 1 — абсолютно идеально хорошо. В этом и заключался следующий огромный шаг работы; поиск и выбор таких функций, которые бы отражали (и адекватно отражали) все представленные характеристики в коридор [0, 1]. А это огромная работа. Взять число рикошетов. Да, оно может быть от 0 до 10, но в среднем нормой считается 1 или 2 рикошета. 8 — это уже исключительная ситуация, встречающаяся в игре крайне редко. Посему линейно переводить диапазон [0, 10] в отрезок [0, 1] глупо и неоправдано. Пришлось искать «обратные» функции распределения для всех характеристик, подгонять, выкапывать их, высасывать из пальца. В результате всё получилось довольно сносно, но с меня сошло 77 потов.Ну, а следующий шаг — поиск весов для этих характеристик. Вес — это важность характеристики. И тут опять дилема. Что важнее чего? Я сначала попробовал поставить всем характеристикам одинаковый вес, но бот играл отвратительно. Потом начался ещё один этап работы — долгий и нудный. Подгонка и поиск нужных коэффициентов. Этот этап также занял у меня пару недель, но мне повезло. Я уже настолько прочувствовал все ситуации, что адаптивным методом вышел на очень хорошую подборку. До сих пор не нашёл ничего лучше;)669e246ef29e59ebd4f6fcc9749aa53b.jpg Итак, главная определяющся функция была готова! Она, напомню, состоит из суммы произведений весовых коэффициентов на функции подгонки. В результате для каждого удара я получаю число «хорошести» удара от 0 до 1 (нормируя). Ну, а дальше — дело техники: выбираем лучший удар и бьём. Коррекция резки шаров За месяцы тестирования я выявил интересный глюк. То ли из-за неточностей движка, то ли из-за каких-то моих косяков ударные шары, стоящие возле борта, не всегда поддаются общим законам нанесения ударов. А должны. Происходит вот что: в ситуации,

© Habrahabr.ru