Make the AI more robust

Signed-off-by: Chris Cromer <chris@cromer.cl>
This commit is contained in:
Chris Cromer 2019-10-26 17:35:11 -03:00
parent fb7c7ec227
commit 8c627bda8c
7 changed files with 305 additions and 80 deletions

View File

@ -318,7 +318,7 @@ public class Canvas extends java.awt.Canvas implements Constants {
} }
for (Key key : keys) { 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(); player.getAi().sortDestinations();

View File

@ -60,6 +60,7 @@ public class Scene extends JComponent implements Constants {
* The cells of the game * The cells of the game
*/ */
private final Cell[][] cells; private final Cell[][] cells;
//private final CopyOnWriteArrayList<CopyOnWriteArrayList<Cell>> cells;
/** /**
* The logger * The logger
*/ */

View File

@ -76,6 +76,17 @@ public class AI implements Runnable {
throw new AIException("The addDestination method should be run by the child only!"); 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 * Sort the destinations
* *

View File

@ -129,10 +129,14 @@ public interface PlayerAI extends Runnable, Constants {
if (player.getAnimation().getCurrentDirection() != Animation.Direction.UP) { if (player.getAnimation().getCurrentDirection() != Animation.Direction.UP) {
player.keyPressed(KeyEvent.VK_UP); player.keyPressed(KeyEvent.VK_UP);
} }
player.interact(); boolean portalWasActive = false;
Portal portal = scene.getCanvas().getPortal(); Portal portal = scene.getCanvas().getPortal();
if (portal.getState() == Portal.State.ACTIVE) { 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(); sortDestinations();
return true; 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) { if (player.getCell().getY() < VERTICAL_CELLS - 1 && scene.getCells()[player.getCell().getX()][player.getCell().getY() + 1].getObject() == null) {
openSpaces.add(State.Type.DOWN); 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); int random = random(0, openSpaces.size() - 1);
return openSpaces.get(random); return openSpaces.get(random);
} }

View File

@ -26,6 +26,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.PriorityQueue; import java.util.PriorityQueue;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.CopyOnWriteArrayList;
/** /**
* The class implements the A* search AI algorithm for the player * 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 * The destinations the player needs to visit
*/ */
private List<State> destinations = new ArrayList<>(); private List<State> destinations = new CopyOnWriteArrayList<>();
/** /**
* The objective that was found * The objective that was found
*/ */
@ -95,7 +96,7 @@ public class PlayerAStarAI extends AI implements PlayerAI, Constants {
cameFrom.put(start, start); cameFrom.put(start, start);
costSoFar.put(start, 0.0); 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(); State current = frontier.poll();
if (current.equals(goal)) { if (current.equals(goal)) {
@ -200,52 +201,155 @@ public class PlayerAStarAI extends AI implements PlayerAI, Constants {
private double getCost(State state) { private double getCost(State state) {
// The cost increases based on how close the enemy is // The cost increases based on how close the enemy is
/* /*
2 22222
444 24442
24842 24842
444 24442
2 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); 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 * Run this in a loop
*/ */
@ -334,11 +407,14 @@ public class PlayerAStarAI extends AI implements PlayerAI, Constants {
} }
destinationIndex++; destinationIndex++;
if (destinationIndex >= destinations.size()) { 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 // No destinations are reachable, make the player move around at random to help move the enemies
if (steps.size() > 0) { if (steps.size() == 0) {
steps.add(1, getOpenSpaceAroundPlayer(scene)); steps.add(0, State.Type.PLAYER);
} }
steps.add(1, getOpenSpaceAroundPlayer(scene));
found = true;
break;
} }
} }
while (!found && !destinations.isEmpty()); 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;
}
}
} }

View File

@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.PriorityQueue; import java.util.PriorityQueue;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.CopyOnWriteArrayList;
/** /**
* This is an implementation of the Breadth-First search algorithm with multiple objectives * 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 * The destinations to visit
*/ */
private List<State> destinations = new ArrayList<>(); private List<State> destinations = new CopyOnWriteArrayList<>();
/** /**
* The initial point to start searching from * The initial point to start searching from
*/ */
@ -220,6 +221,22 @@ public class PlayerBreadthFirstAI extends AI implements PlayerAI, Constants {
destinations.add(destination); 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 * 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++; destinationIndex++;
if (destinationIndex >= destinations.size()) { if (destinationIndex >= destinations.size()) {
destinationIndex = 0; getLogger().info("None of the destinations are reachable for Breadth-First Search!");
if (steps.size() > 0) { // No destinations are reachable, make the player move around at random to help move the enemies
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()); while (!found && !destinations.isEmpty());

View File

@ -18,6 +18,7 @@ package cl.cromer.azaraka.object;
import cl.cromer.azaraka.Cell; import cl.cromer.azaraka.Cell;
import cl.cromer.azaraka.Constants; import cl.cromer.azaraka.Constants;
import cl.cromer.azaraka.Scene; import cl.cromer.azaraka.Scene;
import cl.cromer.azaraka.ai.AIException;
import cl.cromer.azaraka.sound.Sound; import cl.cromer.azaraka.sound.Sound;
import cl.cromer.azaraka.sound.SoundException; import cl.cromer.azaraka.sound.SoundException;
import cl.cromer.azaraka.sprite.Animation; import cl.cromer.azaraka.sprite.Animation;
@ -116,6 +117,12 @@ public class Key extends Object implements Constants {
// Remove the key from the cell // Remove the key from the cell
getCell().setObjectOnBottom(null); getCell().setObjectOnBottom(null);
setState(State.HELD); setState(State.HELD);
try {
getScene().getCanvas().getPlayer().getAi().removeKeyDestination(getCell().getX(), getCell().getY());
}
catch (AIException e) {
getLogger().warning(e.getMessage());
}
} }
/** /**