From 6a01dc315b38a047d202b3924be5a84321db8226 Mon Sep 17 00:00:00 2001
From: mithe24
+ * This class provides methods to parse a JSON configuration and convert it into
+ * an instance of {@code GameConfig}, handling all necessary conversions and validations.
+ */
+public class ConfigBuilder {
+
+ /**
+ * Parses a JSON configuration file from an {@link InputStream},
+ * and returns a {@link GameConfig} object.
+ *
+ * @param inputStream the input stream pointing to a valid JSON configuration file.
+ * @return the constructed {@code GameConfig} instance.
+ * @throws RuntimeException if the JSON is malformed
+ * or if an I/O error occurs during reading.
+ */
+ public static GameConfig fromJson(InputStream inputStream) {
+ /* Notice the reader gets closed by
+ * the try-with-resource statement */
+ try (InputStreamReader reader = new InputStreamReader(inputStream)) {
+ JSONTokener tokener = new JSONTokener(reader);
+ JSONObject jsonObject = new JSONObject(tokener);
+ return buildFromJsonObject(jsonObject);
+ } catch (JSONException jsonException) {
+ throw new RuntimeException("""
+ Failed to parse JSON file, ill formated JSON file.""", jsonException);
+ } catch (IOException ioException) {
+ throw new RuntimeException("""
+ Error reading JSON file.
+ Ensure the file exists and is accessible""", ioException);
+ }
+ }
+
+ /**
+ * Constructs a {@link GameConfig} object from a parsed {@link JSONObject}.
+ *
+ * @param jsonObject the JSON object containing all required game configuration fields.
+ * @return the constructed {@code GameConfig} instance.
+ * @throws RuntimeException if required fields are missing or have invalid types.
+ */
+ private static GameConfig buildFromJsonObject(JSONObject jsonObject) {
+ try {
+ TileType[][] initialBoard = parseBoard(jsonObject.getJSONArray("board"));
+
+ /* Settings */
+ int lives = jsonObject.getInt("lives");
+ double powerModeDuration = jsonObject.getDouble("powerModeDuration");
+ double ghostSpeed = jsonObject.getDouble("ghostSpeed");
+ int numberOfItems = jsonObject.getInt("numberOfItems");
+
+ /* Pacman Entity */
+ JSONObject pacmanObject = jsonObject.getJSONObject("pacman");
+ double pacmanSpeed = pacmanObject.getDouble("speed");
+ Position pacmanStartPosition = new Position(pacmanObject.getInt("x"),
+ pacmanObject.getInt("y"));
+
+ /* Ghost Entities */
+ /* Red Ghost */
+ JSONObject redGhostObject = jsonObject.getJSONObject("redGhost");
+ Position redGhostStartPosition = new Position(redGhostObject.getInt("x"),
+ redGhostObject.getInt("y"));
+ /* Blue Ghost */
+ JSONObject blueGhostObject = jsonObject.getJSONObject("blueGhost");
+ Position blueGhostStartPosition = new Position(blueGhostObject.getInt("x"),
+ blueGhostObject.getInt("y"));
+
+ /* Pink Ghost */
+ JSONObject pinkGhostObject = jsonObject.getJSONObject("pinkGhost");
+ Position pinkGhostStartPosition = new Position(pinkGhostObject.getInt("x"),
+ pinkGhostObject.getInt("y"));
+
+ /* Orange Ghost */
+ JSONObject orangeGhostObject = jsonObject.getJSONObject("orangeGhost");
+ Position orangeGhostStartPosition = new Position(orangeGhostObject.getInt("x"),
+ orangeGhostObject.getInt("y"));
+
+ return new GameConfig(
+ initialBoard,
+ powerModeDuration,
+ lives,
+ ghostSpeed,
+ pacmanSpeed,
+ numberOfItems,
+ pacmanStartPosition,
+ redGhostStartPosition,
+ blueGhostStartPosition,
+ pinkGhostStartPosition,
+ orangeGhostStartPosition);
+ } catch (JSONException jsonException) {
+ throw new RuntimeException("""
+ Missing requried config fields in JSON file.""", jsonException);
+ }
+ }
+
+ /**
+ * Parses a 2D array of {@link TileType} from a {@link JSONArray} representing the board.
+ *
+ * Expected JSON tile symbols:
+ *
- * This class provides static methods to parse game data such as the board,
- * entities, and items from a JSON input stream and use it to initialize
- * a game state. It handles parsing logic and translates string representations
- * into model objects used by the game engine.
- *
- * Expected JSON structure includes:
- *
- * This method extracts the board layout, Pacman parameters,
- * and initializes the entities and items.
- *
- * @param jsonObject the JSON object containing game configuration data.
- * @return a fully constructed {@link GameState} object.
- * @throws RuntimeException if required fields are missing or incorrectly formatted.
-
- */
- private static GameState buildFromJsonObject(JSONObject jsonObject) {
- try {
- JSONArray stringBoard = jsonObject.getJSONArray("board");
- TileType[][] tileBoard = new TileType
- [stringBoard.length()]
- [stringBoard.getJSONArray(0).length()];
- for (int y = 0; y < stringBoard.length(); y++) {
- JSONArray row = stringBoard.getJSONArray(y);
- for (int x = 0; x < row.length(); x++) {
- switch (row.getString(x)) {
- case "W" -> tileBoard[y][x] = TileType.WALL;
- case "E" -> tileBoard[y][x] = TileType.EMPTY;
- default -> {}
- }
- }
- }
- Board board = new Board(tileBoard[0].length, tileBoard.length, tileBoard);
-
- JSONObject pacmanJsonObject = jsonObject.getJSONObject("pacman");
- double speed = pacmanJsonObject.getDouble("speed");
- double radius = pacmanJsonObject.getDouble("radius");
- Position pacmanStartPos = new Position(
- pacmanJsonObject.getInt("x"),
- pacmanJsonObject.getInt("y"));
- Pacman pacman = new Pacman(pacmanStartPos, speed, Direction.NONE, radius);
-
- List
- * This class stores both tile-based {@link Position}
+ * This class stores both tile-based {@link Position}
* and fine-grained sub-tile coordinates
- * to allow smooth movement within and across grid tiles.
- *
- * The full world coordinates are computed by combining
+ * The full world coordinates are computed by combining
* the integer {@code Position} with
* {@code subTileX} and {@code subTileY}, allowing sub-tile precision
- * useful for animation or collision detection.
- *
+ *
+ *
+ * @param jsonArray the JSON array representing the 2D board.
+ * @return a 2D array of {@code TileType}.
+ */
+ private static TileType[][] parseBoard(JSONArray jsonArray) {
+ assert jsonArray != null;
+
+ TileType[][] tileBoard = new TileType[jsonArray.length()][jsonArray.getJSONArray(0).length()];
+ for (int y = 0; y < tileBoard.length; y++) {
+ JSONArray row = jsonArray.getJSONArray(y);
+ for (int x = 0; x < row.length(); x++) {
+ switch (row.getString(x)) {
+ case "W" -> tileBoard[y][x] = TileType.WALL;
+ case "E" -> tileBoard[y][x] = TileType.EMPTY;
+ case "p" -> tileBoard[y][x] = TileType.PELLET;
+ case "P" -> tileBoard[y][x] = TileType.POWER_PELLET;
+ default -> {}
+ }
+ }
+ }
+
+ return tileBoard;
+ }
+}
diff --git a/pacman/model/src/main/java/com/gr15/pacman/model/GameConfig.java b/pacman/model/src/main/java/com/gr15/pacman/model/GameConfig.java
new file mode 100644
index 0000000..da6cd68
--- /dev/null
+++ b/pacman/model/src/main/java/com/gr15/pacman/model/GameConfig.java
@@ -0,0 +1,72 @@
+package com.gr15.pacman.model;
+
+import com.gr15.pacman.model.Board.TileType;
+
+/**
+ * Represents the configuration for a Pac-Man-style game.
+ * This class is immutable and ensures defensive copying of the initial game board to
+ * maintain data integrity.
+ *
+ * @param initialBoard The initial layout of the game board.
+ * @param powerModeDuration Duration (in seconds) that Pac-Man remains in
+ * power mode after consuming a power item.
+ * @param lives The number of lives {@code Pacman} starts with.
+ * @param ghostSpeed The speed of the ghosts.
+ * @param pacmanSpeed The speed of {@code Pacman}
+ * @param numberOfItems The number of collectible items on the board.
+ * @param pacmanStartPosition The starting {@link Position} of {@link Pacman}.
+ * @param redGhostStartPosition The starting {@link Position} of the red ghost.
+ * @param blueGhostStartPosition The starting {@link Position} of the blue ghost.
+ * @param pinkGhostStartPosition The starting {@link Position} of the pink ghost.
+ * @param orangeGhostStartPosition The starting {@link Position} of the orange ghost.
+ */
+public record GameConfig(
+ TileType[][] initialBoard,
+ double powerModeDuration,
+ int lives,
+ double ghostSpeed,
+ double pacmanSpeed,
+ int numberOfItems,
+
+ Position pacmanStartPosition,
+ Position redGhostStartPosition,
+ Position blueGhostStartPosition,
+ Position pinkGhostStartPosition,
+ Position orangeGhostStartPosition) {
+
+ /**
+ * Constructs a new {@code GameConfig} record.
+ * Performs a deep copy of the initial board to ensure immutability.
+ */
+ public GameConfig {
+ initialBoard = deepCopy(initialBoard);
+ }
+
+ /**
+ * Returns a deep copy of the initial board configuration.
+ * This prevents external modification of the board data.
+ *
+ * @return A deep copy of the initial board.
+ */
+ @Override
+ public TileType[][] initialBoard() {
+ return deepCopy(initialBoard);
+ }
+
+ /**
+ * Creates a deep copy of a 2D TileType array.
+ *
+ * @param arg The 2D array to copy.
+ * @return A deep copy of the provided 2D array,
+ * or {@code null} if the input is {@code null}.
+ */
+ private static TileType[][] deepCopy(TileType[][] arg) {
+ if (arg == null) { return null; }
+ TileType[][] copy = new TileType[arg.length][];
+ for (int i = 0; i < arg.length; i++) {
+ copy[i] = arg[i].clone();
+ }
+
+ return copy;
+ }
+}
diff --git a/pacman/model/src/main/java/com/gr15/pacman/model/GameState.java b/pacman/model/src/main/java/com/gr15/pacman/model/GameState.java
index 609bfc0..48c9995 100644
--- a/pacman/model/src/main/java/com/gr15/pacman/model/GameState.java
+++ b/pacman/model/src/main/java/com/gr15/pacman/model/GameState.java
@@ -1,41 +1,86 @@
package com.gr15.pacman.model;
+import java.util.ArrayList;
import java.util.List;
-import java.util.Map;
-import com.gr15.pacman.model.entities.Entity;
+import com.gr15.pacman.model.Board.TileType;
import com.gr15.pacman.model.entities.EntityUtils;
-import com.gr15.pacman.model.entities.Items;
+import com.gr15.pacman.model.entities.Ghost;
import com.gr15.pacman.model.entities.Pacman;
+import com.gr15.pacman.model.entities.Entity.Direction;
+import com.gr15.pacman.model.entities.Ghost.GhostType;
/**
- * GameState
+ * Represents the current state of the game, including the game configuration,
+ * board, entities (Pacman and ghosts), score, lives, and power mode status.
+ * This class manages game updates, entity movements, collisions, and game progress.
*/
public class GameState {
-
+
+ /** Configuration settings for the game such as initial board,
+ * speeds, and start positions. */
+ private final GameConfig config;
+
+ /** The game board containing tile information such as walls and pellets. */
private Board board;
+ /** The Pacman entity controlled by the player. */
private Pacman pacman;
- private List
- *
- *
- * @param inputStream the input stream of the JSON file.
- * @return a {@link GameState} object built from the JSON configuration.
- * @throws RuntimeException if the JSON is invalid or the file cannot be read.
- */
- public static GameState fromJson(InputStream inputStream) {
- /* Reader gets closed automatically,
- * because it's in the resources part of a try-with-resource statement.
- * The InputStream gets closed by Reader */
- try (InputStreamReader reader = new InputStreamReader(inputStream)) {
- JSONTokener tokener = new JSONTokener(reader);
- JSONObject jsonObject = new JSONObject(tokener);
- return buildFromJsonObject(jsonObject);
- } catch (JSONException jsonException) {
- throw new RuntimeException("""
- Failed to parse JSON content.
- Please ensure the file contains valid JSON.""", jsonException);
- } catch (IOException ioException) {
- throw new RuntimeException("""
- Error reading JSON file.
- Ensure the file exists and is accessible""", ioException);
- }
- }
-
- /**
- * Internal helper method that constructs a {@link GameState} object
- * from a given {@link JSONObject}.
- *
- *
- * Subclasses must implement the {@code move} method - * to define entity-specific behavior over time. - *
+ *Handles movement logic including direction changes, wall collisions, + * snapping to tile centers, and updating position on the game board.
*/ public abstract class Entity { @@ -33,6 +27,10 @@ public abstract class Entity { private double radius; + private Direction currentDirection = Direction.UP; + private Direction nextDirection = Direction.NONE; + private double speed; + public enum Direction { UP, DOWN, LEFT, RIGHT, NONE }; /** @@ -43,44 +41,211 @@ public abstract class Entity { * @throws IllegalArgumentException if any parameter is invalid: * - startPos is null * - radius is less than or equal to zero + * - speed is less than zero */ - public Entity(Position startPos, double radius) { - if (startPos == null) { throw new IllegalArgumentException("startPos cannot be null"); } - if (radius <= 0) { throw new IllegalArgumentException("radius must be positive"); } + public Entity(Position startPos, double radius, double speed) { + if (startPos == null) { + throw new IllegalArgumentException("startPos cannot be null"); + } + if (radius <= 0) { + throw new IllegalArgumentException("radius must be positive"); + } + if (speed < 0) { + throw new IllegalArgumentException("Speed must be a positive number"); + } this.radius = radius; this.position = startPos; + this.speed = speed; } /** - * Updates the entity's position based on game logic and elapsed time. + * Updates Pacman's {@link Position} based on the time elapsed + * and the game {@link Board} state. + * Handles turning, snapping to tile center, + * and tile boundary transitions. * - * @param board The board the entity is moving on. - * @param deltaSeconds The time elapsed since the last update, in seconds. + * @param board the game {@link Board}, used to determine valid movement. + * @param deltaSeconds time in seconds since the last update in seconds. + * @throws IllegalArgumentException if board is {@code null} */ - public abstract void move(Board board, double deltaSeconds); + public void move(Board board, double deltaSeconds) { + if (board == null) { + throw new IllegalArgumentException("board must not be null"); + } + double distanceToMove = speed * deltaSeconds; + boolean canMoveNext = canMove(board, position, nextDirection); + boolean canContinue = canMove(board, position, currentDirection); + + Direction direction = decideDirection(canMoveNext, canContinue); + if (direction == Direction.NONE) { + snapToCenter(distanceToMove); + return; /* Returning early, since no more updating required */ + } + + boolean verticalMove = isVertical(direction); + int directionSign = getDirectionSign(direction); + + double subPrimary = verticalMove ? subTileY : subTileX; + double subSecondary = verticalMove ? subTileX : subTileY; + + double center = Math.floor(subSecondary) + 0.5; + double distanceToCenter = Math.abs(subSecondary - center); + + /* Snap to secondary axis center first */ + if (distanceToCenter > 0.00003) { + double moveToCenter = Math.min(distanceToMove, distanceToCenter); + subSecondary += (subSecondary < center ? 1 : -1) * moveToCenter; + distanceToMove -= moveToCenter; + } + + /* Storing new secondry axis sub-tile position */ + if (verticalMove) { + subTileX = (float)subSecondary; + } else { + subTileY = (float)subSecondary; + } + + while (distanceToMove > 0.00003) { + /* Capping movement distance to a single tile */ + double maxStep = directionSign > 0 ? 1.0 - subPrimary : subPrimary; + double step = Math.min(distanceToMove, maxStep); + double newSubPrimary = subPrimary + directionSign * step; + + /* Handling crossing tile boundery */ + if (crossedTileBoundary(direction, newSubPrimary)) { + Position nextPos = position.offset(direction); + if (board.isMovable(nextPos)) { + position = nextPos; + subPrimary = directionSign > 0 ? 0.0 : 1.0; + } else { + /* If hit wall, set direction to none */ + currentDirection = Direction.NONE; + subPrimary = directionSign > 0 ? 1.0 : 0.0; + break; /* stopping at wall */ + } + } else { + subPrimary = newSubPrimary; + } + + /* If there's still longer to go */ + distanceToMove -= step; + } + + /* Storing primary axis sub-tile position */ + if (verticalMove) { + subTileY = (float)subPrimary; + } else { + subTileX = (float)subPrimary; + } + + if (canMoveNext && distanceToMove < 0.0003) { + currentDirection = nextDirection; + nextDirection = Direction.NONE; + } + } - /* Getters */ /** - * Gets the tile-based position of the entity. + * Helper function that determines if Pacman can move + * in the specified direction. * - * @return The current tile position. + * @param board the game {@link Board}. + * @param pos the current {@link Position}. + * @param dir the direction to check. + * @return true if the tile in the given {@link Direction} is movable. */ - public Position getPosition() { return this.position; } + private boolean canMove(Board board, Position pos, Direction dir) { + assert board != null && pos != null && dir != null; + return dir != Direction.NONE && board.isMovable(pos.offset(dir)); + } + + /** + * Helper function that decides the direction to move in + * based on current and next direction availability. + * + * @param canMoveNext true if the next direction is available. + * @param canContinue true if continuing in the current direction is possible. + * @return the direction Pacman should move in. + */ + private Direction decideDirection(boolean canMoveNext, boolean canContinue) { + if (canMoveNext) { return nextDirection; } + if (canContinue) { return currentDirection; } + return Direction.NONE; + } + + /** + * helper function that checks if a direction is vertical. + * + * @param dir the direction to check. + * @return true if the direction is {@link Direction.UP} or {@link Direction.DOWN}. + */ + private boolean isVertical(Direction dir) { + assert dir != null; + return dir == Direction.UP || dir == Direction.DOWN; + } + + /** + * Helper function that gets the sign of movement for a {@link Direction}. + * + * @param dir the direction. + * @return -1 for UP or LEFT and 1 for DOWN or RIGHT. + */ + private int getDirectionSign(Direction dir) { + assert dir != null; + return (dir == Direction.UP || dir == Direction.LEFT) ? -1 : 1; + } /** - * Gets the entity's sub-tile X offset. + * Helper function that determines if Pacman has crossed a tile boundary. * - * @return The sub-tile X coordinate. + * @param dir the direction of movement. + * @param newPrimary the new sub-tile coordinate in the movement axis. + * @return true if a tile boundary was crossed. */ - public float getSubTileX() { return this.subTileX; } + private boolean crossedTileBoundary(Direction dir, double newPrimary) { + assert dir != null; + return (dir == Direction.UP && newPrimary <= 0.0) + || (dir == Direction.DOWN && newPrimary >= 1.0) + || (dir == Direction.LEFT && newPrimary <= 0.0) + || (dir == Direction.RIGHT && newPrimary >= 1.0); + } /** - * Gets the entity's sub-tile Y offset. + * Helper function that snaps Pacman toward the center of the tile when not moving. * - * @return The sub-tile Y coordinate. + * @param distanceToMove the maximum distance Pacman can move. */ - public float getSubTileY() { return this.subTileY; } + private void snapToCenter(double distanceToMove) { + double center = 0.5; + + subTileX = (float)snapAxisToCenter(subTileX, center, distanceToMove); + subTileY = (float)snapAxisToCenter(subTileY, center, distanceToMove); + } + + /** + * Helper function that moves a sub-tile coordinate toward the center up to a maximum distance. + * + * @param sub the current sub-coordinate. + * @param center the center coordinate (typically 0.5). + * @param maxMove the maximum movement allowed. + * @return the updated coordinate. + */ + private double snapAxisToCenter(double sub, double center, double maxMove) { + double dist = Math.abs(sub - center); + if (dist < maxMove) { + return center; + } else { + return sub + ((sub < center) ? 1 : -1) * maxMove; + } + } + + /* Getters */ + /** + * Gets the tile-based position of the entity. + * + * @return The current tile position. + */ + public Position getPosition() { return this.position; } /** * Gets the radius of the entity. @@ -94,23 +259,43 @@ public abstract class Entity { * * @return The entity's X position as a double. */ - public double getPositionX() { return position.x() + subTileX; } + public double getX() { return position.x() + subTileX; } /** * Gets the full Y coordinate, including sub-tile offset. * * @return The entity's Y position as a double. */ - public double getPositionY() { return position.y() + subTileY; } + public double getY() { return position.y() + subTileY; } + + /** + * Gets the current speed of Pacman. + * + * @return the speed in tiles per second. + */ + public double getSpeed() { return this.speed; } + + /** + * Gets the current movement direction of Pacman. + * + * @return the current {@link Direction}. + */ + public Direction getDirection() { return this.currentDirection; } /* Setters */ /** * Sets the entity's tile-based position. * - * @param newPos The new tile position to set. + * @param newPos The new tile position to set (not {@code null}) + * @throws IllegalArgumentException if newPos is null */ - public void setPosition(Position newPos) { this.position = newPos; } + public void setPosition(Position newPos) { + if (newPos == null) { + throw new IllegalArgumentException("newPos must not be null"); + } + this.position = newPos; + } /** * Sets the entity's sub-tile X offset. @@ -132,4 +317,18 @@ public abstract class Entity { * @param newRad The new radius value. */ public void setRadius(double newRad) { this.radius = newRad; } + + /** + * Sets a new speed for Pacman. + * + * @param newSpeed the new speed value in tiles per second. + */ + public void setSpeed(double newSpeed) { this.speed = newSpeed; } + + /** + * Requests Pacman to change direction at the next available opportunity. + * + * @param newDir the desired direction. + */ + public void setDirection(Direction newDir) { this.nextDirection = newDir; } } diff --git a/pacman/model/src/main/java/com/gr15/pacman/model/entities/EntityUtils.java b/pacman/model/src/main/java/com/gr15/pacman/model/entities/EntityUtils.java index 0bdc6d5..cdd11bf 100644 --- a/pacman/model/src/main/java/com/gr15/pacman/model/entities/EntityUtils.java +++ b/pacman/model/src/main/java/com/gr15/pacman/model/entities/EntityUtils.java @@ -13,45 +13,51 @@ package com.gr15.pacman.model.entities; * *This class is final and cannot be instantiated.
*/ - public final class EntityUtils { /** * Calculates the Euclidean distance between * two entities based on their position and sub-tile offsets. * - * @param arg0 the first entity. - * @param arg1 the second entity. + * @param arg0 the first entity (must not be {@code null}) + * @param arg1 the second entity (mus not be {@code null}) * @return the distance between the centers of the two entities. + * @throws IllegalArgumentException if arg0 or arg1 is {@code null}. */ public static double distance(Entity arg0, Entity arg1) { - double arg0X = arg0.getPositionX(); - double arg0Y = arg0.getPositionY(); - double arg1X = arg1.getPositionX(); - double arg1Y = arg1.getPositionY(); + if (arg0 == null || arg1 == null) { + throw new IllegalArgumentException("arg0 and arg1 must not be null"); + } + + double arg0X = arg0.getX(); + double arg0Y = arg0.getY(); + double arg1X = arg1.getX(); + double arg1Y = arg1.getY(); return Math.hypot(arg0X - arg1X, arg0Y - arg1Y); } /** * Determines whether two entities have collided - * based on the distance between their centers and their combined radii. + * based on circular collision detection. + * A collision is detected if the distance between the entities' centers + * is less than or equal to the sum of their radii. * - * @param arg0 the first entity. - * @param arg1 the second entity. - * @return true if the entities have collided; false otherwise. + * @param arg0 the first entity (must not be {@code null}) + * @param arg1 the second entity (must not be {@code null}) + * @return {@code true} if the entities' bounding circles intersect or touch; {@code false} otherwise + * @throws IllegalArgumentException if {@code arg0} or {@code arg1} is {@code null} */ public static boolean hasCollided(Entity arg0, Entity arg1) { - double arg0X = arg0.getPositionX(); - double arg0Y = arg0.getPositionY(); - double arg1X = arg1.getPositionX(); - double arg1Y = arg1.getPositionY(); + if (arg0 == null || arg1 == null) { + throw new IllegalArgumentException("arg0 and arg1 must not be null"); + } - double dx = arg0X - arg1X; - double dy = arg0Y - arg1Y; + double dx = arg0.getX() - arg1.getX(); + double dy = arg0.getY() - arg1.getY(); double distanceSquared = dx * dx + dy * dy; - double combinedRadius = arg0.getRadius() + arg1.getRadius(); + double combinedRadius = arg0.getRadius() + arg1.getRadius(); return distanceSquared <= combinedRadius * combinedRadius; } } diff --git a/pacman/model/src/main/java/com/gr15/pacman/model/entities/Ghost.java b/pacman/model/src/main/java/com/gr15/pacman/model/entities/Ghost.java index 7ba795f..18bfde2 100644 --- a/pacman/model/src/main/java/com/gr15/pacman/model/entities/Ghost.java +++ b/pacman/model/src/main/java/com/gr15/pacman/model/entities/Ghost.java @@ -1,6 +1,9 @@ package com.gr15.pacman.model.entities; +import java.util.ArrayList; +import java.util.List; import com.gr15.pacman.model.Board; +import com.gr15.pacman.model.Board.TileType; import com.gr15.pacman.model.Position; /** @@ -8,12 +11,225 @@ import com.gr15.pacman.model.Position; */ public class Ghost extends Entity{ + private Pacman pacman; + private GhostType ghostType; - public Ghost(Position startPos, double radius) { - super(startPos, radius); + public enum GhostType { RED, BLUE, PINK, ORANGE }; + + public Ghost(Position startPos, double speed, double radius, + Pacman pacman, GhostType type) { super(startPos, radius, speed); + this.pacman = pacman; + this.ghostType = type; } @Override public void move(Board board, double deltaSeconds) { - // TODO Auto-generated method stub + switch (ghostType) { + case RED -> setDirection(chasePacman(board, pacman)); + case BLUE -> setDirection(predictPacman(board, pacman)); + case PINK -> setDirection(chasePacman(board, pacman)); + case ORANGE -> setDirection(randomize(board)); + default -> setDirection(chasePacman(board, pacman)); + } + super.move(board, deltaSeconds); + } + + public boolean canMove(Board board, Position pos, Direction dir) { + if (dir == null || dir == Direction.NONE) { return false; } + + Position next = pos.offset(dir); + if (!next.inBounds(board.getTileBoard())) { return false; } + + return board.getTile(next) != TileType.WALL; + } + /** + * Method that chooses direction based on where pacman currently is. + * @param board the current board + * @param pac entity pacman + * @return return an available direction that is in the direction of pacman. + */ + public Direction chasePacman(Board board, Pacman pac){ + //This is a dumb ghost just gets pacmans position and trys to go there even if there is a wall. + //Needs refining so that ghost can loop around objects if needed. + Position ghostPos = this.getPosition(); + Position PacPos = pac.getPosition(); + + int pacX = PacPos.x(); + int pacY = PacPos.y(); + int ghostX = ghostPos.x(); + int ghostY = ghostPos.y(); + Direction plannedDir = Direction.NONE; + //isAtIntersection() is so that the ghost only move when at a intersection so they don't + // just move back and forth + if(!isAtIntersection(board, ghostPos) && canMove(board,ghostPos,getDirection())){ + return getDirection(); + } + else{ + int dx = pacX-ghostX; + int dy = pacY-ghostY; + if(Math.abs(dx) > Math.abs(dy)){ //prioritize they direction that is the furthest from pacman + if (dx < 0 && canMove(board, ghostPos, Direction.LEFT)) return Direction.LEFT; + if (dx > 0 && canMove(board, ghostPos, Direction.RIGHT)) return Direction.RIGHT; + if (dy < 0 && canMove(board, ghostPos, Direction.UP)) return Direction.UP; + if (dy > 0 && canMove(board, ghostPos, Direction.DOWN)) return Direction.DOWN; + } else { + if (dy < 0 && canMove(board, ghostPos, Direction.UP)) return Direction.UP; + if (dy > 0 && canMove(board, ghostPos, Direction.DOWN)) return Direction.DOWN; + if (dx < 0 && canMove(board, ghostPos, Direction.LEFT)) return Direction.LEFT; + if (dx > 0 && canMove(board, ghostPos, Direction.RIGHT)) return Direction.RIGHT; + } + + } + return plannedDir; + } + /** + * Method that chooses direction based on a predict path of pacman + * @param board current board + * @param pacman entity pacman + * @return an available direction that is towards pacmans predicted path. + */ + public Direction predictPacman(Board board, Pacman pacman){ + Position ghostPos = this.getPosition(); + Position predictedPos = predictedPacPos(board, pacman); + + int pacX = predictedPos.x(); + int pacY = predictedPos.y(); + int ghostX = ghostPos.x(); + int ghostY = ghostPos.y(); + + int dx = pacX-ghostX; + int dy = pacY-ghostY; + + if(!canMove(board,ghostPos,getDirection()) || getDirection() == Direction.NONE){ + if(dx > dy){ //prioritize they direction that is the furthest from pacman + if(dx < 0 && canMove(board,ghostPos,Direction.LEFT)){ return Direction.LEFT; } + else if(dx > 0 && canMove(board,ghostPos,Direction.RIGHT)){ return Direction.RIGHT; } + else if(dy < 0 && canMove(board,ghostPos,Direction.UP)){ return Direction.UP; } + else if(dy > 0 && canMove(board,ghostPos,Direction.DOWN)){ return Direction.DOWN; } + } + else { + if (dy < 0 && canMove(board, ghostPos, Direction.UP)) return Direction.UP; + else if (dy > 0 && canMove(board, ghostPos, Direction.DOWN)) return Direction.DOWN; + else if (dx < 0 && canMove(board, ghostPos, Direction.LEFT)) return Direction.LEFT; + else if (dx > 0 && canMove(board, ghostPos, Direction.RIGHT)) return Direction.RIGHT; + + } + //missing fallback when it gets stuck + } + return getDirection(); + } + /** + * Method that chooses a random direction based on if it is at a intersection + * @param board current board + * @return random available direction + */ + public Direction randomize(Board board){ + // this function is for the last ghost which goes just random + // maybe later make the ghost go nearer pacman for a bit if it is too far away then back to ramdom + Position pos = this.getPosition(); + Position ghostPos = this.getPosition(); + Direction dir = this.getDirection(); + int count = 0; + if (!isAtIntersection(board, pos) && canMove(board,ghostPos,getDirection())) { + return dir; + } + List