From 8c627bda8cdd50132138ff167d1a2f0f905ba99c Mon Sep 17 00:00:00 2001 From: Chris Cromer Date: Sat, 26 Oct 2019 17:35:11 -0300 Subject: [PATCH] Make the AI more robust Signed-off-by: Chris Cromer --- src/main/java/cl/cromer/azaraka/Canvas.java | 2 +- src/main/java/cl/cromer/azaraka/Scene.java | 1 + src/main/java/cl/cromer/azaraka/ai/AI.java | 11 + .../java/cl/cromer/azaraka/ai/PlayerAI.java | 14 +- .../cl/cromer/azaraka/ai/PlayerAStarAI.java | 321 ++++++++++++++---- .../azaraka/ai/PlayerBreadthFirstAI.java | 29 +- .../java/cl/cromer/azaraka/object/Key.java | 7 + 7 files changed, 305 insertions(+), 80 deletions(-) diff --git a/src/main/java/cl/cromer/azaraka/Canvas.java b/src/main/java/cl/cromer/azaraka/Canvas.java index 8554abb..40dc6f4 100644 --- a/src/main/java/cl/cromer/azaraka/Canvas.java +++ b/src/main/java/cl/cromer/azaraka/Canvas.java @@ -318,7 +318,7 @@ public class Canvas extends java.awt.Canvas implements Constants { } for (Key key : keys) { - player.getAi().addDestination(new State(key.getCell().getX(), key.getCell().getY(), State.Type.KEY, null, 2)); + player.getAi().addDestination(new State(key.getCell().getX(), key.getCell().getY(), State.Type.KEY, null, 1)); } player.getAi().sortDestinations(); diff --git a/src/main/java/cl/cromer/azaraka/Scene.java b/src/main/java/cl/cromer/azaraka/Scene.java index e9cf13c..8b8410c 100644 --- a/src/main/java/cl/cromer/azaraka/Scene.java +++ b/src/main/java/cl/cromer/azaraka/Scene.java @@ -60,6 +60,7 @@ public class Scene extends JComponent implements Constants { * The cells of the game */ private final Cell[][] cells; + //private final CopyOnWriteArrayList> cells; /** * The logger */ diff --git a/src/main/java/cl/cromer/azaraka/ai/AI.java b/src/main/java/cl/cromer/azaraka/ai/AI.java index c2ccfb3..f54ff85 100644 --- a/src/main/java/cl/cromer/azaraka/ai/AI.java +++ b/src/main/java/cl/cromer/azaraka/ai/AI.java @@ -76,6 +76,17 @@ public class AI implements Runnable { throw new AIException("The addDestination method should be run by the child only!"); } + /** + * Remove the picked up key from destinations if it is there + * + * @param x The x coordinate of the key + * @param y The y coordinate of the key + * @throws AIException Thrown when the parent method is called directly + */ + public void removeKeyDestination(int x, int y) throws AIException { + throw new AIException("The addDestination method should be run by the child only!"); + } + /** * Sort the destinations * diff --git a/src/main/java/cl/cromer/azaraka/ai/PlayerAI.java b/src/main/java/cl/cromer/azaraka/ai/PlayerAI.java index e70016a..ca84329 100644 --- a/src/main/java/cl/cromer/azaraka/ai/PlayerAI.java +++ b/src/main/java/cl/cromer/azaraka/ai/PlayerAI.java @@ -129,10 +129,14 @@ public interface PlayerAI extends Runnable, Constants { if (player.getAnimation().getCurrentDirection() != Animation.Direction.UP) { player.keyPressed(KeyEvent.VK_UP); } - player.interact(); + boolean portalWasActive = false; Portal portal = scene.getCanvas().getPortal(); if (portal.getState() == Portal.State.ACTIVE) { - addDestination(new State(portal.getCell().getX(), portal.getCell().getY(), State.Type.PORTAL, null, 2)); + portalWasActive = true; + } + player.interact(); + if (!portalWasActive) { + addDestination(new State(portal.getCell().getX(), portal.getCell().getY(), State.Type.PORTAL, null, 1)); } sortDestinations(); return true; @@ -213,6 +217,12 @@ public interface PlayerAI extends Runnable, Constants { if (player.getCell().getY() < VERTICAL_CELLS - 1 && scene.getCells()[player.getCell().getX()][player.getCell().getY() + 1].getObject() == null) { openSpaces.add(State.Type.DOWN); } + + if (openSpaces.size() == 0) { + // The player can't move + return State.Type.EXIT; + } + int random = random(0, openSpaces.size() - 1); return openSpaces.get(random); } diff --git a/src/main/java/cl/cromer/azaraka/ai/PlayerAStarAI.java b/src/main/java/cl/cromer/azaraka/ai/PlayerAStarAI.java index 846f005..81c75cb 100644 --- a/src/main/java/cl/cromer/azaraka/ai/PlayerAStarAI.java +++ b/src/main/java/cl/cromer/azaraka/ai/PlayerAStarAI.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.Queue; +import java.util.concurrent.CopyOnWriteArrayList; /** * The class implements the A* search AI algorithm for the player @@ -58,7 +59,7 @@ public class PlayerAStarAI extends AI implements PlayerAI, Constants { /** * The destinations the player needs to visit */ - private List destinations = new ArrayList<>(); + private List destinations = new CopyOnWriteArrayList<>(); /** * The objective that was found */ @@ -95,7 +96,7 @@ public class PlayerAStarAI extends AI implements PlayerAI, Constants { cameFrom.put(start, start); costSoFar.put(start, 0.0); - while (frontier.size() > 0 && cameFrom.size() <= (HORIZONTAL_CELLS * VERTICAL_CELLS) * 2) { + while (frontier.size() > 0 && cameFrom.size() <= (HORIZONTAL_CELLS * VERTICAL_CELLS) * 5) { State current = frontier.poll(); if (current.equals(goal)) { @@ -200,52 +201,155 @@ public class PlayerAStarAI extends AI implements PlayerAI, Constants { private double getCost(State state) { // The cost increases based on how close the enemy is /* - 2 - 444 + 22222 + 24442 24842 - 444 - 2 + 24442 + 22222 */ - if (state.getOperation() == State.Type.ENEMY) { - return 8; + + EnemyCost enemyCost = EnemyCost.DIRECT_CORNERS; + + if (enemyCost.getLevel() == EnemyCost.NONE.getLevel()) { + return EnemyCost.NONE.getCost(); } - else if (state.getX() > 0 && scene.getCells()[state.getX() - 1][state.getY()].getObject() instanceof Enemy) { - return 4; + + if (enemyCost.getLevel() >= EnemyCost.DIRECT.getLevel()) { + // The enemy + if (scene.getCells()[state.getX()][state.getY()].getObject() instanceof Enemy) { + return EnemyCost.DIRECT.getCost(); + } } - else if (state.getX() < HORIZONTAL_CELLS - 1 && scene.getCells()[state.getX() + 1][state.getY()].getObject() instanceof Enemy) { - return 4; + + if (enemyCost.getLevel() >= EnemyCost.DIRECT_SIDES.getLevel()) { + // Left + if (state.getX() > 0 && scene.getCells()[state.getX() - 1][state.getY()].getObject() instanceof Enemy) { + return EnemyCost.DIRECT_SIDES.getCost(); + } + + // Right + else if (state.getX() < HORIZONTAL_CELLS - 1 && scene.getCells()[state.getX() + 1][state.getY()].getObject() instanceof Enemy) { + return EnemyCost.DIRECT_SIDES.getCost(); + } + + // Up + else if (state.getY() > 0 && scene.getCells()[state.getX()][state.getY() - 1].getObject() instanceof Enemy) { + return EnemyCost.DIRECT_SIDES.getCost(); + } + + // Down + else if (state.getY() < VERTICAL_CELLS - 1 && scene.getCells()[state.getX()][state.getY() + 1].getObject() instanceof Enemy) { + return EnemyCost.DIRECT_SIDES.getCost(); + } } - else if (state.getY() > 0 && scene.getCells()[state.getX()][state.getY() - 1].getObject() instanceof Enemy) { - return 4; + + if (enemyCost.getLevel() >= EnemyCost.DIRECT_CORNERS.getLevel()) { + // Upper left corner + if (state.getX() > 0 && state.getY() > 0 && scene.getCells()[state.getX() - 1][state.getY() - 1].getObject() instanceof Enemy) { + return EnemyCost.DIRECT_CORNERS.getCost(); + } + + // Upper right corner + else if (state.getX() < HORIZONTAL_CELLS - 1 && state.getY() > 0 && scene.getCells()[state.getX() + 1][state.getY() - 1].getObject() instanceof Enemy) { + return EnemyCost.DIRECT_CORNERS.getCost(); + } + + // Lower left corner + else if (state.getX() > 0 && state.getY() < VERTICAL_CELLS - 1 && scene.getCells()[state.getX() - 1][state.getY() + 1].getObject() instanceof Enemy) { + return EnemyCost.DIRECT_CORNERS.getCost(); + } + + // Lower right corner + else if (state.getX() < HORIZONTAL_CELLS - 1 && state.getY() < VERTICAL_CELLS - 1 && scene.getCells()[state.getX() + 1][state.getY() + 1].getObject() instanceof Enemy) { + return EnemyCost.DIRECT_CORNERS.getCost(); + } } - else if (state.getY() < VERTICAL_CELLS - 1 && scene.getCells()[state.getX()][state.getY() + 1].getObject() instanceof Enemy) { - return 4; + + if (enemyCost.getLevel() >= EnemyCost.FAR_SIDES.getLevel()) { + // Left + if (state.getX() > 1 && scene.getCells()[state.getX() - 2][state.getY()].getObject() instanceof Enemy) { + return EnemyCost.FAR_SIDES.getCost(); + } + + // Right + else if (state.getX() < HORIZONTAL_CELLS - 2 && scene.getCells()[state.getX() + 2][state.getY()].getObject() instanceof Enemy) { + return EnemyCost.FAR_SIDES.getCost(); + } + + // Up + else if (state.getY() > 1 && scene.getCells()[state.getX()][state.getY() - 2].getObject() instanceof Enemy) { + return EnemyCost.FAR_SIDES.getCost(); + } + + // Down + else if (state.getY() < VERTICAL_CELLS - 2 && scene.getCells()[state.getX()][state.getY() + 2].getObject() instanceof Enemy) { + return EnemyCost.FAR_SIDES.getCost(); + } } - else if (state.getX() > 1 && scene.getCells()[state.getX() - 2][state.getY()].getObject() instanceof Enemy) { - return 2; + + if (enemyCost.getLevel() >= EnemyCost.FAR_CORNERS.getLevel()) { + // Upper left corner + if (state.getX() > 1 && state.getY() > 0 && scene.getCells()[state.getX() - 2][state.getY() - 1].getObject() instanceof Enemy) { + return EnemyCost.FAR_CORNERS.getCost(); + } + else if (state.getX() > 1 && state.getY() > 1 && scene.getCells()[state.getX() - 2][state.getY() - 2].getObject() instanceof Enemy) { + return EnemyCost.FAR_CORNERS.getCost(); + } + else if (state.getX() > 0 && state.getY() > 1 && scene.getCells()[state.getX() - 1][state.getY() - 2].getObject() instanceof Enemy) { + return EnemyCost.FAR_CORNERS.getCost(); + } + + // Upper right corner + else if (state.getX() < HORIZONTAL_CELLS - 2 && state.getY() > 0 && scene.getCells()[state.getX() + 2][state.getY() - 1].getObject() instanceof Enemy) { + return EnemyCost.FAR_CORNERS.getCost(); + } + else if (state.getX() < HORIZONTAL_CELLS - 2 && state.getY() > 1 && scene.getCells()[state.getX() + 2][state.getY() - 2].getObject() instanceof Enemy) { + return EnemyCost.FAR_CORNERS.getCost(); + } + else if (state.getX() < HORIZONTAL_CELLS - 1 && state.getY() > 1 && scene.getCells()[state.getX() + 1][state.getY() - 2].getObject() instanceof Enemy) { + return EnemyCost.FAR_CORNERS.getCost(); + } + + // Lower left corner + else if (state.getX() > 1 && state.getY() < VERTICAL_CELLS - 1 && scene.getCells()[state.getX() - 2][state.getY() + 1].getObject() instanceof Enemy) { + return EnemyCost.FAR_CORNERS.getCost(); + } + else if (state.getX() > 1 && state.getY() < VERTICAL_CELLS - 2 && scene.getCells()[state.getX() - 2][state.getY() + 2].getObject() instanceof Enemy) { + return EnemyCost.FAR_CORNERS.getCost(); + } + else if (state.getX() > 0 && state.getY() < VERTICAL_CELLS - 2 && scene.getCells()[state.getX() - 1][state.getY() + 2].getObject() instanceof Enemy) { + return EnemyCost.FAR_CORNERS.getCost(); + } + + // Lower right corner + else if (state.getX() < HORIZONTAL_CELLS - 2 && state.getY() < VERTICAL_CELLS - 1 && scene.getCells()[state.getX() + 2][state.getY() + 1].getObject() instanceof Enemy) { + return EnemyCost.FAR_CORNERS.getCost(); + } + else if (state.getX() < HORIZONTAL_CELLS - 2 && state.getY() < VERTICAL_CELLS - 2 && scene.getCells()[state.getX() + 2][state.getY() + 2].getObject() instanceof Enemy) { + return EnemyCost.FAR_CORNERS.getCost(); + } + else if (state.getX() < HORIZONTAL_CELLS - 1 && state.getY() < VERTICAL_CELLS - 2 && scene.getCells()[state.getX() + 1][state.getY() + 2].getObject() instanceof Enemy) { + return EnemyCost.FAR_CORNERS.getCost(); + } } - else if (state.getX() < HORIZONTAL_CELLS - 2 && scene.getCells()[state.getX() + 2][state.getY()].getObject() instanceof Enemy) { - return 2; + + return EnemyCost.NONE.getCost(); + } + + /** + * Remove the picked up key from destinations if it is there + * + * @param x The x coordinate of the key + * @param y The y coordinate of the key + */ + public void removeKeyDestination(int x, int y) { + for (State state : destinations) { + if (state.getOperation() == State.Type.KEY && state.getX() == x && state.getY() == y) { + destinations.remove(state); + sortDestinations(); + break; + } } - else if (state.getY() > 1 && scene.getCells()[state.getX()][state.getY() - 2].getObject() instanceof Enemy) { - return 2; - } - else if (state.getY() < VERTICAL_CELLS - 2 && scene.getCells()[state.getX()][state.getY() + 2].getObject() instanceof Enemy) { - return 2; - } - else if (state.getX() > 0 && state.getY() > 0 && scene.getCells()[state.getX() - 1][state.getY() - 1].getObject() instanceof Enemy) { - return 4; - } - else if (state.getX() < HORIZONTAL_CELLS - 1 && state.getY() > 0 && scene.getCells()[state.getX() + 1][state.getY() - 1].getObject() instanceof Enemy) { - return 4; - } - else if (state.getX() > 0 && state.getY() < VERTICAL_CELLS - 1 && scene.getCells()[state.getX() - 1][state.getY() + 1].getObject() instanceof Enemy) { - return 4; - } - else if (state.getX() < HORIZONTAL_CELLS - 1 && state.getY() < VERTICAL_CELLS - 1 && scene.getCells()[state.getX() + 1][state.getY() + 1].getObject() instanceof Enemy) { - return 4; - } - return 1; } /** @@ -257,37 +361,6 @@ public class PlayerAStarAI extends AI implements PlayerAI, Constants { destinations.add(destination); } - /** - * Calculate the route from the objective to the player - */ - private void calculateRoute() { - getLogger().info("Calculate the route!"); - State predecessor = foundObjective; - do { - steps.add(0, predecessor.getOperation()); - predecessor = predecessor.getPredecessor(); - } - while (predecessor != null); - } - - /** - * Sort the destinations by importance, if the importance is the same then sort them by distance - */ - @Override - public void sortDestinations() { - destinations = sortDestinations(destinations, initial); - } - - /** - * Clear the states to be ready for a new search - */ - private void clearStates() { - frontier.clear(); - cameFrom.clear(); - costSoFar.clear(); - steps.clear(); - } - /** * Run this in a loop */ @@ -334,11 +407,14 @@ public class PlayerAStarAI extends AI implements PlayerAI, Constants { } destinationIndex++; if (destinationIndex >= destinations.size()) { - destinationIndex = 0; + getLogger().info("None of the destinations are reachable for A* Search!"); // No destinations are reachable, make the player move around at random to help move the enemies - if (steps.size() > 0) { - steps.add(1, getOpenSpaceAroundPlayer(scene)); + if (steps.size() == 0) { + steps.add(0, State.Type.PLAYER); } + steps.add(1, getOpenSpaceAroundPlayer(scene)); + found = true; + break; } } while (!found && !destinations.isEmpty()); @@ -348,4 +424,103 @@ public class PlayerAStarAI extends AI implements PlayerAI, Constants { } } } + + /** + * Calculate the route from the objective to the player + */ + private void calculateRoute() { + getLogger().info("Calculate the route!"); + State predecessor = foundObjective; + do { + steps.add(0, predecessor.getOperation()); + predecessor = predecessor.getPredecessor(); + } + while (predecessor != null); + } + + /** + * Sort the destinations by importance, if the importance is the same then sort them by distance + */ + @Override + public void sortDestinations() { + destinations = sortDestinations(destinations, initial); + } + + /** + * Clear the states to be ready for a new search + */ + private void clearStates() { + frontier.clear(); + cameFrom.clear(); + costSoFar.clear(); + steps.clear(); + } + + /** + * The cost based on enemy position + */ + private enum EnemyCost { + /** + * The enemy does not have a cost + */ + NONE(0, 1), + /** + * The enemy cell has a cost + */ + DIRECT(1, 8), + /** + * The cells to the side of the enemy have a cost + */ + DIRECT_SIDES(2, 4), + /** + * The cells on the corner of the enemy + */ + DIRECT_CORNERS(3, 4), + /** + * The cells father to the side of the enemy + */ + FAR_SIDES(4, 2), + /** + * The cells in the corner farthest from the enemy + */ + FAR_CORNERS(5, 2); + + /** + * The level of cost to use for the enemy + */ + private final int level; + /** + * The cost value to use + */ + private final int cost; + + /** + * Initialize the enemy cost and level + * + * @param level The level + * @param cost The cost + */ + EnemyCost(int level, int cost) { + this.level = level; + this.cost = cost; + } + + /** + * Get the cost level of the enemy + * + * @return Returns the cost level + */ + protected int getLevel() { + return this.level; + } + + /** + * Get the cost of the enemy + * + * @return Returns the cost + */ + protected int getCost() { + return this.cost; + } + } } \ No newline at end of file diff --git a/src/main/java/cl/cromer/azaraka/ai/PlayerBreadthFirstAI.java b/src/main/java/cl/cromer/azaraka/ai/PlayerBreadthFirstAI.java index 4ff88f4..9fb7bac 100644 --- a/src/main/java/cl/cromer/azaraka/ai/PlayerBreadthFirstAI.java +++ b/src/main/java/cl/cromer/azaraka/ai/PlayerBreadthFirstAI.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.List; import java.util.PriorityQueue; import java.util.Queue; +import java.util.concurrent.CopyOnWriteArrayList; /** * This is an implementation of the Breadth-First search algorithm with multiple objectives @@ -59,7 +60,7 @@ public class PlayerBreadthFirstAI extends AI implements PlayerAI, Constants { /** * The destinations to visit */ - private List destinations = new ArrayList<>(); + private List destinations = new CopyOnWriteArrayList<>(); /** * The initial point to start searching from */ @@ -220,6 +221,22 @@ public class PlayerBreadthFirstAI extends AI implements PlayerAI, Constants { destinations.add(destination); } + /** + * Remove the picked up key from destinations if it is there + * + * @param x The x coordinate of the key + * @param y The y coordinate of the key + */ + public void removeKeyDestination(int x, int y) { + for (State state : destinations) { + if (state.getOperation() == State.Type.KEY && state.getX() == x && state.getY() == y) { + destinations.remove(state); + sortDestinations(); + break; + } + } + } + /** * Sort the destinations by importance, if the importance is the same then sort them by distance */ @@ -284,10 +301,14 @@ public class PlayerBreadthFirstAI extends AI implements PlayerAI, Constants { } destinationIndex++; if (destinationIndex >= destinations.size()) { - destinationIndex = 0; - if (steps.size() > 0) { - steps.add(1, getOpenSpaceAroundPlayer(scene)); + getLogger().info("None of the destinations are reachable for Breadth-First Search!"); + // No destinations are reachable, make the player move around at random to help move the enemies + if (steps.size() == 0) { + steps.add(0, State.Type.PLAYER); } + steps.add(1, getOpenSpaceAroundPlayer(scene)); + found = true; + break; } } while (!found && !destinations.isEmpty()); diff --git a/src/main/java/cl/cromer/azaraka/object/Key.java b/src/main/java/cl/cromer/azaraka/object/Key.java index 38cc40b..1ae1301 100644 --- a/src/main/java/cl/cromer/azaraka/object/Key.java +++ b/src/main/java/cl/cromer/azaraka/object/Key.java @@ -18,6 +18,7 @@ package cl.cromer.azaraka.object; import cl.cromer.azaraka.Cell; import cl.cromer.azaraka.Constants; import cl.cromer.azaraka.Scene; +import cl.cromer.azaraka.ai.AIException; import cl.cromer.azaraka.sound.Sound; import cl.cromer.azaraka.sound.SoundException; import cl.cromer.azaraka.sprite.Animation; @@ -116,6 +117,12 @@ public class Key extends Object implements Constants { // Remove the key from the cell getCell().setObjectOnBottom(null); setState(State.HELD); + try { + getScene().getCanvas().getPlayer().getAi().removeKeyDestination(getCell().getX(), getCell().getY()); + } + catch (AIException e) { + getLogger().warning(e.getMessage()); + } } /**