FightCode: танковые войны на JavaScript

в 19:03, , рубрики: javascript, Robocode, игры для программистов, обучение программированию, Спортивное программирование, Учебный процесс в IT, метки: , ,

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

FightCode: танковые войны на 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js