package nl.tudelft.bw4t.server.eis; import java.awt.geom.Point2D; import java.awt.geom.Point2D.Double; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.Stack; import org.apache.log4j.Logger; import org.omg.CORBA.Environment; import eis.eis2java.annotation.AsAction; import eis.eis2java.annotation.AsPercept; import eis.eis2java.exception.TranslationException; import eis.eis2java.translation.Filter; import eis.eis2java.translation.Filter.Type; import eis.eis2java.translation.Translator; import eis.exceptions.ActException; import eis.exceptions.AgentException; import eis.exceptions.PerceiveException; import eis.iilang.Action; import eis.iilang.Parameter; import nl.tudelft.bw4t.scenariogui.BotConfig; import nl.tudelft.bw4t.server.eis.translators.BlockWithColorTranslator; import nl.tudelft.bw4t.server.eis.translators.BoundedMovableObjectTranslator; import nl.tudelft.bw4t.server.eis.translators.ColorTranslator; import nl.tudelft.bw4t.server.eis.translators.EPartnerTranslator; import nl.tudelft.bw4t.server.eis.translators.IdAndBooleanTranslator; import nl.tudelft.bw4t.server.eis.translators.ObjectInformationTranslator; import nl.tudelft.bw4t.server.eis.translators.PointTranslator; import nl.tudelft.bw4t.server.eis.translators.ZoneTranslator; import nl.tudelft.bw4t.server.environment.BW4TEnvironment; import nl.tudelft.bw4t.server.model.BoundedMoveableInterface; import nl.tudelft.bw4t.server.model.BoundedMoveableObject; import nl.tudelft.bw4t.server.model.blocks.Block; import nl.tudelft.bw4t.server.model.epartners.EPartner; import nl.tudelft.bw4t.server.model.robots.AbstractRobot; import nl.tudelft.bw4t.server.model.robots.NavigatingRobot; import nl.tudelft.bw4t.server.model.robots.handicap.AbstractRobotDecorator; import nl.tudelft.bw4t.server.model.robots.handicap.IRobot; import nl.tudelft.bw4t.server.model.zone.BlocksRoom; import nl.tudelft.bw4t.server.model.zone.Corridor; import nl.tudelft.bw4t.server.model.zone.DropZone; import nl.tudelft.bw4t.server.model.zone.Room; import nl.tudelft.bw4t.server.model.zone.Zone; import nl.tudelft.bw4t.server.util.RoomLocator; import nl.tudelft.bw4t.server.util.ZoneLocator; import repast.simphony.context.Context; import repast.simphony.space.continuous.NdPoint; import repast.simphony.util.collections.IndexedIterable; /** * EIS entity for a {@link AbstractRobot}. This puts EIS functionality on top of * the repast rpbot. */ public class RobotEntity implements EntityInterface { static { // Register our translators. Translator translator = Translator.getInstance(); translator.registerJava2ParameterTranslator(new BlockWithColorTranslator()); translator.registerJava2ParameterTranslator(new BoundedMovableObjectTranslator()); translator.registerJava2ParameterTranslator(new ZoneTranslator()); translator.registerJava2ParameterTranslator(new PointTranslator()); translator.registerJava2ParameterTranslator(new ObjectInformationTranslator()); translator.registerJava2ParameterTranslator(new ColorTranslator()); translator.registerJava2ParameterTranslator(new EPartnerTranslator()); translator.registerJava2ParameterTranslator(new IdAndBooleanTranslator()); } /** * The log4j logger, logs to the console. */ private static final Logger LOGGER = Logger.getLogger(RobotEntity.class); private final IRobot ourRobot; private final Context<Object> context; /** * Here we store data that needs to be locked for a perception cycle. See * {@link #initializePerceptionCycle()}. */ private Point2D ourRobotLocation; private Point2D spawnLocation; private Room ourRobotRoom; /** * each item in messages is a list with two items: the sender and the * messagetext. */ private List<ArrayList<String>> messages = new LinkedList<>(); /** * Creates a new {@link RobotEntity} that can be launched by an EIS * compatible {@link Environment}. * * @param robot * The {@link AbstractRobot} that this entity can put up for * controlling in EIS. */ public RobotEntity(IRobot robot) { this.ourRobot = robot; this.context = ourRobot.getContext(); } /** * used for logging and testing. Bit hacky, our robot should not be exposed. * * @return our repast robot object */ public IRobot getRobotObject() { return ourRobot; } /** * Connect robot to repast (to be called when an agent is connected to this * entity) */ @Override public void connect() { spawnLocation = new Point2D.Double(ourRobot.getLocation().getX(), ourRobot.getLocation().getY()); ourRobot.connect(); } /** * Disconnects the robot from repast. */ @Override public void disconnect() { if (ourRobot.isConnected()) { // reset before disconnect: reset moves bot to init position. reset(); ourRobot.disconnect(); ourRobot.removeFromContext(); } } /** * This function should be called before perception cycle is started, so * that we can lock the relevant data from the environment. */ @Override public void initializePerceptionCycle() { ourRobotLocation = new Point2D.Double(ourRobot.getLocation().getX(), ourRobot.getLocation().getY()); ourRobotRoom = RoomLocator.getRoomAt(ourRobotLocation.getX(), ourRobotLocation.getY()); } /** * Percepts for the sizes of all the robots. * * @return size of the robot */ @AsPercept(name = "robotSize", multiplePercepts = true, filter = Filter.Type.ON_CHANGE) public List<ObjectInformation> getSizes() { IndexedIterable<Object> allRobots = context.getObjects(IRobot.class); List<ObjectInformation> sizes = new ArrayList<>(allRobots.size()); for (Object obj : allRobots) { IRobot robot = (IRobot) obj; sizes.add(new ObjectInformation(robot.getSize(), robot.getSize(), robot.getId())); } return sizes; } /** * Percepts for the location of rooms (incl. dropzone), robots, epartners, * and the blocks. Send on change * * @return postitions */ @AsPercept(name = "position", multiplePercepts = true, filter = Filter.Type.ON_CHANGE) public List<ObjectInformation> getLocations() { List<ObjectInformation> objects = new LinkedList<>(); // Add the dropzone DropZone dropZone = (DropZone) context.getObjects(DropZone.class).get(0); objects.add(new ObjectInformation(dropZone)); // Add rooms IndexedIterable<Object> allRooms = context.getObjects(BlocksRoom.class); for (Object object : allRooms) { Room r = (Room) object; objects.add(new ObjectInformation(r)); } // Add blocks for (Block block : getVisible(Block.class)) { objects.add(new ObjectInformation(block)); } // Add EPartners for (EPartner ep : getVisible(EPartner.class)) { objects.add(new ObjectInformation(ep)); } // Add Robots for (IRobot ep : getVisible(IRobot.class)) { objects.add(new ObjectInformation(ep.getSuperParent())); } // #2830 add robots own position objects.add(new ObjectInformation(ourRobotLocation.getX(), ourRobotLocation.getY(), ourRobot.getId())); return objects; } /** * Percept for navpoints the robot is at. Send on change. If robot is in a * {@link Zone}, that zone name is returned. If not, the nearest * {@link Corridor} name is returned. * * @return a list of blockID * @throws PerceiveException */ @AsPercept(name = "at", multiplePercepts = false, filter = Filter.Type.ON_CHANGE) public String getAt() throws PerceiveException { Zone navpt = ZoneLocator.getNearestZone(ourRobot.getLocation()); if (navpt == null) { throw new PerceiveException( "perceiving 'at' percept failed, because map has no suitable navpoint for position " + ourRobotLocation); } return navpt.getName(); } /** * Percept for blocks the robot is at Send on change * * @return a list of blockID of blocks within reach. */ @AsPercept(name = "atBlock", multiplePercepts = true, filter = Filter.Type.ON_CHANGE_NEG) public List<Long> getAtBlock() { List<Long> blocksInReach = new LinkedList<>(); for (Object object : context.getObjects(Block.class)) { Block b = (Block) object; if (ourRobot.canPickUp(b)) { blocksInReach.add(b.getId()); } } return blocksInReach; } /** * Percept for the room that the player is in, null if not in a room. Send * on change * * @return room name */ @AsPercept(name = "in", multiplePercepts = false, filter = Filter.Type.ON_CHANGE_NEG) public String getRoom() { if (ourRobotRoom == null) { return null; } return ourRobotRoom.getName(); } /** * Percept for the location of this robot Send on change * * @return location */ @AsPercept(name = "location", multiplePercepts = false, filter = Filter.Type.ON_CHANGE) public Point2D getLocation() { return new Point2D.Double(ourRobotLocation.getX(), ourRobotLocation.getY()); } /** * Percept for the places in the world. Send at the beginning * * @return rooms */ @AsPercept(name = "place", multiplePercepts = true, filter = Filter.Type.ONCE) public List<String> getRooms() { IndexedIterable<Object> objects = context.getObjects(Zone.class); List<String> places = new ArrayList<>(objects.size()); for (Object o : objects) { Zone zone = (Zone) o; places.add(zone.getName()); } return places; } /** * how much the speed of the robot is multiplied by. Default is 0.5, which * means the bot normally runs at 50% of the maximum speed. * * @return id */ @AsPercept(name = "speed", filter = Filter.Type.ON_CHANGE) public double getSpeed() { return ourRobot.getSpeedMod(); } /** * Percept of the id of the robot Send at the beginning * * @return id */ @AsPercept(name = "robot", filter = Filter.Type.ONCE) public long getRobot() { return ourRobot.getId(); } /** * The names of other players Send at the beginning. We are assuming that * each agent controls one entity. * * @return the names of the other players */ @AsPercept(name = "player", multiplePercepts = true, filter = Filter.Type.ON_CHANGE_NEG) public List<String> getPlayers() { BW4TEnvironment env = BW4TEnvironment.getInstance(); List<String> agents = env.getAgents(); List<String> result = new LinkedList<>(); for (String agt : agents) { try { Set<String> entities = env.getAssociatedEntities(agt); if (!entities.contains(ourRobot.getName())) { result.addAll(entities); } } catch (AgentException e) { LOGGER.error("Ignoring an Agent's percept problem", e); } } return result; } /** * The names of other players Send at the beginning * * @return the names of the other players */ @AsPercept(name = "ownName", multiplePercepts = false, filter = Filter.Type.ONCE) public String getOwnName() { return ourRobot.getName(); } /** * Percept for the colors of all blocks that are visible. Send always. They * are visible only when inside a room. * * @return color */ @AsPercept(name = "color", multiplePercepts = true, filter = Filter.Type.ALWAYS) public List<BlockColor> getColor() { Set<Block> blocks = getVisible(Block.class); boolean isColorBlind = isColorBlind(); List<BlockColor> colors = new ArrayList<>(blocks.size()); for (Block block : blocks) { colors.add(new BlockColor(block, isColorBlind)); } return colors; } /** * Percept if the robot is holding something. Send if it becomes true, and * send negated if it becomes false again. * * @return holding block */ @AsPercept(name = "holding", multiplePercepts = true, filter = Filter.Type.ON_CHANGE_NEG) public List<Long> getHolding() { Stack<Block> holding = ourRobot.getHolding(); List<Long> holds = new ArrayList<>(holding.size()); for (Block b : holding) { holds.add(b.getId()); } return holds; } /** * Percept if the robot is holding something. Sent when the blocks that the * robot holds changes. * * @return holding block */ @AsPercept(name = "holdingblocks", filter = Filter.Type.ON_CHANGE) public List<Long> getHoldingBlocks() { // stack.toArray gives stack with top=LAST element. Need to reverse. // to reverse, we need to make copy first of the array. // Notice that collections.reverse is modifying the provided array! List<Block> blockstack = new ArrayList<>(ourRobot.getHolding()); Collections.reverse(blockstack); List<Long> holds = new ArrayList<>(blockstack.size()); for (Block b : blockstack) { holds.add(b.getId()); } return holds; } /** * Actual gripper capacity * * @return max number of blocks the bot can hold. 0 if bot has gripper * handicap. */ @AsPercept(name = "gripperCapacity", filter = Filter.Type.ON_CHANGE) public Integer gripperCapacity() { return ourRobot.getGripperCapacity(); } /** * The sequence in which the blocks should be returned. Send at the * beginning * * @return sequence of blocks */ @AsPercept(name = "sequence", filter = Filter.Type.ONCE) public List<nl.tudelft.bw4t.map.BlockColor> getSequence() { DropZone dropZone = (DropZone) context.getObjects(DropZone.class).get(0); return dropZone.getSequence(); } /** * The index of the block that needs to be brought back now. Send on change * * @return sequence index */ @AsPercept(name = "sequenceIndex", filter = Filter.Type.ON_CHANGE) public Integer getSequenceIndex() { DropZone dropZone = (DropZone) context.getObjects(DropZone.class).get(0); return dropZone.getSequenceIndex(); } /** * occupied percept, tells which rooms are occupied by robot. Send if true * and send negated if no longer true * * @return list of occupied room IDs */ @AsPercept(name = "occupied", multiplePercepts = true, filter = Filter.Type.ON_CHANGE_NEG) public List<String> getOccupied() { List<String> rooms = new LinkedList<>(); for (Object r : context.getObjects(Room.class)) { Room room = (Room) r; if (room.getOccupier() != null) { rooms.add(room.getName()); } } return rooms; } /** * navpoint percept, tells which {@link Zone}s there are in the world and * their neighbours. Send at the beginning * * @return list of navpoints */ @AsPercept(name = "zone", multiplePercepts = true, filter = Filter.Type.ONCE) public List<Zone> getNavPoints() { IndexedIterable<Object> objects = context.getObjects(Zone.class); List<Zone> zones = new ArrayList<>(objects.size()); for (Object o : objects ) { Zone zone = (Zone) o; zones.add(zone); } return zones; } /** * Returns all messages received by the player, Send on change * * @return the messages that were received */ @AsPercept(name = "message", multiplePercepts = true, filter = Filter.Type.ALWAYS) public List<ArrayList<String>> getMessages() { List<ArrayList<String>> msg = messages; messages = new LinkedList<>(); return msg; } /** * The current state of the robot. See {@link NavigatingRobot#getState()}. * Send on change * * @return state */ @AsPercept(name = "state", filter = Filter.Type.ON_CHANGE) public String getState() { return ourRobot.getState().toString().toLowerCase(); } /** * The battery level of the robot. The battery can be re-charged by moving * through a charge zone. The battery use depends on the size and speed of * the robot as set in the configuration. By default this is off. See also * {@link BotConfig#calculateDischargeRate(int, int)}. * * * @return batterypercentage */ @AsPercept(name = "battery", multiplePercepts = false, filter = Filter.Type.ON_CHANGE) public double getBatteryPercentage() { return ourRobot.getBattery().getPercentage(); } /** * The battery level of the robot. The battery can be re-charged by moving * through a charge zone. The battery use depends on the size and speed of * the robot as set in the configuration. By default this is off. See also * {@link BotConfig#calculateDischargeRate(int, int)}. * * * @return batterypercentage */ @AsPercept(name = "colorblind", multiplePercepts = false, filter = Filter.Type.ONCE) public boolean isColorBlind() { return ourRobot.getHandicapsList().contains("ColorBlind"); } /** * Instructs the robot to move to the given location. * * @param x * The X coordinate of the location. * @param y * The Y coordinate of the location. */ @AsAction(name = "goTo") public void goTo(double x, double y) { ourRobot.setTargetLocation(new NdPoint(x, y)); } /** * Instructs the robot to move to the given object. Only works if we are in * the room containing the block - the block must be visible. * * @param targetid * is the target object id. This can be the id of any object in * the map (not a free x,y location) */ @AsAction(name = "goToBlock") public void goTo(long targetid) { BoundedMoveableObject target = getBlock(targetid); ourRobot.setTarget(target); } /** * Instructs the robot to move to the given navpoint * * @param navPoint * , the navpoint the robot should move to */ @AsAction(name = "goTo") public void goTo(String navPoint) { Zone target = ZoneLocator.getZone(navPoint); if (target == null) { throw new IllegalArgumentException("unknown place " + navPoint); } ourRobot.setTarget(target); } /** * Instructs the robot to pick up a block. */ @AsAction(name = "pickUp") public void pickUp(long blockId) { LOGGER.debug(String.format("%s is trying to pick up a block.", ourRobot.getName())); Block block = getBlock(blockId); LOGGER.debug(String.format("%s will pickup block %d.", ourRobot.getName(), block.getId())); if (!ourRobot.canPickUp(block)) return; ourRobot.pickUp(block); } /** * Instruct the robot to navigate the current obstacles. */ @AsAction(name = "navigateObstacles") public void navigateObstacles() { LOGGER.debug(String.format("%s is trying to navigate the following obstacles: ", ourRobot.getName())); for (BoundedMoveableObject obj : ourRobot.getObstacles()) { LOGGER.debug(obj + " at " + obj.getBoundingBox()); } if (ourRobot instanceof NavigatingRobot) { NavigatingRobot navbot = (NavigatingRobot) ourRobot; navbot.navigateObstacles(); } else if (ourRobot instanceof AbstractRobotDecorator) { IRobot robotEarliestParent = ourRobot.getEarliestParent(); if (robotEarliestParent != null && robotEarliestParent instanceof NavigatingRobot) { NavigatingRobot navbot = (NavigatingRobot) robotEarliestParent; navbot.navigateObstacles(); } } } /** * Instructs the robot to send a message * * @param message * , the message that should be sent * @param receiver * , the receiver of the message (can be all or the id of another * robot * @throws ActException * if the action fails */ @AsAction(name = "sendMessage") public void sendMessage(String receiver, String message) throws ActException { ourRobot.getAgentRecord().addSentMessage(); // Translate the message into parameters Parameter[] parameters = new Parameter[2]; try { parameters[0] = Translator.getInstance().translate2Parameter(ourRobot.getName())[0]; parameters[1] = Translator.getInstance().translate2Parameter(message)[0]; } catch (TranslationException e) { throw new ActException("translating of message failed:" + message, e); } // Send to all other entities (except self) if ("all".equals(receiver)) { for (String entity : BW4TEnvironment.getInstance().getEntities()) { BW4TEnvironment.getInstance().performClientAction(entity, new Action("receiveMessage", parameters)); } // Send to a single entity } else { BW4TEnvironment.getInstance().performClientAction(receiver, new Action("receiveMessage", parameters)); } } /** * Instructs the robot to receive a certain message, should only be used * internally in the server environment and therefore is not documented in * the manual. * * @param message * , the message to be received * @param sender * , the sender of the message */ @AsAction(name = "receiveMessage") public void receiveMessage(String sender, String message) { // Add message to messageArray ArrayList<String> messageArray = new ArrayList<>(2); messageArray.add(sender); messageArray.add(message); messages.add(messageArray); } /** * Instructs the robot to drop the block it is holding. */ @AsAction(name = "putDown") public void putDown() { ourRobot.drop(); } /** * Hack. Seems we need to get context through an entity... #1955 * * @return true if sequence is finished. */ @AsAction(name = "checkSequenceDone") public boolean checkSequenceDone() { DropZone dropzone = (DropZone) context.getObjects(DropZone.class).get(0); if (dropzone == null) { return false; } return dropzone.sequenceComplete(); } /** * Give the robot a list of all the EPartners in the zone. * * @return the list of EPartners */ @AsPercept(name = "epartner", multiplePercepts = true, filter = Type.ALWAYS) public List<EPartner> getEPartners() { List<EPartner> eps = new LinkedList<>(); for (EPartner ep : getVisible(EPartner.class)) { eps.add(ep); } EPartner mine = ourRobot.getEPartner(); if (mine != null && !eps.contains(mine)) { eps.add(mine); } return eps; } /** * Only available for the human: picks up the e-Partner. */ @AsAction(name = "pickUpEPartner") public void pickUpEPartner() { LOGGER.debug(String.format("%s is trying to pick up an e-partner.", ourRobot.getName())); EPartner nearest = getClosest(EPartner.class); if (nearest == null) { LOGGER.debug(String.format("%s can not pickup any e-partners.", ourRobot.getName())); return; } LOGGER.debug(String.format("%s will pickup e-partner %d.", ourRobot.getName(), nearest.getId())); ourRobot.pickUpEPartner(nearest); } /** * Only available for the human: drops the e-Partner they are currently * holding. */ @AsAction(name = "putDownEPartner") public void dropEPartner() { if (ourRobot.isHuman()) { ourRobot.dropEPartner(); } } /** * Send the robot a bumped percept, informing it that the path is blocked by * the given robot. * * @return Bump percept with the name of the robot in the way, if any. */ @AsPercept(name = "bumped", multiplePercepts = true, filter = Type.ON_CHANGE_NEG) public List<String> getBumped() { List<String> bumpedList = new LinkedList<>(); if (ourRobot.isCollided() && !ourRobot.getObstacles().isEmpty()) { for (BoundedMoveableObject obj : ourRobot.getObstacles()) { if (obj instanceof IRobot) { IRobot bot = (IRobot) obj; bumpedList.add(bot.getName()); } } } return bumpedList; } /** * Percepts for when this robot has a target that is unreachable/reachable * (on change). * * @return The percept in the form of an object with an id (long) and a * boolean. */ @AsPercept(name = "oldTargetUnreachable", multiplePercepts = false, filter = Filter.Type.ON_CHANGE) public IdAndBoolean getOldTargetUnreachable() { return new IdAndBoolean(ourRobot.getId(), ourRobot.isDestinationUnreachable()); } /********************* support funcs *********************/ /** * @return All bounded moveable objects of class T that are visible to the * robot. Excluding the one the robot is holding. * @param <T> * @param type */ private <T extends BoundedMoveableInterface> Set<T> getVisible(Class<T> type) { Set<T> set = new HashSet<>(); if (context == null) { return set; } // Add all objects in the same room as the robot. Iterable<Object> allObjects = context.getObjects(type); Zone zone = ZoneLocator.getZoneAt(ourRobotLocation.getX(), ourRobotLocation.getY()); for (Object b : allObjects) { @SuppressWarnings("unchecked") T aObject = (T) b; Double p = new Point2D.Double(aObject.getLocation().getX(), aObject.getLocation().getY()); if (zone != null && zone.getBoundingBox().contains(p)) { set.add(aObject); } } return set; } /** * @param targetid * @return block that has given target ID * @throws IllegalArgumentException * if no such block. */ private Block getBlock(long targetid) { for (Block b : getVisible(Block.class)) { if (b.getId() == targetid) { return b; } } throw new IllegalArgumentException("there is no block with id=" + targetid); } /** * Reset the robot's location and should set it to its default spawn state. * We try to have the bot drop all the blocks before it exists. */ protected void reset() { while (!ourRobot.getHolding().isEmpty()) { ourRobot.drop(); } ourRobot.moveTo(this.spawnLocation.getX(), this.spawnLocation.getY()); } /** * Find the closest {@link BoundedMoveableObject} that can be picked up by * the Robot. * * @param type * the type of {@link BoundedMoveableObject} we are looking for * @param <T> * @return null if non were found, otherwise the closest */ private <T extends BoundedMoveableObject> T getClosest(Class<T> type) { Iterable<Object> allBlocks = context.getObjects(type); T nearest = null; double nearestDistance = Integer.MAX_VALUE; for (Object o : allBlocks) { @SuppressWarnings("unchecked") T aBlock = (T) o; double distance = ourRobot.distanceTo(aBlock); LOGGER.trace(String.format("%s is %f units away from %d.", ourRobot.getName(), distance, aBlock.getId())); if (ourRobot.canPickUp(aBlock)) { LOGGER.trace(String.format("%s can pick up %d checking distance.", ourRobot.getName(), aBlock.getId())); if (nearest == null || distance < nearestDistance) { nearest = aBlock; nearestDistance = distance; } } } return nearest; } }