diff --git a/assets/saves/oct26-2/best-calculator-2.json b/assets/saves/oct26-2/best-calculator-2.json new file mode 100644 index 0000000..5dbd566 --- /dev/null +++ b/assets/saves/oct26-2/best-calculator-2.json @@ -0,0 +1 @@ +{"isAddBias":true,"inputs":[{"id":0,"x":0.1,"incoming":[]},{"id":1,"x":0.1,"incoming":[]},{"id":2,"x":0.1,"incoming":[]},{"id":3,"x":0.1,"incoming":[]},{"id":4,"x":0.1,"incoming":[]},{"id":5,"x":0.1,"incoming":[]},{"id":6,"x":0.1,"incoming":[]},{"id":7,"x":0.1,"incoming":[]},{"id":8,"x":0.1,"incoming":[]}],"hidden":[],"outputs":[{"id":9,"x":0.9,"incoming":[{"fromId":0,"weight":1.8324667,"enabled":true},{"fromId":1,"weight":3.3833725,"enabled":true},{"fromId":2,"weight":-0.054615706,"enabled":true},{"fromId":3,"weight":-1.6720431,"enabled":true},{"fromId":4,"weight":0.10172549,"enabled":true},{"fromId":5,"weight":0.020776946,"enabled":true},{"fromId":6,"weight":-2.0471458,"enabled":true},{"fromId":7,"weight":-1.5629632,"enabled":true},{"fromId":8,"weight":0.13760364,"enabled":true}]},{"id":10,"x":0.9,"incoming":[{"fromId":0,"weight":0.85197365,"enabled":true},{"fromId":1,"weight":0.9681164,"enabled":true},{"fromId":2,"weight":0.29334953,"enabled":true},{"fromId":3,"weight":-0.54721165,"enabled":true},{"fromId":4,"weight":0.027339846,"enabled":true},{"fromId":5,"weight":1.4024067,"enabled":true},{"fromId":6,"weight":1.2905818,"enabled":true},{"fromId":7,"weight":-1.8324815,"enabled":true},{"fromId":8,"weight":-0.5099077,"enabled":true}]},{"id":11,"x":0.9,"incoming":[{"fromId":0,"weight":-0.50607824,"enabled":true},{"fromId":1,"weight":-1.0208844,"enabled":true},{"fromId":2,"weight":0.44424167,"enabled":true},{"fromId":3,"weight":-1.2910391,"enabled":true},{"fromId":4,"weight":0.034571037,"enabled":true},{"fromId":5,"weight":1.2249593,"enabled":true},{"fromId":6,"weight":-0.15091224,"enabled":true},{"fromId":7,"weight":0.64671206,"enabled":true},{"fromId":8,"weight":-0.106307045,"enabled":true}]},{"id":12,"x":0.9,"incoming":[{"fromId":0,"weight":-0.4848314,"enabled":true},{"fromId":1,"weight":-0.02907367,"enabled":true},{"fromId":2,"weight":0.04155484,"enabled":true},{"fromId":3,"weight":0.7356783,"enabled":true},{"fromId":4,"weight":-0.65222055,"enabled":true},{"fromId":5,"weight":1.0584612,"enabled":true},{"fromId":6,"weight":-1.0657367,"enabled":true},{"fromId":7,"weight":-1.2688935,"enabled":true},{"fromId":8,"weight":-0.32647762,"enabled":true}]}]} \ No newline at end of file diff --git a/assets/saves/oct26-8/best-calculator-50.json b/assets/saves/oct26-8/best-calculator-50.json new file mode 100644 index 0000000..a7e8c79 --- /dev/null +++ b/assets/saves/oct26-8/best-calculator-50.json @@ -0,0 +1 @@ +{"isAddBias":true,"inputs":[{"id":0,"x":0.1,"incoming":[]},{"id":1,"x":0.1,"incoming":[]},{"id":2,"x":0.1,"incoming":[]},{"id":3,"x":0.1,"incoming":[]},{"id":4,"x":0.1,"incoming":[]},{"id":5,"x":0.1,"incoming":[]},{"id":6,"x":0.1,"incoming":[]},{"id":7,"x":0.1,"incoming":[]},{"id":8,"x":0.1,"incoming":[]},{"id":9,"x":0.1,"incoming":[]},{"id":10,"x":0.1,"incoming":[]},{"id":11,"x":0.1,"incoming":[]},{"id":12,"x":0.1,"incoming":[]}],"hidden":[{"id":52,"x":0.29999998,"incoming":[{"fromId":7,"weight":0.55496633,"enabled":true},{"fromId":0,"weight":-2.228508,"enabled":true}]},{"id":73,"x":0.29999998,"incoming":[{"fromId":6,"weight":0.48558712,"enabled":true},{"fromId":0,"weight":0.4345972,"enabled":true}]},{"id":19,"x":0.49999997,"incoming":[{"fromId":7,"weight":1.0295924,"enabled":false},{"fromId":0,"weight":2.6991332,"enabled":true},{"fromId":52,"weight":0.30984354,"enabled":true}]},{"id":30,"x":0.49999997,"incoming":[{"fromId":8,"weight":0.02581057,"enabled":true},{"fromId":0,"weight":-0.5465857,"enabled":true},{"fromId":11,"weight":0.46978152,"enabled":true},{"fromId":9,"weight":-0.007936746,"enabled":true}]},{"id":37,"x":0.49999997,"incoming":[{"fromId":6,"weight":0.12942661,"enabled":true},{"fromId":0,"weight":1.7212927,"enabled":true},{"fromId":11,"weight":0.21053489,"enabled":true},{"fromId":1,"weight":1.7341816,"enabled":true},{"fromId":73,"weight":1.3575654,"enabled":true},{"fromId":9,"weight":-0.24852258,"enabled":true}]},{"id":46,"x":0.49999997,"incoming":[{"fromId":6,"weight":-0.6546517,"enabled":true},{"fromId":0,"weight":-0.20992944,"enabled":true}]},{"id":77,"x":0.49999997,"incoming":[{"fromId":4,"weight":-0.21262969,"enabled":true},{"fromId":0,"weight":0.6063913,"enabled":true}]},{"id":84,"x":0.7,"incoming":[{"fromId":19,"weight":2.1982403,"enabled":true},{"fromId":0,"weight":0.38986376,"enabled":true}]},{"id":85,"x":0.7,"incoming":[{"fromId":77,"weight":0.42196923,"enabled":true},{"fromId":0,"weight":-0.2050695,"enabled":true}]}],"outputs":[{"id":13,"x":0.9,"incoming":[{"fromId":0,"weight":-0.86397016,"enabled":true},{"fromId":1,"weight":0.16719003,"enabled":true},{"fromId":2,"weight":-1.3761001,"enabled":true},{"fromId":3,"weight":1.1805954,"enabled":true},{"fromId":4,"weight":-0.6630107,"enabled":true},{"fromId":5,"weight":5.701949,"enabled":true},{"fromId":6,"weight":0.44960445,"enabled":true},{"fromId":7,"weight":0.021090955,"enabled":true},{"fromId":8,"weight":-0.48344734,"enabled":true},{"fromId":30,"weight":-0.35903573,"enabled":true},{"fromId":37,"weight":1.3333621,"enabled":true},{"fromId":19,"weight":-0.32492244,"enabled":false},{"fromId":9,"weight":1.0683477,"enabled":true},{"fromId":77,"weight":0.14141536,"enabled":true},{"fromId":84,"weight":0.8991051,"enabled":true},{"fromId":85,"weight":-0.6737307,"enabled":true}]},{"id":14,"x":0.9,"incoming":[{"fromId":0,"weight":0.9966605,"enabled":true},{"fromId":1,"weight":0.98815656,"enabled":true},{"fromId":2,"weight":-0.14601178,"enabled":true},{"fromId":3,"weight":-2.0109222,"enabled":true},{"fromId":4,"weight":0.67859244,"enabled":true},{"fromId":5,"weight":-0.28100336,"enabled":true},{"fromId":6,"weight":0.03473513,"enabled":true},{"fromId":7,"weight":0.6009386,"enabled":true},{"fromId":8,"weight":0.35581443,"enabled":true},{"fromId":12,"weight":-0.078998834,"enabled":true},{"fromId":10,"weight":0.2730469,"enabled":true},{"fromId":30,"weight":0.6095357,"enabled":true},{"fromId":19,"weight":-0.65000033,"enabled":true},{"fromId":9,"weight":0.5810351,"enabled":true},{"fromId":37,"weight":0.451707,"enabled":true}]},{"id":15,"x":0.9,"incoming":[{"fromId":0,"weight":-1.0039276,"enabled":true},{"fromId":1,"weight":0.42218173,"enabled":true},{"fromId":2,"weight":-0.05447316,"enabled":true},{"fromId":3,"weight":1.2558259,"enabled":true},{"fromId":4,"weight":0.7104747,"enabled":true},{"fromId":5,"weight":-1.2626678,"enabled":true},{"fromId":6,"weight":-0.7866507,"enabled":true},{"fromId":7,"weight":1.1627291,"enabled":true},{"fromId":8,"weight":-1.5199153,"enabled":true},{"fromId":19,"weight":1.1756326,"enabled":true},{"fromId":46,"weight":0.4242193,"enabled":true},{"fromId":9,"weight":0.0042515565,"enabled":true}]},{"id":16,"x":0.9,"incoming":[{"fromId":0,"weight":1.1559062,"enabled":true},{"fromId":1,"weight":0.53096974,"enabled":true},{"fromId":2,"weight":-0.26391035,"enabled":true},{"fromId":3,"weight":0.66762894,"enabled":true},{"fromId":4,"weight":-2.4051938,"enabled":true},{"fromId":5,"weight":-1.5032532,"enabled":true},{"fromId":6,"weight":-0.16034475,"enabled":true},{"fromId":7,"weight":-0.40630278,"enabled":true},{"fromId":8,"weight":0.37963352,"enabled":true},{"fromId":10,"weight":0.5831176,"enabled":true}]}]} \ No newline at end of file diff --git a/core/src/main/java/com/buaisociety/pacman/Main.java b/core/src/main/java/com/buaisociety/pacman/Main.java index 29113e7..d5a0f8e 100644 --- a/core/src/main/java/com/buaisociety/pacman/Main.java +++ b/core/src/main/java/com/buaisociety/pacman/Main.java @@ -103,9 +103,9 @@ public void create() { public @NotNull Neat createNeat() { // Change this to true/false as needed, if you want to load from file - if (false) { + if (true) { // TODO: Change this to the exact file you want to load - File exactFile = new File("saves" + File.separator + "oct26-4" + File.separator + "generation-51.json"); + File exactFile = new File("saves" + File.separator + "oct26-2" + File.separator + "generation-2.json"); // load exactFile contents to string String json; try { @@ -115,7 +115,7 @@ public void create() { } NeatImpl impl = NeatImpl.fromJson(json); // modify this as needed - //impl.updateNodeCounts(8, 4); // Add 4 new inputs + impl.updateNodeCounts(12, 4); // Add 4 new inputs //impl.updateClients(200); // have 200 pacman games at once return impl; } else { @@ -125,7 +125,7 @@ public void create() { neatParameters.setTargetClientsPerSpecies(12); // targeting ~12 clients per species neatParameters.setStagnationLimit(10); // lower stagnation limit neatParameters.setUseBiasNode(true); // use bias node - return new NeatImpl(4, 4, totalGames, neatParameters); + return new NeatImpl(8, 4, totalGames, neatParameters); } } diff --git a/core/src/main/java/com/buaisociety/pacman/SpecialTrainingConditions.java b/core/src/main/java/com/buaisociety/pacman/SpecialTrainingConditions.java index d9d5684..61a6834 100644 --- a/core/src/main/java/com/buaisociety/pacman/SpecialTrainingConditions.java +++ b/core/src/main/java/com/buaisociety/pacman/SpecialTrainingConditions.java @@ -28,10 +28,15 @@ private SpecialTrainingConditions() { public static @NotNull EventListener onEntityPreSpawn() { return event -> { - // Prevent ghosts from spawning during training - if (event.getEntityType() == EntityType.GHOST) { + // Only 1 ghost + if (event.getEntityType() == EntityType.GHOST && event.getMaze().getEntities().stream().anyMatch(e -> e.getType() == EntityType.GHOST)) { event.setCancelled(true); } + +// // Prevent all ghosts from spawning during training +// if (event.getEntityType() == EntityType.GHOST) { +// event.setCancelled(true); +// } }; } diff --git a/core/src/main/java/com/buaisociety/pacman/Tournament.java b/core/src/main/java/com/buaisociety/pacman/Tournament.java index 697fa74..1c652a6 100644 --- a/core/src/main/java/com/buaisociety/pacman/Tournament.java +++ b/core/src/main/java/com/buaisociety/pacman/Tournament.java @@ -47,7 +47,7 @@ public class Tournament extends ApplicationAdapter { */ public Behavior setupBehavior() { // TODO: Choose your best client here - File file = new File("saves" + File.separator + "oct20-4" + File.separator + "best-calculator-127.json"); + File file = new File("saves" + File.separator + "oct26-8" + File.separator + "best-calculator-50.json"); if (!file.exists()) { System.err.println("Could not find the file: " + file.getAbsolutePath()); return null; diff --git a/core/src/main/java/com/buaisociety/pacman/entity/PacmanEntity.java b/core/src/main/java/com/buaisociety/pacman/entity/PacmanEntity.java index f09755c..71f976e 100644 --- a/core/src/main/java/com/buaisociety/pacman/entity/PacmanEntity.java +++ b/core/src/main/java/com/buaisociety/pacman/entity/PacmanEntity.java @@ -5,10 +5,9 @@ import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.buaisociety.pacman.entity.behavior.AggressiveChaseBehavior; import com.buaisociety.pacman.entity.behavior.Behavior; -import com.buaisociety.pacman.maze.Maze; -import com.buaisociety.pacman.maze.Tile; -import com.buaisociety.pacman.maze.TileState; +import com.buaisociety.pacman.maze.*; import com.buaisociety.pacman.sprite.GrayscaleSpriteSheet; +import kotlin.Pair; import org.jetbrains.annotations.NotNull; import org.joml.Vector2d; import org.joml.Vector2i; @@ -23,6 +22,10 @@ public class PacmanEntity extends Entity { private int freezeTicks; private boolean isAlive = true; + // For graph traversal: the maze graph and the shortest pathfinder + private final MazeGraph graph; + private final ShortestPathFinder pathFinder; + public PacmanEntity(@NotNull Maze maze, @NotNull Config config) { super(maze, EntityType.PACMAN); @@ -33,6 +36,36 @@ public PacmanEntity(@NotNull Maze maze, @NotNull Config config) { // This sprite sheet is 3x4 tiled sprite sheet, each tile is 20x20 pixels this.spriteSheet = config.spriteSheet; this.spriteSheet.setColors(Color.CLEAR, Color.YELLOW); + + this.graph = new MazeGraph(maze); + this.pathFinder = new ShortestPathFinder(graph); + } + + public int getDistanceToNearestPellet(Direction direction) { + Tile startTile = maze.getTile(getTilePosition()); + Tile neighborTile = startTile.getNeighbor(direction); + if (!neighborTile.getState().isPassable()) { + return Integer.MAX_VALUE; + } + return pathFinder.getDistanceToNearestPellet(neighborTile); + } + + public int getDistanceToNearestGhost(Direction direction) { + Tile startTile = maze.getTile(getTilePosition()); + Tile neighborTile = startTile.getNeighbor(direction); + if (!neighborTile.getState().isPassable()) { + return Integer.MAX_VALUE; + } + return pathFinder.getDistanceToNearestGhost(neighborTile); + } + + public boolean dfsCheckForGhost(Direction direction) { + Tile startTile = maze.getTile(getTilePosition()); + Tile neighborTile = startTile.getNeighbor(direction); + if (!neighborTile.getState().isPassable()) { + return false; + } + return pathFinder.dfsCheckForGhost(startTile, neighborTile); } @Override @@ -75,6 +108,15 @@ public double getSpeed() { } } + public Pair getDistanceAndDirectionToNearestPelletAndGhost() { + Tile startTile = maze.getTile(getTilePosition()); + return pathFinder.getDistanceAndDirectionToNearestPowerPellet(startTile); + } + + public boolean isInSuperMode() { + return maze.getFrightenedTimer() > 0; + } + @Override public @NotNull Behavior getBehavior() { return behavior; diff --git a/core/src/main/java/com/buaisociety/pacman/entity/behavior/NeatPacmanBehavior.java b/core/src/main/java/com/buaisociety/pacman/entity/behavior/NeatPacmanBehavior.java index d3fd62f..eff4cdb 100644 --- a/core/src/main/java/com/buaisociety/pacman/entity/behavior/NeatPacmanBehavior.java +++ b/core/src/main/java/com/buaisociety/pacman/entity/behavior/NeatPacmanBehavior.java @@ -3,13 +3,16 @@ import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.buaisociety.pacman.maze.Maze; +import com.buaisociety.pacman.maze.Tile; import com.buaisociety.pacman.sprite.DebugDrawing; import com.cjcrafter.neat.Client; import com.buaisociety.pacman.entity.Direction; import com.buaisociety.pacman.entity.Entity; import com.buaisociety.pacman.entity.PacmanEntity; +import kotlin.Pair; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.joml.Vector2i; public class NeatPacmanBehavior implements Behavior { @@ -21,6 +24,9 @@ public class NeatPacmanBehavior implements Behavior { // specific pools of points instead of subtracting from all. private int scoreModifier = 0; + private int numberUpdatesSincelastscone = 0; + private int lastScore = 0; + public NeatPacmanBehavior(@NotNull Client client) { this.client = client; } @@ -42,6 +48,19 @@ public Direction getDirection(@NotNull Entity entity) { // TODO: Make changes here to help with your training... // END OF SPECIAL TRAINING CONDITIONS + // If pacman got stuck (not collecting anything anymore), kill it + int newScore = pacman.getMaze().getLevelManager().getScore(); + if (newScore > lastScore) { + lastScore = newScore; + numberUpdatesSincelastscone = 0; + } + + // 60 updates per seconds * 10 seconds + if (numberUpdatesSincelastscone++ > 60 * 10) { + pacman.kill(); + return Direction.UP; + } + // We are going to use these directions a lot for different inputs. Get them all once for clarity and brevity Direction forward = pacman.getDirection(); Direction left = pacman.getDirection().left(); @@ -54,13 +73,92 @@ public Direction getDirection(@NotNull Entity entity) { boolean canMoveRight = pacman.canMove(right); boolean canMoveBehind = pacman.canMove(behind); + // Nearest distance to Pallet: + int distanceToNearestPelletForward = pacman.getDistanceToNearestPellet(forward); + int distanceToNearestPelletLeft = pacman.getDistanceToNearestPellet(left); + int distanceToNearestPelletRight = pacman.getDistanceToNearestPellet(right); + int distanceToNearestPelletBehind = pacman.getDistanceToNearestPellet(behind); + + // Nearest distance to ghost: + int distanceToGhostForward = pacman.getDistanceToNearestGhost(forward); + int distanceToGhostLeft = pacman.getDistanceToNearestGhost(left); + int distanceToGhostRight = pacman.getDistanceToNearestGhost(right); + int distanceToGhostBehind = pacman.getDistanceToNearestGhost(behind); + + // One hot encoding for the direction of the closest power pellet + int minDistance = Math.min(distanceToNearestPelletForward, Math.min(distanceToNearestPelletLeft, Math.min(distanceToNearestPelletRight, distanceToNearestPelletBehind))); + boolean closestPalletIsForward = distanceToNearestPelletForward == minDistance; + boolean closestPalletIsLeft = distanceToNearestPelletLeft == minDistance; + boolean closestPalletIsRight = distanceToNearestPelletRight == minDistance; + boolean closestPalletIsBehind = distanceToNearestPelletBehind == minDistance; + + // Get the fruit position + Vector2i fruitPosition = pacman.getMaze().getFruitPosition(); + if (fruitPosition != null) { + // Calculate direction to the fruit + Tile pacmanTile = pacman.getMaze().getTile(pacman.getTilePosition()); + Tile fruitTile = pacman.getMaze().getTile(fruitPosition); + Direction directionToFruit = pacmanTile.getDirectionTo(fruitTile); + + // Use the direction to the fruit in your NEAT algorithm inputs + boolean fruitIsForward = directionToFruit == pacman.getDirection(); + boolean fruitIsLeft = directionToFruit == pacman.getDirection().left(); + boolean fruitIsRight = directionToFruit == pacman.getDirection().right(); + boolean fruitIsBehind = directionToFruit == pacman.getDirection().behind(); + } + + boolean ghostLeft = pacman.dfsCheckForGhost(left); + boolean ghostRight = pacman.dfsCheckForGhost(right); + boolean ghostForward = pacman.dfsCheckForGhost(forward); + boolean ghostBehind = pacman.dfsCheckForGhost(behind); + + // Get the current score and number of pellets left + int pelletsLeft = pacman.getMaze().getPelletsRemaining(); + + // Check if Pacman should eat a PowerPellet + boolean shouldEatPowerPellet = (distanceToGhostForward < 5 || distanceToGhostLeft < 5 || distanceToGhostRight < 5 || distanceToGhostBehind < 5); + // Get closest power pellet direction and distance + Pair closestPowerPellet = pacman.getDistanceAndDirectionToNearestPelletAndGhost(); + int distanceToClosestPowerPellet = closestPowerPellet.getFirst(); + Direction directionToClosestPowerPellet = closestPowerPellet.getSecond(); + boolean closestPowerPelletIsForward = directionToClosestPowerPellet == forward; + boolean closestPowerPelletIsLeft = directionToClosestPowerPellet == left; + boolean closestPowerPelletIsRight = directionToClosestPowerPellet == right; + boolean closestPowerPelletIsBehind = directionToClosestPowerPellet == behind; + boolean isInSuperMode = pacman.isInSuperMode(); + float[] outputs = client.getCalculator().calculate(new float[]{ canMoveForward ? 1f : 0f, canMoveLeft ? 1f : 0f, canMoveRight ? 1f : 0f, canMoveBehind ? 1f : 0f, + closestPalletIsForward ? 1f : 0f, + closestPalletIsLeft ? 1f : 0f, + closestPalletIsRight ? 1f : 0f, + closestPalletIsBehind ? 1f : 0f, + ghostLeft ? 1f : 0f, + ghostRight ? 1f : 0f, + ghostForward ? 1f : 0f, + ghostBehind ? 1f : 0f, }).join(); + // closestPalletIsForward ? 1f : 0f, + // closestPalletIsLeft ? 1f : 0f, + // closestPalletIsRight ? 1f : 0f, + // closestPalletIsBehind ? 1f : 0f, + +// distanceToGhostForward, +// distanceToGhostLeft, +// distanceToGhostRight, +// distanceToGhostBehind, +// shouldEatPowerPellet ? 1f : 0f, +// distanceToClosestPowerPellet, +// closestPowerPelletIsForward ? 1f : 0f, +// closestPowerPelletIsLeft ? 1f : 0f, +// closestPowerPelletIsRight ? 1f : 0f, +// closestPowerPelletIsBehind ? 1f : 0f, +// isInSuperMode ? 1f : 0f, + int index = 0; float max = outputs[0]; for (int i = 1; i < outputs.length; i++) { diff --git a/core/src/main/java/com/buaisociety/pacman/entity/behavior/TournamentBehavior.java b/core/src/main/java/com/buaisociety/pacman/entity/behavior/TournamentBehavior.java index 22cab50..0ac5c40 100644 --- a/core/src/main/java/com/buaisociety/pacman/entity/behavior/TournamentBehavior.java +++ b/core/src/main/java/com/buaisociety/pacman/entity/behavior/TournamentBehavior.java @@ -15,6 +15,9 @@ public class TournamentBehavior implements Behavior { private int previousScore = 0; private int framesSinceScoreUpdate = 0; + private int numberUpdatesSincelastscone = 0; + private int lastScore = 0; + public TournamentBehavior(Calculator calculator) { this.calculator = calculator; } @@ -49,8 +52,50 @@ public Direction getDirection(@NotNull Entity entity) { // TODO: Put all your code for info into the neural network here + // We are going to use these directions a lot for different inputs. Get them all once for clarity and brevity + Direction forward = pacman.getDirection(); + Direction left = pacman.getDirection().left(); + Direction right = pacman.getDirection().right(); + Direction behind = pacman.getDirection().behind(); + + // Input nodes 1, 2, 3, and 4 show if the pacman can move in the forward, left, right, and behind directions + boolean canMoveForward = pacman.canMove(forward); + boolean canMoveLeft = pacman.canMove(left); + boolean canMoveRight = pacman.canMove(right); + boolean canMoveBehind = pacman.canMove(behind); + + // Nearest distance to Pallet: + int distanceToNearestPelletForward = pacman.getDistanceToNearestPellet(forward); + int distanceToNearestPelletLeft = pacman.getDistanceToNearestPellet(left); + int distanceToNearestPelletRight = pacman.getDistanceToNearestPellet(right); + int distanceToNearestPelletBehind = pacman.getDistanceToNearestPellet(behind); + + // One hot encoding for the direction of the closest power pellet + int minDistance = Math.min(distanceToNearestPelletForward, Math.min(distanceToNearestPelletLeft, Math.min(distanceToNearestPelletRight, distanceToNearestPelletBehind))); + boolean closestPalletIsForward = distanceToNearestPelletForward == minDistance; + boolean closestPalletIsLeft = distanceToNearestPelletLeft == minDistance; + boolean closestPalletIsRight = distanceToNearestPelletRight == minDistance; + boolean closestPalletIsBehind = distanceToNearestPelletBehind == minDistance; + + boolean ghostLeft = pacman.dfsCheckForGhost(left); + boolean ghostRight = pacman.dfsCheckForGhost(right); + boolean ghostForward = pacman.dfsCheckForGhost(forward); + boolean ghostBehind = pacman.dfsCheckForGhost(behind); + float[] inputs = new float[] { // TODO: Add your inputs here + canMoveForward ? 1f : 0f, + canMoveLeft ? 1f : 0f, + canMoveRight ? 1f : 0f, + canMoveBehind ? 1f : 0f, + closestPalletIsForward ? 1f : 0f, + closestPalletIsLeft ? 1f : 0f, + closestPalletIsRight ? 1f : 0f, + closestPalletIsBehind ? 1f : 0f, + ghostLeft ? 1f : 0f, + ghostRight ? 1f : 0f, + ghostForward ? 1f : 0f, + ghostBehind ? 1f : 0f, }; float[] outputs = calculator.calculate(inputs).join(); diff --git a/core/src/main/java/com/buaisociety/pacman/maze/Maze.java b/core/src/main/java/com/buaisociety/pacman/maze/Maze.java index 1e31fb5..8e960a9 100644 --- a/core/src/main/java/com/buaisociety/pacman/maze/Maze.java +++ b/core/src/main/java/com/buaisociety/pacman/maze/Maze.java @@ -414,6 +414,25 @@ public void spawnFruit() { entities.add(fruit); } + public boolean isFruitPresent() { + for (Entity entity : entities) { + if (entity instanceof FruitEntity) { + return true; + } + } + return false; + } + + // Maze.java + public @Nullable Vector2i getFruitPosition() { + for (Entity entity : entities) { + if (entity instanceof FruitEntity) { + return entity.getTilePosition(); + } + } + return null; + } + public void update() { ticks++; if (freezeTicks > 0) { diff --git a/core/src/main/java/com/buaisociety/pacman/maze/MazeGraph.java b/core/src/main/java/com/buaisociety/pacman/maze/MazeGraph.java new file mode 100644 index 0000000..19e2778 --- /dev/null +++ b/core/src/main/java/com/buaisociety/pacman/maze/MazeGraph.java @@ -0,0 +1,31 @@ +package com.buaisociety.pacman.maze; + +import com.buaisociety.pacman.entity.Direction; + +import java.util.*; + +public class MazeGraph { + private final Map> adjList = new HashMap<>(); + + public MazeGraph(Maze maze) { + for (int y = 0; y < maze.getDimensions().y(); y++) { + for (int x = 0; x < maze.getDimensions().x(); x++) { + Tile tile = maze.getTile(x, y); + if (tile.getState().isPassable()) { + List neighbors = new ArrayList<>(); + for (Direction direction : Direction.values()) { + Tile neighbor = tile.getNeighbor(direction); + if (neighbor.getState().isPassable()) { + neighbors.add(neighbor); + } + } + adjList.put(tile, neighbors); + } + } + } + } + + public Map> getAdjList() { + return adjList; + } +} diff --git a/core/src/main/java/com/buaisociety/pacman/maze/ShortestPathFinder.java b/core/src/main/java/com/buaisociety/pacman/maze/ShortestPathFinder.java new file mode 100644 index 0000000..22585a3 --- /dev/null +++ b/core/src/main/java/com/buaisociety/pacman/maze/ShortestPathFinder.java @@ -0,0 +1,207 @@ +// ShortestPathFinder.java +package com.buaisociety.pacman.maze; + +import com.buaisociety.pacman.entity.Direction; +import com.buaisociety.pacman.entity.Entity; +import com.buaisociety.pacman.entity.EntityType; +import com.buaisociety.pacman.entity.GhostEntity; +import kotlin.Pair; + +import java.util.*; + +public class ShortestPathFinder { + private final MazeGraph mazeGraph; + + + public ShortestPathFinder(MazeGraph mazeGraph) { + this.mazeGraph = mazeGraph; + } + + public int getDistanceToNearestPellet(Tile startTile) { + Queue queue = new LinkedList<>(); + Set visited = new HashSet<>(); + Map distance = new HashMap<>(); + + queue.add(startTile); + visited.add(startTile); + distance.put(startTile, 0); + + while (!queue.isEmpty()) { + Tile current = queue.poll(); + int currentDistance = distance.get(current); + + if (current.getState() == TileState.PELLET || current.getState() == TileState.POWER_PELLET) { + return currentDistance; + } + + for (Tile neighbor : mazeGraph.getAdjList().get(current)) { + if (!visited.contains(neighbor)) { + queue.add(neighbor); + visited.add(neighbor); + distance.put(neighbor, currentDistance + 1); + } + } + } + + return Integer.MAX_VALUE; // No pellet found + } + + public int getDistanceToNearestGhost(Tile startTile) { + Queue queue = new LinkedList<>(); + Set visited = new HashSet<>(); + Map distance = new HashMap<>(); + + queue.add(startTile); + visited.add(startTile); + distance.put(startTile, 0); + + while (!queue.isEmpty()) { + Tile current = queue.poll(); + int currentDistance = distance.get(current); + + // Check if the current tile has a ghost + for (Entity entity : current.getMaze().getEntities()) { + if (entity instanceof GhostEntity && entity.getTilePosition().equals(current.getPosition())) { + return currentDistance; + } + } + + for (Tile neighbor : mazeGraph.getAdjList().get(current)) { + if (!visited.contains(neighbor)) { + queue.add(neighbor); + visited.add(neighbor); + distance.put(neighbor, currentDistance + 1); + } + } + } + + return Integer.MAX_VALUE; // No ghost found + } + + private List getAdjacentTiles(Tile startTile) { + return new ArrayList<>(mazeGraph.getAdjList().get(startTile)); + } + + public Pair getDistanceAndDirectionToNearestPowerPellet(Tile startTile) { + Queue queue = new LinkedList<>(); + Set visited = new HashSet<>(); + Map distance = new HashMap<>(); + Map directionMap = new HashMap<>(); + queue.add(startTile); + visited.add(startTile); + distance.put(startTile, 0); + for (Direction direction : Direction.values()) { + Tile neighbor = startTile.getNeighbor(direction); + if (neighbor.getState().isPassable()) { + queue.add(neighbor); + visited.add(neighbor); + distance.put(neighbor, 1); + directionMap.put(neighbor, direction); + } + } + while (!queue.isEmpty()) { + Tile current = queue.poll(); + int currentDistance = distance.get(current); + if (current.getState() == TileState.POWER_PELLET) { + Direction initialDirection = directionMap.get(current); + return new Pair<>(currentDistance, initialDirection); + } + for (Tile neighbor : getAdjacentTiles(current)) { + if (!visited.contains(neighbor) && neighbor.getState().isPassable()) { + queue.add(neighbor); + visited.add(neighbor); + distance.put(neighbor, currentDistance + 1); + directionMap.put(neighbor, directionMap.getOrDefault(current, null)); + } + } + } + return new Pair<>(Integer.MAX_VALUE, null); // No PowerPellet found + } + + public boolean dfsCheckForGhost(Tile startTile, Tile directionTile) { + Set visited = new HashSet<>(); + visited.add(startTile); // Mark Pacman's current position as visited + Tile current = directionTile; // Start from the tile in the specified direction + + int counter = 0; + + // Traverse through the hallway until a root is found + while (getUnvisitedNeighborCount(current, visited) == 1) { + counter++; + // Check for a ghost immediately upon visiting each tile + if (containsGhost(current)) { + System.out.printf("Ghost found at %s\n", current.getPosition()); + return true; + } + + // Move to the next tile in the hallway + for (Tile neighbor : mazeGraph.getAdjList().get(current)) { + if (!visited.contains(neighbor)) { + visited.add(neighbor); + current = neighbor; + break; + } + } + } + + if (counter > 8) { + return false; // No ghost found within 8 tiles + } + + // Check the current tile (root) for a ghost + if (containsGhost(current)) { + System.out.printf("Ghost found at %s\n", current.getPosition()); + return true; + } + + // Start a countdown when a root is reached + int countdown = 2; + Stack stack = new Stack<>(); + stack.push(current); + + while (!stack.isEmpty() && countdown > 0) { + Tile tile = stack.pop(); + visited.add(tile); + + // Check for a ghost on every visited tile during countdown + if (containsGhost(tile)) { + System.out.printf("Ghost found at %s\n", tile.getPosition()); + return true; + } + + // Add unvisited neighbors to the stack + for (Tile neighbor : mazeGraph.getAdjList().get(tile)) { + if (!visited.contains(neighbor)) { + stack.push(neighbor); + visited.add(neighbor); + } + } + + // Decrease the countdown after expanding one depth level + countdown--; + } + + return false; // No ghost found within two tiles from the root + } + + + + private boolean containsGhost(Tile t){ + for (Entity entity : t.getMaze().getEntities()) { + if (entity instanceof GhostEntity && entity.getTilePosition().equals(t.getPosition())) { + return true; + } + } + return false; + } + + private int getUnvisitedNeighborCount(Tile tile, Set visited) { + int count = 0; + for (Tile neighbor : mazeGraph.getAdjList().get(tile)) { + if (!visited.contains(neighbor)) { + count++; + } + } + return count; + } +} diff --git a/core/src/main/java/com/buaisociety/pacman/maze/Tile.java b/core/src/main/java/com/buaisociety/pacman/maze/Tile.java index 1de2ede..a49babd 100644 --- a/core/src/main/java/com/buaisociety/pacman/maze/Tile.java +++ b/core/src/main/java/com/buaisociety/pacman/maze/Tile.java @@ -96,6 +96,17 @@ public boolean equals(Object o) { return Objects.equals(maze, tile.maze) && Objects.equals(position, tile.position); } + public @NotNull Direction getDirectionTo(@NotNull Tile targetTile) { + int dx = targetTile.getPosition().x() - this.position.x; + int dy = targetTile.getPosition().y() - this.position.y; + + if (Math.abs(dx) > Math.abs(dy)) { + return dx > 0 ? Direction.RIGHT : Direction.LEFT; + } else { + return dy > 0 ? Direction.UP : Direction.DOWN; + } + } + @Override public int hashCode() { return Objects.hash(maze, position);