full stack javascript developer
0,0
рейтинг
27 марта 2013 в 23:03

Разработка → FightCode: танковые войны на JavaScript tutorial

FightCode — это онлайн-игра для программистов, построенная по образу и подобию классической Robocode. Для программирования танков используется JavaScript, все сражения происходят прямо в браузере, а редактор кода на сайте имеет встроенную «песочницу», которая позволяет в реальном времени видеть эффект от изменений кода. В отличие от многих других подобных игр, создатели неплохо поработали над дизайном — игровое поле и весь сайт в целом выглядят привлекательно и ярко.



Всё это делает FightCode одним из лучших вариантов для новичков в подобных играх или для обучения программированию. Проект довольно молодой, и несмотря на то, что на сайте зарегистрировано почти 9000 игроков, пробиться в первую сотню рейтинга можно без особых усилий. Очень удобно организована система боёв со случайными соперниками — из всех доступных роботов автоматически выбираются те, чей рейтинг близок к вашему. Очки считаются по системе Эло — победа над более сильным противником даёт гораздо больше очков, чем над слабым.

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

Для начала пара слов об игровом мире и правилах игры. Роботы воюют на плоском прямоугольном поле. Игровой мир двухмерен, так что перелётов или недолётов снаряда здесь не бывает. Танки играют один на один, но каждый игрок может создать «клон» своего танка, который имеет меньше здоровья и погибает если убит его родитель.

Танки могут двигаться вперёд и назад, поворачиваться, вращать башней и стрелять. Отдельного радара, как в Robocode, нет. Основной игровой цикл задаётся в обработчике события "onIdle". Когда в поле зрения башни попадает танк, происходит событие "onScannedRobot", при столкновении со стеной — "onWallCollision", и так далее (полное описание API есть на сайте).

Код робота по умолчанию выглядит так:

var Robot = function(robot) {

};

Robot.prototype.onIdle = function(ev) {
    var robot = ev.robot;       // получаем текущий экземпляр робота
    robot.ahead(100);           // проходим сто единиц вперёд
    robot.rotateCannon(360);    // осматриваемся
    robot.back(100);            // проходим сто единиц назад
    robot.rotateCannon(360);    // снова осматриваемся
};

// Стреляем во всё, что шевелится
Robot.prototype.onScannedRobot = function(ev) {
    var robot = ev.robot;
    robot.fire();
};

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

Robot.prototype.onScannedRobot = function(ev) {
    var r = ev.robot;     // сокращаем robot до r, так как это имя нам придётся набирать буквально в каждой строке
    r.fire();
    r.rotateCannon(10);   // 10 градусов вправо
    r.rotateCannon(-20);  // 20 влево
};

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

Robot.prototype.onIdle = function(ev) {
    var r = ev.robot;
    r.clone();       // этот метод создаёт клона
    r.rotateCannon(360);
...

// как только нас засекли - включаем маскировку
Robot.prototype.onHitByBullet = function(ev) {
    var r = ev.robot;
    r.disappear();
...

Следующий шаг — научим робота не стрелять по своим. По умолчанию он палит во всё, что попадается ему на глаза, включая собственного клона или родителя. Чтобы отучить его от этого, нужно перед стрельбой проверять, кто перед нами. Делается это с помощью свойств id и parentId:

Robot.prototype.onScannedRobot = function(ev) {
  var r = ev.robot;

  // Если мы видим собственный клон или своего родителя, то выходим, ничего не делая
  if (ev.scannedRobot.parentId == r.id || ev.scannedRobot.id == r.parentId) {
    return;
  }
  r.fire();
...

Теперь сделаем нашего робота чуть более подвижным и научим его стрелять в движении. Самый простой способ — во время стрельбы двигаться по кругу. Роботы начального уровня не умеют стрелять с упреждением, так что наворачивая круги, большую часть снарядов противника мы пропустим мимо себя независимо от того, с какой стороны в нас стреляют. Для этого будем после каждого выстрела слегка поворачивать корпус робота и проезжать немного вперёд:

Robot.prototype.onScannedRobot = function(ev) {
  var r = ev.robot;
  if (ev.scannedRobot.parentId == r.id || ev.scannedRobot.id == r.parentId) {
    return;
  };
  r.fire();
  r.turn(10);               // вместо башни теперь поворачиваем весь танк
  r.rotateCannon(-20);
  r.ahead(15);              // проезжаем чуть-чуть вперёд
};

И ещё один нюанс — событие onScannedRobot происходит, как только «луч радара» пересекается с краем танка противника. Так как большую часть времени мы крутим башню вправо, то снаряды летят в левый край цели и часто пролетают мимо. Чтобы бить точнее, будем перед выстрелом доворачивать башню на пару градусов вправо:

Robot.prototype.onScannedRobot = function(ev) {
  var r = ev.robot;
  if (ev.scannedRobot.parentId == r.id || ev.scannedRobot.id == r.parentId) {
    return;
  }
  r.rotateCannon(2);     // пара градусов вправо
  r.fire();
  r.turn(8);             // 10 минус 2 градуса вправо
  r.rotateCannon(-20)
  r.ahead(15);
};

Вместо того чтобы бесполезно ездить вперёд-назад на одном месте, заставим танк активно двигаться по полю. Это поможет избежать ситуаций, когда мы в одном углу, противник в другом, снаряды летят через всё поле и практически никогда не попадают. Будем двигаться всё время вперёд, и не на 100, а на 150 единиц. Кроме того, будем немного менять курс, чтобы быть менее предсказуемыми:

Robot.prototype.onIdle = function(ev) {
    var r = ev.robot;
    r.clone();
    r.rotateCannon(360);
    r.ahead(150);     
    r.turn(30);
    r.ahead(150);
};

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

Robot.prototype.onWallCollision = function(ev) {
    var r = ev.robot;
    r.back(50);
};

Robot.prototype.onRobotCollision = function(ev) {
    var r = ev.robot;
    r.back(30);
};

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

Полный код нашего робота
//FightCode can only understand your robot
//if its class is called Robot
var Robot = function(r) {
};

Robot.prototype.onIdle = function(ev) {
    var r = ev.robot;
    r.clone();
    r.rotateCannon(360);
    r.ahead(150);
    r.turn(30);
    r.ahead(150);
};

Robot.prototype.onScannedRobot = function(ev) {
    var r = ev.robot;
    if (ev.scannedRobot.parentId == r.id || ev.scannedRobot.id == r.parentId) {
        return;
    };
    r.rotateCannon(2);
    r.fire();
    r.turn(8);
    r.rotateCannon(-20);
    r.ahead(15);
};

Robot.prototype.onHitByBullet = function(ev) {
    var r = ev.robot;
    r.disappear();
};

Robot.prototype.onWallCollision = function(ev) {
    var r = ev.robot;
    r.back(50);
};

Robot.prototype.onRobotCollision = function(ev) {
    var r = ev.robot;
    r.back(30);
};



Илья Сименко @ilya42
карма
526,7
рейтинг 0,0
full stack javascript developer
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

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

  • +5
    Я пару дней назад на целый день залип на этой войнушке.
    • +4
      А я, походу, на ночь сегодня залипну.
  • +4
    Один в один же последний Russian AI cup
    • +2
      Не, там API был много лучше!
      • 0
        Второе апи, первое их апи было не далеко от этого
  • +3
    Проверил. Робот автора победил.
  • 0
    Интересно, изнутри робота есть ограничения на доступный код? Можно ли получить код робота противника и проанализировать его (например, для расчёта упреждения)?
    По событию onScannedRobot(e) можно получить ссылку на обнаруженного робота e.robot, и у него уже получить код:
    Robot.prototype.onScannedRobot = function(ev) {
    var r = ev.robot;
    var enemyCode = ev.scannedRobot.onIdle;
    r.rotateCannon( analyzeEnemyMovement(enemyCode) );
    r.fire();
    //…
    };

    Понятно, что функция analyzeEnemyMovement получится непростой, но всё же…
    • 0
      Вообще говоря вам доступны положение и углы противника и его пушки
      • –2
        но недоступно, что он планирует сделать через секунду…
        • +4
          Если завтра война, вам не стоит командовать танком. Без обид
          • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      поправочка:
      var enemyCode = ev.scannedRobot.prototype.onIdle;
      • 0
        scannedRobot имеет, скорее всего, другой тип, его прототип равен null. Проверено через консоль. Вообще это была бы огромная дыра в безопасности, даже если просто граббить чужой код, анализировать и брать лучшее себе.
        • 0
          (удалено)
          • +1
            Да, всё там есть, я что-то прогадал этот момент… Код соперника в элементе script type=«robot/script» id=«robot-%userid%-code»

            Зря удалили комментарий)
            • 0
              Я понял, что не смотря на 4 или 5 часов возни с апи этих танчиков я не догодался стянуть код самых крутых из них :D
            • 0
              Спасибо. Я не догадался просто на страничке поискать. Сейчас сижу, смотрю код соперников и радуюсь — мои 40 строк вообще без какого-либо интеллекта, расчётов, тригонометрии легко уделывают вдесятеро более сложных роботов )
              • 0
                Более того, просто изменяя айди реплея в строке fightcodegame.com/robots/replay/717933/ мы можем перебирать ввобще весь код, т.к. реплей — это бой с теми же входными параметрами)
                • +1
                  Весь спортивный интерес теряется, когда код чужих роботов становится доступным
                  • 0
                    Ну терять его или не терять — выбирать Вам. Я, к примеру, смотрю чужой код разве когда несколько боев подряд не могу победить.
                  • +1
                    Как я понял, проект воспитывает юных робототехников, даёт первый опыт программирования роботов в реальном времени. В случае открытого кода обучение проходит более эффективно, молодежь учится друг у друга новым алгоритмам. Очень полезный проект имхо.
                    • НЛО прилетело и опубликовало эту надпись здесь
                  • 0
                    Теперь прийдется обфускацию кода делать.
        • –1
          Не могу найти никак эту консоль. Где она?
          • +1
            в браузере Developers tools/firebug и т.п.
            • 0
              Тоже не нашел, точнее нашел на вызов
              log("qwe");
              никак не реагирует.
              • 0
                ev.robot.log("qwe");
                
  • +2
    еще советую на Scalatron посмотреть, заодно и Scala поучить.
  • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    Почему ev.robot.parentId у клонов null?
    • 0
      Это у главного танка равен null, а у клонов id родителя. Так и проверяется кто клон, а кто нет.
      UPD: Упс, вам уже ниже написали.
  • 0
    А есть ли что-нибудь подобное, но для шашек или других интеллектуальных игр?
    • 0
      Зачем Вы спросили? Теперь или Вы напишете или я :)
    • 0
      Есть наш проект: sourceforge.net/projects/softwarebattle/
      Он еще довольно сырой, но использовать во всяких компьютерных школах вполне возможно (чем мы и занимаемся :))
  • +1
    Как показывает практика, неплохая стратегия — пускать робота и клона врассыпную. Так не настолько больно от заграждающего огня.

    И ещё там можно создавать глобальные переменные, в коих можно хранить последние известные данные про роботов соперника, чтобы целиться более адекватно.
    • 0
      не могу придумать как отличить клона от оригинала.
      • 0
        У оригинала parentId нулевой
        • 0
          у моих почему-то всегда parentId null
          • 0
            Разобрался, у клона не работает robot.log
      • +2
        if(r.parentId) {
        //клон
        }
        else {
        //оригинал
        }
  • +1
    помнится давно, когда microsoft начали продвигать .net, была такая программерска игра — terrarium
    image
    habrahabr.ru/post/74523/ — упоминается на хабре.
    • 0
      Эх. Клевая была.

      А сейчас нет никаких аналогов?
      • 0
        именно аналогов — эмуляторов экосистем, не встречал.
  • +3
    Почувствуй себя водителем марсохода Curiosity ;)
  • 0
    Я не нашел, как стрелять с упреждением по цели? Как получить курс цели?
    • 0
      Можно получить только направление, в котором танк повернут носом, а так же направление, в котором повернуто дуло. Нельзя получить никаких данных об его динамике, разве вести руками статистику какую-либо.
      • 0
        Направление своего танка и направление дула? Данных о противнике — никаких получить нельзя?
        • 0
          Читайте документацию по апи, функцию «onRobotScanned», там всё прекрасно описано.
          • 0
            Еще можно вопрос? Не нашел ответа.
            Если я обнаружил врага в событии onScannedRobot, я могу изменить поведение робота в событии onIdle, те запретить ему мотать башней?
            • 0
              Да, там есть функция robot.stop()
              • 0
                Поясните, пожалуйста, еще про функцию notify.
                • 0
                  Вот на эту функцию не смотрел, и сечас не имею возможности, т.к. на работе…
  • 0
    Известна скорость полета снаряда? Можно оценить скорость противника?
  • 0
    Эта статья перевод документации от игрока или автор имеет отношение к проекту?
    Я очень интересуюсь подобными проектами в жанре «игры для программистов». Хотелось бы узнать про закулисье данной игры. Как долго она создавалась, какие цели, какие способы монетизации и перспективы?

    Я еще сам не успел поиграть, как рабочий день закончится обязательно присоединюсь!
    • 0
      Я не имею отношения к проекту, и это не перевод документации. Я просто описал свой опыт написания простенького бота.
  • 0
    Ни кто для себя не переводил на русский весь API?

    // Moves ahead if direction equals to 1, backwards otherwise
    move: functon(amount, direction)

    Что за параметры? никак не пойму((
    • 0
      amount — расстояние, которое необходимо пройти (любое натуральное число);
      direction — если ровна 1 робот идёт вперёд. Если указанно другое значение — назад;

      ПРИМЕРЫ:
      // 10 Шагов вперёд
      r.move(10, 1);
      
      // 20 Шагов назад
      r.move(20, 0);
      
      // 30 Шагов назад
      r.move(30, -1);
      
    • 0
      Даже гуглотранслейтом понятно:

      // Перемещение вперед, если направлении равна 1, в противном случае в обратном направлении
      ход: функция (количество, направление)
      • 0
        Я всегда буду нажимать «Обновить комментарии»
        Я всегда буду нажимать «Обновить комментарии»
        Я всегда буду нажимать «Обновить комментарии»
  • 0
    У меня одного они с нормальной скоростью ездят только когда я на другую вкладку переключаюсь? Chrome, Q8800.
    • 0
      Оказалось, что нужно было отключить (то ли включить, не помню) вертикальную синхронизацию канваса в chrome://flags
  • 0
    У меня периодически когда нормально, когда «полчаса джамп — полчаса летаешь у гейта, да ещё и скаутишь через тимспик — а потом в клонилке видишь что тебя убили полтора часа назад» (простите, за сленг Eve Online, но, думаю, понятно). Заметил, что при тормозах нехило жрет CPU модуль GPU O_o (Cromium, A4, Ubuntu 12/10)
    А вообще неверный код может завесить вкладку.
  • 0
    Кто-нибудь разобрался с системАМИ координат (прежде всего угловых)? Или, хотя бы, как на Robot брикпоинты повесить в Фоксе или Хроме?
    • 0
      Извините за вопрос, но что не так с системами координат?
      Интересно, что именно хотели сделать и не получилось?
      • 0
        Например, эмпирческим путем (в конструкторе) определил, что robot.cannonRelativeAngle == 0 означает, что пушка направо свернута, а robot.cannonAbsoluteAngle (как же автодопа от JetBrains не хватает ещё :) может не совпадать с robot.angle даже с учетом их 90 градусов сдвинутости.

        В общем хотел реализовать «инновационный алгоритм» исходя из школьной геометрии с учетом position в robot и scannedRobot (учитывал неоднозначность arccos в форме acos(dx/dd), а не acos(dx, dy), чтобы считал углу большие пи (к градусам, естествеено, приводил), а всё крутится непонятно куда. Ещё прикол, кстати с углами — врезаюсь в стену четко под 90 градусов, а ev.bearing дает (через ev.robot.log) — 0.

        Хотя, сейчас подумал, какие-то неразберихи могла внести неправильно мною понятая модель «прерываний», хотя robot.stop совал куда угодно, лишь бы задумке не мешало.

        целом, как-то плохо с доками, а ведь хватило бы одной картинки по идее.
        • 0
          robot.cannonAbsoluteAngle может не совпадать с robot.angle даже с учетом их 90 градусов сдвинутости
          Может проблема в том что Вы вызываете robot.log(robot.cannonAbsoluteAngle) перед тем как танк успевает поворачивать пушку?

          а всё крутится непонятно куда
          Как Вы уже заметили, есть замечательная функция robot.log() — очень помогает понять как вообще работают остальные функции. Используйте глобальные переменные и функции чтобы удобнее управлять этим процессом.

          Ещё прикол, кстати с углами — врезаюсь в стену четко под 90 градусов, а ev.bearing дает (через ev.robot.log) — 0.
          Попробуйте столкутся перед другим углом, например 45 и увидите что ev.bearing = -45. Это связано с тем, что ev.bearing возвращает угол стены относительно роботом, а не наоборот.

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

          var Robot = function () {},
          bot = Robot.prototype,
          
          botStopped, // узнаем если можно продолжить движение вперёд
          gunRotated; // чтобы убедится что пушка будет перевернута лишь один раз
          
          bot.onIdle = function (ev) {
             if (!botStopped) {
                var r = ev.robot;
          
                // Ждём пока робот не будет перевернут на 90 градусов
                if (r.angle == 90) {
                   if (!gunRotated) {
                      // Не забываем что пушка уже перевёрнута
                      gunRotated = true;
                      
                      // Не забываем, что робот перевёрнут уже на 90 градусов
                      // и чтобы пушка также была перевёрнута на 90 градусов
                      // вращаем её на 90 градусов влево
                      r.turnGunLeft(90);
          
                      // Не смотря на то, что мы задали новое значение для угла пушки,
                      // здесь показывает старое значение, так как пушка не успела перевернутся
                      r.log(r.cannonAbsoluteAngle); // -> 180
                   }
                   
                   // Пока есть возможность, идём вперёд маленькими шагами
                   r.ahead(1);
                } else {
                   // Будем вращать танк в наименьшую сторону
                   r.turn((r.angle > 180) ? 1 : -1);
                }
             }
          };
          
          bot.onWallCollision = function (ev) {
             // Врезались в стену, нужно остановится
             botStopped = true;
          
             // Так как пушка уже перевёрнута, показывает правильное значение
             ev.robot.log(ev.robot.cannonAbsoluteAngle); // -> 90
          };
          
          • 0
            Перепроверил сейчас — всё же robot.angle и robot.cannonAbsoluteAngle разные углы, когда пушка направлена вперед. Отличаются на robot.cannonRelativeAngle (что логично), который для пушки направленной вперед равен 90, а 0 градусов — развернутая на левый борт (что не логично). А для танка 0 градусов значит направление вверх, градусы считаются против часовой стрелки.

            Спасибо, что помогли разобраться с тем, что команды танку асинхронные, что возврат происходит сразу, а не когда действие завершится.
  • 0
    Как вычислить, двигается ли противник и с какой скоростью?
    • 0
      Используйте глобальное переменное чтобы сохранить текущее местоположение противника и каждый раз, в событии onScannedRobot() обновите её значение. Если прежнее значение и текущее отличаются, значит противник движется.

      Насчёт скорости, она постоянна (думаю, важнее узнать не скорость, а направление). Если Вы хотите рассчитать скорость противника, нужно использовать ещё одно глобальное переменное для того чтобы сохранить время когда получили последнее местоположение противника. Теперь, зная расстояние и время, Вы легко можете рассчитать скорость движения.

      ПРИМЕР. Проверяем, двигается ли наш противник.
      var Robot = function (robot) {},
      bot = Robot.prototype,
      enemy = {
         position : ''
      };
      
      bot.onIdle = function (ev) {
         // Вращаем пушку дабы найти наш противник
         ev.robot.rotateCannon(1);
      };
      
      bot.onScannedRobot = function (ev) {
         // Получаем текущее местоположение противника
         var pos = ev.scannedRobot.position,
      
         // Правильнее, сохранить данные в виде массива
         // чтобы было удобнее использовать их в дальнейшим
         // Но для простого примера, сохраняем местоположение в виде строки
         currentPosition = pos.x + '/' + pos.y;
      
         // Проверяем если противник изменил своё местоположение
         if (currentPosition != enemy.position) {
            // Проверяем если задано предыдущее местоположение
            if (enemy.position) {
               // Кажется, противник двигается
               // Что приказываете делать?
            }
      
            // Сохраняем новое местоположение противника
            enemy.position = currentPosition;
         }
      };
      

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.