All Downloads are FREE. Search and download functionalities are using the official Maven repository.

samplesentry.BorderGuard Maven / Gradle / Ivy

/*
 * Copyright (c) 2001-2023 Mathew A. Nelson and Robocode contributors
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * https://robocode.sourceforge.io/license/epl-v10.html
 */
package samplesentry;


import robocode.*;
import robocode.util.Utils;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Point2D;
import java.util.*;


/**
 * BorderGuard - is a sample robot that demonstrates how the BorderSentry interface can be used for
 * creating a robot that guards the border area of the battle field, and which is efficient against
 * robots trying to hide at corners and robots sneaking around near the borders.
 * 

* This robot is somewhat advanced due to:
* 1) it uses linear targeting for predicting how to hit robots that moves in straight lines.
* 2) it will only fire at a robot, if it figures out that its bullets will do damage to that * particular robot, e.g. when the robots predicted future position will be within the sentry border * attack range.
* 3) it has an efficient scanner that keeps the scan angle as little as possible to get as new * scanned data for enemy robots as possible.
* 4) it picks a target robot to fire at, which is the nearest robot our robot will be able to * damage (by predicting its future position using linear targeting).
* 5) it only moves along the borders of the battle field and constantly changes its direction, so * it is not an easy target, and it will try to get as close to its target robot as possible.
*

* Lots of improvements can be made to this robot by copying it - making it even stronger. The * intention with of this sample robot is to serve as a more advanced example of how a AdvancedRobot * can be made, and how it can be structured as most sample robots are far simpler. *

* * Credits goes to the hard working Robocoders at the RoboWiki. :-) *

* * This robot makes use of the Oldest Scanned melee scanner from the RoboWiki:
* https://robowiki.net/wiki/Melee_Radar *

* * In addition, it makes use of the Exact Non-iterative Solution for Linear Targeting from the * RoboWiki:
* https://robowiki.net/wiki/Linear_Targeting * * @author Flemming N. Larsen * * @version 1.0 * * @since 1.9.0.0 */ public class BorderGuard extends AdvancedRobot implements BorderSentry { // Constants final double FIREPOWER = 3; // Max. power => violent as this robot can afford it! final double HALF_ROBOT_SIZE = 18; // Robot size is 36x36 units, so the half size is 18 units // Map containing data for all scanned robots. // The key to the map is a robot name and the value is an object containing robot data. final Map enemyMap; // Scanning direction, where the radar turns to the right with positive values, and turns // to the left with negative values. double scanDir = 1; // Oldest scanned robot. Can be null. RobotData oldestScanned; // Target robot for the gun. Can be null meaning that there is currently no target robot. RobotData target; // Last time when the robot shifted its direction long lastDirectionShift; // Current direction, where 1 means ahead (forward) and -1 means back int direction = 1; /** * Constructs this robot. */ public BorderGuard() { // We initialize a specialized HashMap that uses a linked list for the access order. // This means that the last accessed robot entry is listed first, when we iterate over its // values. This robot always sweep the radar towards the oldest scanned robot. enemyMap = new LinkedHashMap(5, 2, true); } /** * Main method that is called by the game when the robot engage in the next round of a battle. */ @Override public void run() { // Do initialization stuff here before the loop initialize(); // Loop forever. If the robot does not take action, the game will disable our robot! while (true) { // Handle a single turn... // Handle the radar that scans enemy robots handleRadar(); // Handle the gun by turning it and fire at our target handleGun(); // Move the robot around on the battlefield moveRobot(); // Scan for other robots. Note that this method will execute all pending commands for // the next turn. Hence, scan() ends the turn for our robot. scan(); } } /** * This method is called by the game when your robot sees another robot, i.e. when the robot's * radar scan "hits" another robot. * * @param scannedRobotEvent * is a ScannedRobotEvent event. */ @Override public void onScannedRobot(ScannedRobotEvent scannedRobotEvent) { // Check that the scanned robot is not a sentry robot if (!scannedRobotEvent.isSentryRobot()) { // The scanned robot is not a sentry robot... // Update the enemy map updateEnemyMap(scannedRobotEvent); // Update the scan direction updateScanDirection(scannedRobotEvent); // Update enemy target positions updateEnemyTargetPositions(); } } /** * This method is called by the game when another robot dies. * * @param robotDeathEvent * is the RobotDeathEvent that occurs, when another robot dies, which contains data * for the robot that died. */ @Override public void onRobotDeath(RobotDeathEvent robotDeathEvent) { // Gets the name of the robot that died final String deadRobotName = robotDeathEvent.getName(); // Remove the robot data for the robot that died from the enemy map enemyMap.remove(deadRobotName); // Remove the data entry for the oldest scanned robot, if we have such an entry if (oldestScanned != null && oldestScanned.name.equals(deadRobotName)) { oldestScanned = null; } if (target != null && target.name.equals(deadRobotName)) { target = null; } } /** * This method is called by the every time the robot is painted. In order to see the painting, * make sure to enable the Paint button on the robot console for this robot. * * @param g * is the {@link Graphics2D} object, which is the graphics context used for painting * various shapes like rectangles, circles, lines etc. on top of the battlefield for * debugging graphics. */ @Override public void onPaint(Graphics2D g) { // Set the line width to 2 pixels g.setStroke(new BasicStroke(2f)); // Prepare colors for painting the scanned coordinate and target coordinate Color color1 = new Color(0x00, 0xFF, 0x00, 0x40); // Green with 25% alpha blending Color color2 = new Color(0xFF, 0xFF, 0x00, 0x40); // Yellow with 25% alhpa blending // Paint a two circles for each robot in the enemy map. One circle where the robot was // scanned the last time, and another circle where our robot must point the gun in order to // hit it (target coordinate). In addition, a line is drawn between these circles. for (RobotData robot : enemyMap.values()) { // Paint the two circles and a line fillCircle(g, robot.scannedX, robot.scannedY, color1); // scanned coordinate fillCircle(g, robot.targetX, robot.targetY, color2); // target coordinate g.setColor(color1); g.drawLine((int) robot.scannedX, (int) robot.scannedY, (int) robot.targetX, (int) robot.targetY); } // Paint a two circles for the target robot. One circle where the robot was // scanned the last time, and another circle where our robot must point the gun in order to // hit it (target coordinate). In addition, a line is drawn between these circles. if (target != null) { // Prepare colors for painting the scanned coordinate and target coordinate color1 = new Color(0xFF, 0x7F, 0x00, 0x40); // Orange with 25% alpha blending color2 = new Color(0xFF, 0x00, 0x00, 0x80); // Red with 50% alpha blending // Paint the two circles and a line fillCircle(g, target.scannedX, target.scannedY, color1); // scanned coordinate fillCircle(g, target.targetX, target.targetY, color2); // target coordinate g.setColor(color1); g.drawLine((int) target.scannedX, (int) target.scannedY, (int) target.targetX, (int) target.targetY); } } /** * Initializes this robot before a new round in a battle. */ private void initialize() { // Let the robot body, gun, and radar turn independently of each other setAdjustRadarForGunTurn(true); setAdjustGunForRobotTurn(true); // Set robot colors setBodyColor(new Color(0x5C, 0x33, 0x17)); // Chocolate Brown setGunColor(new Color(0x45, 0x8B, 0x74)); // Aqua Marine setRadarColor(new Color(0xD2, 0x69, 0x1E)); // Orange Chocolate setBulletColor(new Color(0xFF, 0xD3, 0x9B)); // Burly wood setScanColor(new Color(0xCA, 0xFF, 0x70)); // Olive Green } /** * This method handles the radar that scans for enemy robots. */ private void handleRadar() { // Set the radar to turn infinitely to the right if the scan direction is positive; // otherwise the radar is moved to the left, if the scan direction is negative. // Notice that onScannedRobot(ScannedRobotEvent) is responsible for determining the scan // direction. setTurnRadarRightRadians(scanDir * Double.POSITIVE_INFINITY); } /** * Method that handles the gun be turning it and fire at a target. */ private void handleGun() { // Update our target robot to fire at updateTarget(); // Update the gun direction updateGunDirection(); // Fires the gun, when it is ready fireGunWhenReady(); } /** * Method that moves our robot around the battlefield. */ private void moveRobot() { // The movement strategy is to move as close to our target robot as possible. // Our robot should move along the borders all the time, vertically or horizontally. // When we get close to our target, or have no where to go, our robot should shift its // direction from side to side so it does not stand still at any time. // If the robot stands still, it will be an easy target for enemy robots. int newDirection = direction; // Get closer to our target if we have a target robot if (target != null) { // Calculate the range from the walls/borders, our robot should keep within int borderRange = getSentryBorderSize() - 20; // The horizontal and vertical flags are used for determining, if our robot should // move horizontal or vertical. boolean horizontal = false; boolean vertical = false; // Initialize the new heading of the robot to the current heading of the robot double newHeading = getHeadingRadians(); // Check if our robot is at the upper or lower border and hence should move horizontally if (getY() < borderRange || getY() > getBattleFieldHeight() - borderRange) { horizontal = true; } // Check if our robot is at the left or right border and hence should move vertically if (getX() < borderRange || getX() > getBattleFieldWidth() - borderRange) { vertical = true; } // If we are in one of the corners of the battlefield, we could move both horizontally // or vertically. // In this situation, we need to choose one of the two directions. if (horizontal && vertical) { // If the horizontal distance to our target is lesser than the vertical distance, // then we choose to move vertically, and hence we clear the horizontal flag. if (Math.abs(target.targetX - getX()) <= Math.abs(target.targetY - getY())) { horizontal = false; // Do not move horizontally => move vertically } } // Adjust the heading of our robot with 90 degrees, if it must move horizontally. // Otherwise the calculated heading is towards moving vertically. if (horizontal) { newHeading -= Math.PI / 2; } // Set the robot to turn left the amount of radians we have just calculated setTurnLeftRadians(Utils.normalRelativeAngle(newHeading)); // Check if our robot has finished turning, i.e. has less than 1 degrees left to turn if (Math.abs(getTurnRemaining()) < 1 || Math.abs(getVelocity()) < 0.01) { // If we should move horizontally, the set the robot to move ahead with the // horizontal distance to the target robot. Otherwise, use the vertical distance. double delta; // delta is the delta distance to move if (horizontal) { delta = target.targetX - getX(); } else { delta = target.targetY - getY(); } setAhead(delta); // Set the new direction of our robot to 1 (meaning move forward) if the delta // distance is positive; otherwise it is set to -1 (meaning move backward). newDirection = delta > 0 ? 1 : -1; // Check if more than 10 turns have past since we changed the direction the last // time if (getTime() - lastDirectionShift > 10) { // If so, set the new direction to be the reverse direction if the velocity < 1 if (Math.abs(getVelocity()) < 1) { newDirection = direction * -1; } // Check if the direction really changed if (newDirection != direction) { // If the new direction != current direction, then set the current direction // to be the new direction and save the current time so we know when we // changed the direction the last time. direction = newDirection; lastDirectionShift = getTime(); } } } } // Set ahead 100 units forward or backward depending on the direction setAhead(100 * direction); } /** * Method the updates the enemy map based on new scan data for a scanned robot. * * @param scannedRobotEvent * is a ScannedRobotEvent event containing data about a scanned robot. */ private void updateEnemyMap(ScannedRobotEvent scannedRobotEvent) { // Gets the name of the scanned robot final String scannedRobotName = scannedRobotEvent.getName(); // Get robot data for the scanned robot, if we have an entry in the enemy map RobotData scannedRobot = enemyMap.get(scannedRobotName); // Check if data entry exists for the scanned robot if (scannedRobot == null) { // No data entry exists => Create a new data entry for the scanned robot scannedRobot = new RobotData(scannedRobotEvent); // Put the new data entry into the enemy map enemyMap.put(scannedRobotName, scannedRobot); } else { // Data entry exists => Update the current entry with new scanned data scannedRobot.update(scannedRobotEvent); } } /** * Method that updates the direction of the radar based on new scan data for a scanned robot. * * @param scannedRobotEvent * is a ScannedRobotEvent event containing data about a scanned robot. */ private void updateScanDirection(ScannedRobotEvent scannedRobotEvent) { // Gets the name of the scanned robot final String scannedRobotName = scannedRobotEvent.getName(); // Change the scanning direction if and only if we have no record for the oldest scanned // robot or the scanned robot IS the oldest scanned robot (based on the name) AND the enemy // map contains scanned data entries for ALL robots (the size of the enemy map is equal to // the number of opponent robots found by calling the getOthers() method). if ((oldestScanned == null || scannedRobotName.equals(oldestScanned.name)) && enemyMap.size() == getOthers()) { // Get the oldest scanned robot data from our LinkedHashMap, where the first value // contains the oldest accessed entry, which is the robot we need to get. RobotData oldestScannedRobot = enemyMap.values().iterator().next(); // Get the recent scanned position (x,y) of the oldest scanned robot double x = oldestScannedRobot.scannedX; double y = oldestScannedRobot.scannedY; // Get the heading of our robot double ourHeading = getRadarHeadingRadians(); // Calculate the bearing to the oldest scanned robot. // The bearing is the delta angle between the heading of our robot and the other robot, // which can be a positive or negative angle. double bearing = bearingTo(ourHeading, x, y); // Update the scan direction based on the bearing. // If the bearing is positive, the radar will be moved to the right. // If the bearing is negative, the radar will be moved to the left. scanDir = bearing; } } /** * Updates the target positions for all enemies. The target position is the position our robot * must fire at in order to hit the target robot. This robot uses Linear Targeting (Exact * Non-iterative Solution) as described on the RoboWiki here: * https://robowiki.net/wiki/Linear_Targeting */ private void updateEnemyTargetPositions() { // Go thru all robots in the enemy map for (RobotData enemy : enemyMap.values()) { // Variables prefixed with e- refer to enemy and b- refer to bullet double bV = Rules.getBulletSpeed(FIREPOWER); double eX = enemy.scannedX; double eY = enemy.scannedY; double eV = enemy.scannedVelocity; double eH = enemy.scannedHeading; // These constants make calculating the quadratic coefficients below easier double A = (eX - getX()) / bV; double B = (eY - getY()) / bV; double C = eV / bV * Math.sin(eH); double D = eV / bV * Math.cos(eH); // Quadratic coefficients: a*(1/t)^2 + b*(1/t) + c = 0 double a = A * A + B * B; double b = 2 * (A * C + B * D); double c = (C * C + D * D - 1); // If the discriminant of the quadratic formula is >= 0, we have a solution meaning that // at some time, t, the bullet will hit the enemy robot if we fire at it now. double discrim = b * b - 4 * a * c; if (discrim >= 0) { // Reciprocal of quadratic formula. Calculate the two possible solution for the // time, t double t1 = 2 * a / (-b - Math.sqrt(discrim)); double t2 = 2 * a / (-b + Math.sqrt(discrim)); // Choose the minimum positive time or select the one closest to 0, if the time is // negative double t = Math.min(t1, t2) >= 0 ? Math.min(t1, t2) : Math.max(t1, t2); // Calculate the target position (x,y) for the enemy. That is the point that our gun // should point at in order to hit the enemy at the time, t. double targetX = eX + eV * t * Math.sin(eH); double targetY = eY + eV * t * Math.cos(eH); // Assume enemy stops at walls. Hence, we limit that target position at the walls. double minX = HALF_ROBOT_SIZE; double minY = HALF_ROBOT_SIZE; double maxX = getBattleFieldWidth() - HALF_ROBOT_SIZE; double maxY = getBattleFieldHeight() - HALF_ROBOT_SIZE; enemy.targetX = limit(targetX, minX, maxX); enemy.targetY = limit(targetY, minY, maxY); } } } /** * Updates which enemy robot from the enemy map that should be our current target. */ private void updateTarget() { // Set target to null, meaning that we have no target robot yet target = null; // Create a list over possible target robots that is a copy of robot data from the enemy map List targets = new ArrayList(enemyMap.values()); // Run thru all the possible target robots and remove those that are outside the attack // range for this border sentry robot as our robot cannot do harm to robots outside its // range. Iterator it = targets.iterator(); while (it.hasNext()) { RobotData robot = it.next(); if (isOutsideAttackRange(robot.targetX, robot.targetY)) { it.remove(); } } // Set the target robot to be the one among all possible target robots that is closest to // our robot. double minDist = Double.POSITIVE_INFINITY; for (RobotData robot : targets) { double dist = distanceTo(robot.targetX, robot.targetY); if (dist < minDist) { minDist = dist; target = robot; } } // If we still haven't got a target robot, then take the first one from our list of target // robots if the list is not empty. if (target == null && targets.size() > 0) { target = targets.get(0); } } /** * Method that updates the gun direction to point at the current target. */ private void updateGunDirection() { // Only update the gun direction, if we have a current target if (target != null) { // Calculate the bearing between the gun and the target, which can be positive or // negative double targetBearing = bearingTo(getGunHeadingRadians(), target.targetX, target.targetY); // Set the gun to turn right the amount of radians defined by the bearing to the target setTurnGunRightRadians(targetBearing); // positive => turn right, negative => turn left } } /** * Method that fires a bullet when the gun is ready to fire. */ private void fireGunWhenReady() { // We only fire the fun, when we have a target robot if (target != null) { // Only fire when the angle of the gun is pointing at our (virtual) target robot // Calculate the distance between between our robot and the target robot double dist = distanceTo(target.targetX, target.targetY); // Angle that "covers" the the target robot from its center to its edge double angle = Math.atan(HALF_ROBOT_SIZE / dist); // Check if the remaining angle (turn) to move the gun is less than our calculated cover // angle if (Math.abs(getGunTurnRemaining()) < angle) { // If so, our gun should be pointing at our target so we can hit it => fire!! setFire(FIREPOWER); } } } /** * Method that checks if a coordinate (x,y) is outside the Border Sentry's attack range. * * @param x * is the x coordinate. * @param y * is the y coordinate. * @return true if the coordinate is outside the attack range; false otherwise. */ private boolean isOutsideAttackRange(double x, double y) { double minBorderX = getSentryBorderSize(); double minBorderY = getSentryBorderSize(); double maxBorderX = getBattleFieldWidth() - getSentryBorderSize(); double maxBorderY = getBattleFieldHeight() - getSentryBorderSize(); return (x > minBorderX) && (y > minBorderY) && (x < maxBorderX) && (y < maxBorderY); } /** * Method that returns a value that is guaranteed to be within a value range defined by a * minimum and maximum value based on an input value.
* If the input value is lesser than the minimum value, the returned value will be set to the * minimum value.
* If the input value is greater than the maximum value, the returned value will be set to the * maximum value.
* Otherwise the returned value will be equal to the input value. * * @param value * is the input value to limit. * @param min * is the allowed minimum value. * @param max * is the allowed maximum value. * @return the limited input value that is guaranteed to be within the specified minimum and * maximum range. */ private double limit(double value, double min, double max) { return Math.min(max, Math.max(min, value)); } /** * Methods that returns the distance to a coordinate (x,y) from our robot. * * @param x * is the x coordinate. * @param y * is the y coordinate. * @return the distance to the coordinate (x,y). */ private double distanceTo(double x, double y) { return Math.hypot(x - getX(), y - getY()); } /** * Method that returns the angle to a coordinate (x,y) from our robot. * * @param x * is the x coordinate. * @param y * is the y coordinate. * @return the angle to the coordinate (x,y). */ private double angleTo(double x, double y) { return Math.atan2(x - getX(), y - getY()); } /** * Method that returns the bearing to a coordinate (x,y) from the position and heading of our * robot. The bearing is the delta angle between the heading of our robot and the angle of the * specified coordinate. * * @param x * is the x coordinate. * @param y * is the y coordinate. * @return the angle to the coordinate (x,y). */ private double bearingTo(double heading, double x, double y) { return Utils.normalRelativeAngle(angleTo(x, y) - heading); } /** * Method that paints a filled circle at the specified coordinate (x,y) and given color. The * circle will have a radius of 20 pixels (meaning that the diameter will be 40 pixels). * * @param gfx * is the graphics context to draw within. * @param x * is the x coordinate for the center of the circle. * @param y * is the y coordinate for the center of the circle. * @param color * is the color of the filled circle. */ private void fillCircle(Graphics2D gfx, double x, double y, Color color) { // Set the pen color gfx.setColor(color); // Paint a filled circle (oval) that has a radius of 20 pixels with a center at the input // coordinates. gfx.fillOval((int) x - 20, (int) y - 20, 40, 40); } /** * This class is used for storing data about a robot that has been scanned.
* The data is mainly a snapshot of specific scanned data like the scanned position (x,y), * velocity and heading, put also the calculated predicted target position of the robot when our * robot needs to fire at the scanned robot.
* Note that this class calculates the position (x,y) of the scanned robot as our robot moves, * and hence data like the angle and distance to the scanned robot will change over time. by * using the position, it is easy to calculate a new angle and distance to the robot. */ class RobotData { final String name; // name of the scanned robot double scannedX; // x coordinate of the scanned robot based on the last update double scannedY; // y coordinate of the scanned robot based on the last update double scannedVelocity; // velocity of the scanned robot from the last update double scannedHeading; // heading of the scanned robot from the last update double targetX; // predicated x coordinate to aim our gun at, when firing at the robot double targetY; // predicated y coordinate to aim our gun at, when firing at the robot /** * Creates a new robot data entry based on new scan data for a scanned robot. * * @param event * is a ScannedRobotEvent event containing data about a scanned robot. */ RobotData(ScannedRobotEvent event) { // Store the name of the scanned robot name = event.getName(); // Updates all scanned facts like position, velocity, and heading update(event); // Initialize the coordinates (x,y) to fire at to the updated scanned position targetX = scannedX; targetY = scannedY; } /** * Updates the scanned data based on new scan data for a scanned robot. * * @param event * is a ScannedRobotEvent event containing data about a scanned robot. */ void update(ScannedRobotEvent event) { // Get the position of the scanned robot based on the ScannedRobotEvent Point2D.Double pos = getPosition(event); // Store the scanned position (x,y) scannedX = pos.x; scannedY = pos.y; // Store the scanned velocity and heading scannedVelocity = event.getVelocity(); scannedHeading = event.getHeadingRadians(); } /** * Returns the position of the scanned robot based on new scan data for a scanned robot. * * @param event * is a ScannedRobotEvent event containing data about a scanned robot. * @return the position (x,y) of the scanned robot. */ Point2D.Double getPosition(ScannedRobotEvent event) { // Gets the distance to the scanned robot double distance = event.getDistance(); // Calculate the angle to the scanned robot (our robot heading + bearing to scanned // robot) double angle = getHeadingRadians() + event.getBearingRadians(); // Calculate the coordinates (x,y) of the scanned robot double x = getX() + Math.sin(angle) * distance; double y = getY() + Math.cos(angle) * distance; // Return the position as a point (x,y) return new Point2D.Double(x, y); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy