Фильмы, где огромные армии сходятся друг с другом на поле боя в эпичной битве обычно вызывают в людях бурю эмоций. Сцены сражений из "Звездных войн" с мастерски владеющими световыми мечами джедаями и ордами боевых дроидов — не исключение.


Но иногда бывает интересно посмотреть на сам процесс битвы как бы с высоты птичьего полета и увидеть весь ход развития событий. Для этого можно использовать различные средства виртуальной симуляции. В этом посте приведен пример моделирования битвы между боевыми дроидами Федерации и орденом Джедаев с помощью такой простой дискретной модели как клеточный автомат.





Клеточные автоматы


Представьте, что весь мир — это сетка, разбитая на квадраты, называемые клетками. Каждая из таких клеток может находиться в различных состояниях из заданного множества. На состояние клетки влияют состояния соседних клеток, которые обычно определяются через окрестность Мура порядка 1. Яркий пример такой модели — игра "Жизнь", придуманная математиком Джоном Конвеем в 1970-х. Ее правила предельно просты:


  1. Каждая клетка может быть либо "живой", либо "мертвой".
  2. Мертвая клетка, рядом с которой находится ровно 3 живые, на следующем ходу становится живой.
  3. Если рядом с живой клеткой находится 2 или 3 живые клетки, то на следующем ходу она продолжает жить.
  4. Если рядом с живой клеткой находится меньше 2 или больше 3 живых клеток, то она умирает.

image

Именно благодаря простоте описания правил подобных моделей мы и будем использовать клеточный автомат для моделирования эпичной битвы.


Придумываем свой автомат


Всего в нашем клеточном автомате будет 4 типа клеток — поле (зеленый), дроид (бежевый), джедай (голубой), бластер (красный). Теперь придумаем правила. Нам нужно, чтобы дроны сбивались в кучки и пускали лучи бластеров в джедаев, а джедаи, в свою очередь, атаковали эти густые кучки дроидов. Для простоты моделирования добавим клеткам новую возможность — перемещаться. По сути, перемещение клетки в клеточном автомате можно задать как переход одной из клеток в состояние переходящей, а саму переходящую перевести в нулевое состояние (в нашем случае это поле).


Что потребуется реализовать для создания симулятора битвы:


  1. Дроиды сбиваются в группы
  2. Дроид стреляет в ближайшего джедая в радиусе поражения
  3. Джедай атакует ближайший к нему отряд

Нахождение групп дроидов


Если представить дроидов как вершины графа, а соседних дроидов как связанные вершины, то группы дроидов — это компоненты связности такого графа. Их мы легко сможем получить по завершении каждого шага с помощью поиска в глубину (DFS):


    components = []
    var colors = new Array(field.grid.length)
    colors.fill(0) // 0 is white, 1 is gray, 2 is black
    for (var i = 0; i < field.grid.length; i++)
        if (field.grid[i].color == 1)
            if (colors[i] == 0) {
                var center = dfs(colors, i)
                components.push({ x: Math.round(center.x / center.k), y: Math.round(center.y / center.k) })
                droidsAmount += center.k
            }

Сама функция определения компоненты связности рекурсивна:


function dfs(colors, v) {
    colors[v] = 1
    var x = field.grid[v].x, y = field.grid[v].y, k = 1
    for (var i = 0; i < field.grid[v].n.length; i++)
        if (field.grid[field.grid[v].n[i]].color == 1 && colors[field.grid[v].n[i]] == 0) {
            var newPos = dfs(colors, field.grid[v].n[i])
            x += newPos.x
            y += newPos.y
            k += newPos.k
        }
    colors[v] = 2
    return { x: x, y: y, k: k }
}

Свойство k соответствует числу дроидов в группе и требуется для вычисления центра группы (среднего арифметического по x- и y-координатам каждого дроида). Каждый дроид обращается к своим соседям, координаты которых передаются в первый вызов рекурсии, который, в свою очередь, вернет суммы всех координат дроидов, а также их количество.


Движение к цели


У всех участников сражения есть цели — клетки, на которые они стремятся попасть: джедай — в центр ближайшей группы дроидов, дроиды — к своим союзникам, а снаряд — в клетку с координатами, где находился джедай в тот момент, когда был сделан выстрел.


function moveToTarget(n, me, priority, ban) {
    var newX = me.x, newY = me.y
    var deltaX = me.tx - me.sx, deltaY = me.ty - me.sy
    var scope = deltaY / deltaX

    if ((deltaX == 0) || (deltaY == 0)) {
        newX = me.x + (deltaX == 0 ? 0 : deltaX / Math.abs(deltaX))
        newY = me.y + (deltaY == 0 ? 0 : deltaY / Math.abs(deltaY))
    } else {
        if (Math.abs(scope) >= 1) {
            newY = me.y + deltaY / Math.abs(deltaY)
            newX = me.sx + Math.round((newY - me.sy) / (scope))
        } else {
            newX = me.x + deltaX / Math.abs(deltaX)
            newY = me.sy + Math.round((newX - me.sx) * (scope))
        }
    }

    if (!set(n, { x: newX, y: newY }).length) {
        me.color = 0
        return false
    }

    var newCell = set(n, { x: newX, y: newY })[0]
    for (var key in ban)
        if (newCell[key] == ban[key])
            return false

    return { x: newX, y: newY, instead: { color: 0 }, priority: priority }
}

Рассмотрим, как это работает. Расстояние между целью и начальным положением объекта по оси X обозначим за deltaX, по оси Y — за deltaY. Понятно, что если изначальная x-координата объекта имеет большее значение, чем координата цели, то deltaX будет отрицательно. То же самое касается deltaY.


Обозначим отношение deltaY к deltaX как scope(наклон). Если наклон больше единицы, то на каждом шагу объект сдвигается на единицу по оси Y, и иногда — по оси X. Тогда newY = me.y + deltaY / Math.abs(deltaY), где деление на абсолютное значение позволяет получить единицу с правильным знаком. Так как движение можно определить как
x = y / scope, то newX = me.sx + Math.round((newY — me.sy) / (scope)). Аналогично поступаем для случаев с scope < 1.


Бывают случаи, когда объект и цель находятся в одинаковом столбце или одинаковой строке. Тогда может возникнуть деление на ноль. Для таких случаев делается проверка (deltaX == 0) || (deltaY == 0). Если это правда, то мы просто проверяем с помощью тернарного оператора разность координат, и, если ее значение ненулевое, прибавляем единицу с правильным знаком.


В параметре ban прописаны свойства, которые недопустимы для клетки, на которую объект переходит. Например, дроид не должен наталкиваться на других роботов. Следовательно, для дроида ban = { color: 1 } (1 — индекс цвета дроида). Если клетка, на которую объект хочет перейти, будет обладать хотя бы одним свойством, имеющим равное значение с одноименным свойством ban, перемещения не будет.


Если объект попытается попасть на несуществующую клетку (переместиться за границу поля), то он исчезает с карты.


Но если все прошло успешно, то в качестве новых координат функция вернет newX и newY.


Алгоритм дроида


Дроид делает выстрелы в сторону ближайшего джедая. Радиус видимости дроида задается коэффициентом k2. Дроид выбирает ближайшего джедая, выстрел в которого не задел бы дроидов, находящихся в радиусе длины 5.


Коэффициент k2 — дистанция обнаружения джедая дроидом.



Алгоритм джедая


Для вычисления оптимального направления удара джедая зададим в соответствие каждой окружающей его клетке вес, который будем вычислять следующим образом:


Джедая окружает 8 клеток (в случае, если джедай находится в углу или у границы поля — 3 или 5 клеток). Вес вычисляется отдельно для каждой из этих клеток и зависит от состояний остальных клеток вокруг джедая. Представим вес как полином вида 5a1 + 4b1 + 4b2 + 3c1 + 3c2 + 2d1 + 2d2 + 1e, где каждая переменная равняется единице если соответствующая клетка занята дроидом, в противном случае она равна нулю. Чем дальше клетка находится от обрабатываемой, тем меньше коэффициент при соответствующем ей одночлене.


Пример вычисления веса клетки можно увидеть на рисунке:



Веса клеток при конкретной ситуации:



В приведенном случае джедай имеет три оптимальных варианта передвижения. Один из них будет выбран случайным образом.


Осталось помочь джедаю обнаружить скопления дроидов. Если представить дроидов как вершины графа, которые связаны в случае соседства дроидов, то после каждого шага с помощью поиска в глубину(DFS) мы сможем найти все компоненты связности этого графа, записать координаты их центров и количество дроидов в компоненте в массив. На следующем шаге джедаи ищут ближайшие к себе отряды и немедленно устремляются к ним.


Коэффициент k1 — максимально число снарядов, от которых джедай может увернуться. По умолчанию k1 = 4.



Полет снаряда


Бесконечно снаряд лететь не может, так что после 32 шагов он исчезает:


function processBullet(n, me) {
    me.age++
    if (me.age > 32)
        me.color = 0
}

Движение его предельно просто — обычный вызов функции направления к цели:


function moveBullet(n, me) {
    return moveToTarget(n, me, 1, {})
}

Неприятные фишки


Что уж говорить — программа, формально выполняя строго заданный алгоритм, иногда может удивлять нас странными результатами.


Наиболее заметным является "эффект мертвых джедаев". Джедаи стоят мертво и кучно.


   


Почему? Ответ прост: один из джедаев достиг цели и пытается выбраться из толпы, но остальные джедаи мешают ему это сделать. Они, в свою очередь, пытаются протиснуться к цели, но так как она занята и находящийся там джедай не может ее покинуть, возникает данная ситуация.


Моделируем сражение


Поиграться с симуляцией можно прямо в браузере. Выберите тип клеток и начните рисовать им по экрану, пока не получите желаемое расположение. Нажмите на "Start" и наслаждайтесь битвой. Если битва закончилась или просто наскучила вам, можно начать на "Stop", после чего сгенерируются графики. На первом будет показано количество дроидов в определенные моменты времени, на втором — такая же статистика для джедаев.



» Поиграться с симуляцией можно на тут


Для создания графиков использовалась библиотека Chart.js. О чем могут говорить графики?


  • Количество дроидов до определенного момента оставалось одинаковым. На том шаге, когда число дроидов начало снижаться, произошло первое столкновение джедаев с дроидами.

  • Самые частые случаи гибели джедаев. Был ли боец поражен снарядом до того, как вклинился в ряды противника, или же пал в самый разгар битвы, окруженный численно превосходящим противником? Все это можно узнать, сравнив на двух графиках момент его смерти.

  • Общая картина боя. Шел ли он плавно, или были «передышки» и «кульминационные моменты»? Посмотрим, есть ли резкие скачки в графе и получим ответ на вопрос!

Заключение


Исходный код с комментариями доступен на GitHub. Для запуска скачайте репозиторий и откройте index.html в своем браузере.


Получилось, на мой взгляд, занятное зрелище. Графики позволяют собирать информацию о ходе боев при различных начальных конфигурациях. Интересно, как бы выглядели графики настоящих исторических сражений? Разумеется, в этом посте рассматривается вымышленная вселенная, но настоящие исторические битвы проходили примерно по тем же правилам: солдаты сбивались в группы, атаковали друг друга и, разумеется, несли потери.


Впрочем, одной симуляцией ограничиваться не стоит — ведь есть еще более интересная задача, а именно нахождение наилучшего алгоритма действий для каждого участника сражения. И тут исследователю поможет теория игр. Но это уже совсем другая история...

Поделиться с друзьями
-->

Комментарии (31)


  1. vektory79
    28.09.2016 12:26
    +5

    Прикольная модель получилась.


    У меня после каждого запуска оставалось по два джедая. В результате попробовал сразу только двух ставить — и они выживали без проблем.


    Аутентичненько...


    1. fireSparrow
      28.09.2016 13:54
      +1

      «Всегда двое их — учитель и ученик»
      Хотя это было сказано про ситхов.


    1. redQueen_66
      28.09.2016 14:16

      У меня видать темные джедаи. В конце, такое ощущение, что один другого убивает.


  1. vdeneko
    28.09.2016 12:28
    +3

    поигрался с симуляцией
    почему-то странная ситуация бывает — куча дроидов и один дункан маклауд джедай побеждает прям не то что пачку, а целый ряд прям дроидов
    это нормальное поведение?
    нарисовал большой круг/дугу, в нем поменьше дроидов, внутри еще меньше круг/дугу джедаев
    image


    1. mtivkov
      28.09.2016 13:24
      +3

      Джедаям главное — добежать до противника.


      Один в поле воин:




    1. fireSparrow
      28.09.2016 13:59
      +2

      По отдельности джедаи вёрткие. Джедай практически всегда в движении, а дроиды тупые — с упреждением стрелять не умеют, потому почти всегда мажут.

      А толпа джедаев — хорошая мешень. Даже если по одному джедаю промазал — то соседнего зацепил.


    1. Volvox
      28.09.2016 14:20

      Тут все просто: когда джедай в самой гуще дроидов — в него отказываются стрелять, боясь задеть своих товарищей в радисуе 5. Если бы железяки были поумнее, они бы в таких ситуациях стреляли в джедая, задевая своих ближайших товарищей, но спасая остальную армию.



  1. gnkoshelev
    28.09.2016 13:06
    +2

    Симуляция в моём браузере не завелась:
    image

    var chosen = new Array(m.grid.length)
    chosen.fill(false)
    


    Метод Array.prototype.fill появился только в ES6, чтобы было кроссбраузерно используют polyfill.


    1. Volvox
      28.09.2016 14:28

      Спасибо большое) Пофиксил


  1. belurd
    28.09.2016 14:04

    Слишком «шустрые» джедаи получаются, что двигаются со скоростью снарядов, поэтому и с легкостью могут убежать от опасности и «добить» оставшихся.

    @Vovlox не думали внести учет скорости полета выстрела?


    1. Volvox
      28.09.2016 15:24

      Сейчас работаю над совершенствованием среды. Пользователи смогут сами выбирать скорости и алгоритмы.


  1. Legushka
    28.09.2016 14:16
    +2

    научился строить дроидов так что бы они мочили джедаев превосходящих по количеству


  1. lexaficus
    28.09.2016 14:16

    Убило всех джедаев((
    image


  1. Akomis
    28.09.2016 14:16

    Зарисовал все поле, вышло 4500 дроидов. 35 джедаев все выкосили, 7 выжило.
    Джедаи умирают только если совсем некуда отпрыгнуть. Соответственно самый надежный способ их убить — поставить рядом с другим джедаем. Идеально — построить колонной. Поодиночке мрут только если дроиды ведут очень плотный огонь, что редко бывает из-за достаточно малого радиуса видимости дроида. У меня одиночные джедаи только случайно умирали, когда сами врывались в центр уже летящего залпа.

    Я нашел только один надежный способ убить одного джедая — поставить его в центр куба 9х9. Из за специфики AI джедаи действует каждый раз одинаково и стабильно нарывается на один и тот же выстрел бластера. Но если снаружи поставить — у дроидов нет шансов.


  1. Aspecter
    28.09.2016 14:16

    Ровный строй из полтысячи дроидов спокойно перерабатывает полсотни джедаев, а из 16 рыцарей, окруженных 150 дроидами, выжил только один.


  1. daddyksen
    28.09.2016 14:17

    Жутко имбовые джедаи :)


  1. KinsleR
    28.09.2016 14:17

    Построил дроидов в линию прямую, джедаев клином (примерно столько же по количеству), джедай до линии добежал один, и истребил ее всю…
    что-то пошло не так ...


    1. Kenya
      28.09.2016 16:34

      вообще все логично, джедаи ведь очень сильны в ближнем бою, а дроиды — наоборот. Джедаю сложно будет уклониться от десятков бластерных выстрелов сразу (в фильме отбивали по одному-два выстрела), поэтому процент добежавших будет небольшой. Но зато стоит им добежать и применить световой меч в во всей красе — перевес будет явно не в пользу дроидов


      1. mustdy
        29.09.2016 18:26

        Это как конница и лучники. Пока между ними есть расстояние, конница будет умирать ударными темпами. Но стоит ворваться в строй — и лучникам хана


  1. belurd
    28.09.2016 14:22

    На дэйли завтра: «Еще я потратил час на изучение „повадок“ дроидов и джедаев...»



  1. Kenya
    28.09.2016 14:44
    +3

    Стоит еще добавить симуляцию Джа-Джа Бинкса для достоверности)


  1. Sirion
    28.09.2016 16:37
    +4

    Очень интересная симуляция. Одна маленькая проблема: она не является клеточным автоматом.


    1. TimeCoder
      30.09.2016 09:15

      А почему?


      1. Sirion
        30.09.2016 15:41
        +2

        Клеточный автомат — это набор ячеек и функция, которая из состояния каждой ячейки и состояний некоторого набора соседних ячеек выводит новое состояние ячейки. А что мы видим здесь? Рекурсивный алгоритм поиска центра группы. Выстрелы из бластеров, которые каким-то образом запоминают, куда они летели. Джедаи, бегущие к ближайшей группе дроидов независимо от того, насколько она далеко. То есть симуляция на клетки на самом деле не разбивается, это просто алгоритм на двумерном массиве.


        1. TimeCoder
          30.09.2016 21:08

          Гм… Но ведь сложность алгоритма не является критерием? В том смысле, что в «Жизни» на вход алгоритма подается состояние 8 соседей, на выходе получаем новое состояние для данной клетки, причем сам алгоритм принятия решения — пара строк кода. Если там будет тысячи строк сложнейшего кода, и на вход подается состояние о сотнях окружающих клеток — это ведь все еще клеточный автомат? Мне просто понять хочется. Симуляция перестанет быть автоматом после:
          1. Добавления памяти алгоритму, т.е. когда появятся какие-либо данные кроме самого поля.
          2. а вот второй кейс сформулировать не могу, но он как-то связан с вашим сообщением (про разбиние и пр.).

          Кстати, покуда вам интересна «Жизнь», у меня была статья на эту тему: Путешествия во времени и программирование-2.


          1. Sirion
            30.09.2016 21:24
            +1

            Не думайте понятиями алгоритма и кода, думайте понятием функции и множества. Есть множество клеток и множество состояний. Каждая клетка в каждый момент времени находится в одном из допустимых состояний. Есть функция, которая каждой клетке ставит в соответствие множество её соседей. И есть функция, которая состоянию клетки и множеству состояний её соседей ставит в соответствие новое состояние. Следующее состояние клеточного автомата получается из предыдущего применением вот этой последней функции ко всем клеткам.

            Чисто теоретически, эту симуляцию можно переформулировать как клеточный автомат. В общем-то, всё можно переформулировать как клеточный автомат, потому что можно сочинить КА, эквивалентный машине Тьюринга. Однако в статье не сформулировано правил соответствующего клеточного автомата. В ней просто что-то внешне похожее. Хочу отметить, что я не ругаю статью. Симуляция действительно забавная, я поставил плюс. Но как человек, писавший по КА диплом, не мог не позанудствовать в комментах.


    1. xenohunter
      30.09.2016 13:26

      Потому что присутствует случайность выбора?


      1. Sirion
        30.09.2016 15:45
        +1

        Нет, недетерминированные КА — тема вполне благодатная. Ответил выше.


        1. xenohunter
          01.10.2016 18:33

          Спасибо!