Первые шаги в Robocode

в 9:53, , рубрики: game development, java, Robocode, введение, Спортивное программирование, метки: , ,

Я пишу эту статью по просьбам в комментариях к статье “Как я стал чемпионом Robocode” и продолжая начатое в ней дело по привлечению внимания к Robocode русскоговорящих разработчиков. Robocode — это игра для программистов, в которой задача заключается в разработке системы управления танком. Для затравки приведу несколько роликов, чтобы показать о чём вообще пойдёт разговор:

Введение

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

В Robocode есть 4 официальных турнира:

  • Дуэль, 2 робота на поле 800 на 600
  • Схватка (Melee), 10 роботов на поле 1000 на 1000
  • Команды (Teams), 2 команды по 5 роботов на поле 1200 на 1200
  • Двойная дуэль (TwinDuel), 2 команды по 2 робота на поле 800 на 800

В дуэли и схватке есть 4 весовых категории:

  • Нано <= 249 байт исполняемого кода
  • Микро <= 749 байт исполняемого кода
  • Мини <= 1499 байт исполняемого кода
  • Мега (Общая) — без ограничений

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

Подготовка

Для того чтобы начать программировать своего чемпиона, вам как минимум потребуется скачать и установить:

  • Последнюю версию Java (однако на данный момент игра официально поддерживает только Java 6, поэтому роботов надо компилировать для этой версии)
  • Последнюю версию Robocode (опять же, в официальных соревнованиях не поддерживается последняя версия, поэтому надо следить за тем, чтобы робот не использовал API, отсутствующие в поддерживаемой на данный момент версии).

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

  • Последнюю версию любой современной Java IDE (не буду здесь ни кого рекламировать, если есть вопросы — прошу в личку)
  • Систему сборки Ant

Физика игры

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

Управление роботом и взаимодействие с внешним миром

Вообще в игре есть целая иерархия классов, от которых можно отнаследоваться для написания своего робота. Есть “простой” класс robocode.Robot, который за раз может выполнять только одну команду, т.е. либо двигаться, либо поворачивать, либо стрелять (есть, однако, и бонус — такие роботы не получают повреждения от столкновений со стенами). Но на мой взгляд, программирование таких роботов наоборот сложнее и я рекомендую (и в дальнейшем буду исходить из предположения, что вы последовали моей рекомендации) наследоваться от класса robocode.AdvancedRobot, который за один ход может управлять всеми частями сразу. Так же ещё небезинтересен класс robocode.TeamRobot, который позволяет создавать команды, но это отдельная тема для отдельной статьи.

Робот управляется множеством команд, но я приведу только наиболее важные:

  • setAhead — двигаться вперёд на максимальной скорости (и торможением, если в данный момент робот движется назад) на указанное количество пикселей. Если аргумент отрицательный, то робот движется назад
  • setFire и setFireBullet — выстрелить с заданной мощностью.
  • setTurnRightRadians, setTurnLeftRadians, setTurnGunRightRadians, setTurnGunLeftRadians, setTurnRadarRightRadians, setTurnRadarLeftRadians — поворот соответствующих частей, в соответствующем направлении на заданный угол в радианах (есть методы без суфикса Radians, которые на вход принимают угол в градусах). *RightRadians — поворачивает соответсвующую часть по часовой стрелке, *LeftRadians, соответственно, против часовой стрелке.

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

  • StatusEvent — состояние робота (положение, скорость, направления частей и т.п.).
  • DeathEvent — робот только что был уничтожен.
  • BulletHitEvent — пуля робота попала по другому роботу.
  • HitByBulletEvent — по роботу попала пуля противника.
  • HitRobotEvent — произошло столкновение с противником.
  • HitWallEvent — произошло столкновение со стеной.
  • ScannedRobotEvent — последним ходом был обнаружен противник. Данное событие содержит некоторую информацию о противнике (всё что необхоидмо для того чтобы вычислить его положение, его энергию, направление его движения.

Подготовка сборки

Если вы по каким-то причинам решили не использовать ант для сборки, то этот раздел можете пропустить. Вы можете выкачать репозиторий отсюда: https://github.com/alexey-zhidkov/HabrahabrTutorial, либо руками создать у себя следующую структуру папок:

/HabrahabrTutorial
+ - /src
   - build.properties
   - build.xml

Файл build.properties имеет следующие содержание:

bin.dir = bin
builds.dir = builds
robocode.dir = ; путь к домашней директории Robocode, по умолчанию C:Robocode
robocode.jar = ${robocode.dir}\libs\robocode.jar

Файл build.xml имеет следующие содержание:

<?xml version="1.0" encoding="UTF-8"?>

<project name="HabrahabrTutorial" basedir="."
         default="release">

    <property file="build.properties"/>
    <property name="robot.version" value="0.1"/>
    <property name="robot.package" value="ru.jdev.habrahabr"/>
    <property name="robot.path" value="ru/jdev/habrahabr"/>
    <property name="robot.name" value="HabrahabrTutorial"/>

    <path id="src.files">
        <pathelement location="src"/>
    </path>

    <target name="init">
        <mkdir dir="${bin.dir}"/>
    </target>

    <target name="compile" depends="init" description="Compiles source files">

        <javac destdir="${bin.dir}" debug="on" debuglevel="lines,vars,source" optimize="yes" target="1.6">
            <src refid="src.files"/>

            <classpath>
                <pathelement location="${robocode.jar}"/>
            </classpath>
        </javac>

    </target>

    <target name="clean" description="Deletes all previous build artifacts">
        <delete dir="${bin.dir}"/>
    </target>

    <target name="release" depends="clean, compile">
        <copy todir="${bin.dir}">
            <fileset dir="src"/>
        </copy>

        <echo file="${bin.dir}/${robot.path}/${robot.name}.properties">robocode.version=1.7.3
            robot.java.source.included=true
            robot.version=${robot.version}
            robot.author.name=Alexey jdev Zhidkov
            robot.classname=${robot.package}.${robot.name}
            robot.name=${robot.name}
            robot.description=Tutorial robot for habrahabr.ru
        </echo>

        <jar destfile="${builds.dir}${robot.package}.${robot.name}_${robot.version}.jar" compress="true">
            <fileset dir="${bin.dir}"/>
        </jar>

        <copy todir="${robocode.dir}robots">
            <fileset file="${builds.dir}${robot.package}.${robot.name}_${robot.version}.jar"/>
        </copy>

        <delete includeEmptyDirs="true">
            <fileset dir="${bin.dir}" includes="**/*"/>
        </delete>

    </target>

</project>

Если вы всё сделали правильно, то после выполнения команды ant в корне проекта, у вас должен появиться файл HabrahabrTutorialbuildsru.jdev.habrahabr.HabrahabrTutorial_0.1.jar, в котором должен быть файл rujdevhabrahabrHabrahabrTutorial.properties.

Создание робота

Создайте в выбранной вами среде разработки проект в директории из предыдущего раздела, подключите к нему библиотеку ${robocode_home}/libs/robocode.jar. Затем настройте среду так, чтобы либо она сама компилировала код в папку с роботами, либо чтобы она при сборке использовала скрипт из предыдущего раздела. Наконец, создайте новый класс ru.jdev.habrahabr.HabrahabrTutorial со следующим кодом:

package ru.jdev.habrahabr;

import robocode.AdvancedRobot;

public class HabrahabrTutorial extends AdvancedRobot {

    @Override
    public void run() {
        while (true) {
            /**
             * Вызовом этого метода робот сообщает движку, что он закончил вычисления и отдал все команды на текущий ход
             * Этот вызов блокируется до начала следующего кода
             */
            execute();
        }
    }
}

Запустите игру, выберите Battle -> New и убедитесь, что в списке роботов появился ru.jdev.habrahabr.HabrahabrTutorial.

Шаг первый: Выход после смерти, вычисление позиции противника, управление радаром и рисование.

Приводим код к следующему виду (здесь все комментарии приведены прямо в коде. Так же, здесь и далее добавленные строки или методы отмечены комментарием /*+*/, изменённые строки или методы отмечены комментарием /*~*/):

package ru.jdev.habrahabr;

import robocode.AdvancedRobot;
import robocode.DeathEvent;
import robocode.ScannedRobotEvent;
import robocode.util.Utils;

import java.awt.*;
import java.awt.geom.Point2D;

import static java.lang.Math.signum;
import static java.lang.Math.toRadians;

public class HabrahabrTutorial extends AdvancedRobot {

    /*+*/private static final double RADIANS_5 = toRadians(5);

    /*+*/private boolean isAlive = true;

    /*+*/private double enemyX = -1;
    /*+*/private double enemyY = -1;

    @Override
    public void run() {
        /*+*/setTurnRadarRightRadians(Double.POSITIVE_INFINITY); // пока противник не найден бесконечно крутим радар в право
        /*~*/while (isAlive) { // в принципе это не обязательно и можно оставить true, не я предпочитаю избегать бесконечных циклов

            /*+*/if (enemyX > -1) { // если противник обнаружен
                /*+*/final double radarTurn = getRadarTurn();
                /*+*/setTurnRadarRightRadians(radarTurn);
                /*+*/}

            /**
             * Вызовом этого метода робот сообщает движку, что он закончил вычисления и отдал все команды на текущий ход
             * Этот вызов блокируется до начала следующего кода
             */
            execute();
        }
    }

    /*+*/private double getRadarTurn() {
        // роботу жизненно необходимо постоянно видеть противника
        // считаем абсолютный угол до противника:
        final double alphaToEnemy = angleTo(getX(), getY(), enemyX, enemyY);
        // считаем направление, на который надо повернуть радар, чтобы противник остался в фокусе (Utils, это встренный в Robocode класс):
        final double sign = (alphaToEnemy != getRadarHeadingRadians())
                ? signum(Utils.normalRelativeAngle(alphaToEnemy - getRadarHeadingRadians()))
                : 1;

        // добавляем 5 градусов поворта для надёжности и получаем результирующий угол
        return Utils.normalRelativeAngle(alphaToEnemy - getRadarHeadingRadians() + RADIANS_5 * sign);
        // В принципе, прямо здесь можно вызвать setTurnRadarRightRadians, но я противник функций с сайд эффектами и стараюсь
        // минимизировать их количество
    }

    @Override
    /*+*/public void onScannedRobot(ScannedRobotEvent event) {
        /** ScannedRobotEvent не содержит в себе явно положения противника, однако, его легко вычислить, зная направление
         * своего корпуса, беаринг (по сути угол относительный чего-то, в данном случае относительно корпуса) и расстояние до противника
         */

        // абсолютный угол до противника
        final double alphaToEnemy = getHeadingRadians() + event.getBearingRadians();

        // а далее элементарная геометрия
        enemyX = getX() + Math.sin(alphaToEnemy) * event.getDistance();
        enemyY = getY() + Math.cos(alphaToEnemy) * event.getDistance();
    }

    @Override
    /*+*/public void onDeath(DeathEvent event) {
        isAlive = false;
    }

    @Override
    /*+*/public void onPaint(Graphics2D g) {
        // убеждаемся, что вычислили позицию противника верно
        // для того чтобы увидеть что мы ресуем, необходимо во время битвы на правой понели кликнуть по имени робота
        // и в появившемся окне нажать кнопку Paint

        if (enemyX > -1) {
            g.setColor(Color.WHITE);
            g.drawRect((int) (enemyX - getWidth() / 2), (int) (enemyY - getHeight() / 2), (int) getWidth(), (int) getHeight());
        }
    }

    /**
     * В Robocode немного извращённые углы - 0 смотрит на север и далее по часовой стрелке:
     * 90 - восток, 180 - юг, 270 - запад, 360 - север.
     * <p/>
     * Из-за этого приходится писать собственный метод вычисления угла между двумя точками.
     * Вообще говоря, математика никогда не была моим коньком, поэтому, возможно, существует лучшее решение
     */
    /*+*/private static double angleTo(double baseX, double baseY, double x, double y) {
        double theta = Math.asin((y - baseY) / Point2D.distance(x, y, baseX, baseY)) - Math.PI / 2;
        if (x >= baseX && theta < 0) {
            theta = -theta;
        }
        return (theta %= Math.PI * 2) >= 0 ? theta : (theta + Math.PI * 2);
    }

}

Шаг второй: Начинаем движение

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

private double getDistance() {
        // вычесление дистанции движения элементарно
        return 200 - 400 * random();
    }

    private double getBodyTurn() {
        // а вот вычисление угла поворота посложее
        final double alphaToMe = angleTo(enemyX, enemyY, getX(), getY());

        // определяем угловое направление относительно противника (по часовой стрелке, либо против) ...
        final double lateralDirection = signum((getVelocity() != 0 ? getVelocity() : 1) * Math.sin(Utils.normalRelativeAngle(getHeadingRadians() - alphaToMe)));
        // получаем желаемое направление движения
        final double desiredHeading = Utils.normalAbsoluteAngle(alphaToMe + Math.PI / 2 * lateralDirection);
        // нормализуем направление по скорости
        final double normalHeading = getVelocity() >= 0 ? getHeadingRadians() : Utils.normalAbsoluteAngle(getHeadingRadians() + Math.PI);
        // и возвращаем угол поворта
        return Utils.normalRelativeAngle(desiredHeading - normalHeading);
    }

А внутри условия, что противник обнаружен в основном цикле добавим следующие строки:

                setTurnRadarRightRadians(radarTurn);

/*+*/                final double bodyTurn = getBodyTurn();
/*+*/                setTurnRightRadians(bodyTurn);
/*+*/
/*+*/                if (getDistanceRemaining() == 0) {
/*+*/                    final double distance = getDistance();
/*+*/                    setAhead(distance);
/*+*/                }
            }

Шаг третий: Огонь!

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

private double getGunTurn() {
       // вычисления тривиальны: считаем на какой угол надо повернуть пушку, чтобы она смотрела прямо на противника:
       return Utils.normalRelativeAngle(angleTo(getX(), getY(), enemyX, enemyY) - getGunHeadingRadians());
   }

И добавлением трёх строк в основной цикл:

                    setAhead(distance);
                }

/*+*/                final double gunTurn = getGunTurn();
/*+*/                setTurnGunRightRadians(gunTurn);
/*+*/                setFire(2);
            }

Что дальше

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

P.S. Это мой первый туториал, по этому буду рад конструктивной критике

Автор: jdev

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