From 6a01dc315b38a047d202b3924be5a84321db8226 Mon Sep 17 00:00:00 2001 From: mithe24 Date: Fri, 23 May 2025 19:24:52 +0200 Subject: Last commits. I won't be contributing anymore. (#27) * im done * feat(GameConfig): Adds immutability for GameConfig record * accidentally removed main menu button * throwing exceptions * Please stop using protected --- .../src/main/java/com/gr15/pacman/model/Board.java | 114 ++++++-- .../java/com/gr15/pacman/model/ConfigBuilder.java | 142 ++++++++++ .../java/com/gr15/pacman/model/GameConfig.java | 72 +++++ .../main/java/com/gr15/pacman/model/GameState.java | 295 +++++++++++++++++---- .../com/gr15/pacman/model/GameStateBuilder.java | 118 --------- .../main/java/com/gr15/pacman/model/Position.java | 59 ++++- .../com/gr15/pacman/model/entities/Entity.java | 265 +++++++++++++++--- .../gr15/pacman/model/entities/EntityUtils.java | 42 +-- .../java/com/gr15/pacman/model/entities/Ghost.java | 222 +++++++++++++++- .../java/com/gr15/pacman/model/entities/Items.java | 17 -- .../com/gr15/pacman/model/entities/Pacman.java | 235 +--------------- 11 files changed, 1077 insertions(+), 504 deletions(-) create mode 100644 pacman/model/src/main/java/com/gr15/pacman/model/ConfigBuilder.java create mode 100644 pacman/model/src/main/java/com/gr15/pacman/model/GameConfig.java delete mode 100644 pacman/model/src/main/java/com/gr15/pacman/model/GameStateBuilder.java delete mode 100644 pacman/model/src/main/java/com/gr15/pacman/model/entities/Items.java (limited to 'pacman/model/src/main/java/com/gr15') diff --git a/pacman/model/src/main/java/com/gr15/pacman/model/Board.java b/pacman/model/src/main/java/com/gr15/pacman/model/Board.java index 399ddfc..f519d5b 100644 --- a/pacman/model/src/main/java/com/gr15/pacman/model/Board.java +++ b/pacman/model/src/main/java/com/gr15/pacman/model/Board.java @@ -1,48 +1,106 @@ package com.gr15.pacman.model; /** - * Board + * Represents the game board for a Pac-Man-style game. + * The board is a 2D grid of {@link TileType} elements, + * indicating different tile types such as walls, pellets, and empty spaces. */ public class Board { - private TileType[][] tileBoard; - private int height; - private int width; + /** 2D array representing the board layout with tile types. */ + private TileType[][] board; - public enum TileType { WALL, EMPTY }; + /** Enumeration of possible tile types on the board. */ + public enum TileType { WALL, EMPTY, PELLET, POWER_PELLET }; - public Board(int width, int height) { - this.width = width; - this.height = height; - } - - public Board(int width, int height, TileType[][] tileBoard) { - this(width, height); - this.tileBoard = tileBoard; + /** + * Constructs a new {@code Board} with the given 2D tile array. + * + * @param board the initial board layout; must not be {@code null}. + * @throws IllegalArgumentException if the provided board is {@code null}. + */ + public Board(TileType[][] board) { + if (board == null) { + throw new IllegalArgumentException("board must not be null"); + } + this.board = board; } + /** + * Checks if a position on the board is movable + * (i.e., not a wall and within bounds). + * + * @param pos the position to check. + * @return {@code true} if the position is within bounds + * and not a wall, {@code false} otherwise. + */ public boolean isMovable(Position pos) { - if (!pos.inBounds(width, height)) { - return false; - } else { - return tileBoard[pos.y()][pos.x()] != TileType.WALL; - } + if (pos == null) { return false; } + return pos.inBounds(board) && getTile(pos) != TileType.WALL; } + /** + * Returns the entire 2D tile board array. + * + * @return the tile board. + */ + public TileType[][] getTileBoard() { return this.board; } - /* Getters and setters */ - public TileType[][] getTileBoard() { return this.tileBoard; } - public TileType getTile(int x, int y) { return tileBoard[y][x]; } - public int getHeight() { return this.height; } - public int getWidth() { return this.width; } + /** + * Returns the tile type at the specified coordinates. + * + * @param x the x-coordinate (column). + * @param y the y-coordinate (row). + * @return the tile type at (x, y). + * @throws IllegalArgumentException if x and y is out of bounds. + */ + public TileType getTile(int x, int y) { + return getTile(new Position(x, y)); + } - public void setHeight(int newHeight) { this.height = newHeight; } - public void setWidth(int newWidth) { this.width = newWidth; } + /** + * Returns the tile type at the specified coordinates. + * + * @param pos the position to retrieve the tile from. + * @return the tile type at (x, y). + * @throws IllegalArgumentException if x and y is out of bounds. + */ + public TileType getTile(Position pos) { + if (!pos.inBounds(board)) { + throw new IllegalArgumentException("x and y must be within bounds"); + } + return board[pos.y()][pos.x()]; + } + + /** + * Sets the tile type at the specified coordinates. + * + * @param x the x-coordinate (column). + * @param y the y-coordinate (row). + * @param newTile the new tile type to set. + * @throws IllegalArgumentException if x and y is out of bounds, + * or newTile is {@code null}. + */ public void setTile(int x, int y, TileType newTile) { - this.tileBoard[y][x] = newTile; + if (!new Position(x, y).inBounds(board)) { + throw new IllegalArgumentException("x and y must be within bounds"); + } + if (newTile == null) { + throw new IllegalArgumentException("newTile must not be null"); + } + this.board[y][x] = newTile; } + + /** + * Replaces the current tile board with a new 2D tile array. + * + * @param newBoard the new tile board to set. + * @throws IllegalArgumentException if newBoard is {@code null} + */ public void setTileBoard(TileType[][] newBoard) { - this.tileBoard = newBoard; + if (board == null) { + throw new IllegalArgumentException("board must not be null"); + } + this.board = newBoard; } - } diff --git a/pacman/model/src/main/java/com/gr15/pacman/model/ConfigBuilder.java b/pacman/model/src/main/java/com/gr15/pacman/model/ConfigBuilder.java new file mode 100644 index 0000000..55eaaf2 --- /dev/null +++ b/pacman/model/src/main/java/com/gr15/pacman/model/ConfigBuilder.java @@ -0,0 +1,142 @@ +package com.gr15.pacman.model; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import com.gr15.pacman.model.Board.TileType; + +/** + * Utility class for building a {@link GameConfig} instance from a JSON file. + *

+ * 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: + *

+ * + * @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 entities; - private Map items; + /** The list of ghost entities that act as AI opponents. */ + private List ghosts; + + /** The current score of the player. */ private int score = 0; - private int lives = 3; + + /** The remaining duration of power mode (in seconds), + * during which Pacman can eat ghosts. */ private double powerModeDuration = 0; - protected GameState(Board board, Pacman pacman, - List entities, Map items) { - this.board = board; - this.entities = entities; - this.items = items; - this.pacman = pacman; - } + /** The number of remaining lives for the player. */ + private int lives; - public void update(double deltaSeconds) { - if (lives == 0) { - /* Gameover */ + /** The number of pellets and power pellets left on the board. */ + private int numberOfItemsLeft; + + /** + * Constructs a new {@code GameState} with the specified game configuration. + * Initializes the board, player (Pacman), ghosts, score, lives, and item count + * based on the provided configuration. + * + * @param config the game configuration to initialize the state (must not be {@code null}) + * @throws IllegalArgumentException if {@code config} is {@code null} + */ + public GameState(GameConfig config) { + if (config == null) { + throw new IllegalArgumentException("config must not be null"); } + this.config = config; + this.lives = config.lives(); + this.numberOfItemsLeft = config.numberOfItems(); + this.board = new Board(config.initialBoard()); + this.pacman = new Pacman(config.pacmanStartPosition(), config.pacmanSpeed(), 0.5); + + this.ghosts = new ArrayList<>(); + this.ghosts.add(new Ghost(config.redGhostStartPosition(), config.ghostSpeed(), + 0.5, pacman, GhostType.RED)); + ghosts.add(new Ghost(config.blueGhostStartPosition(), config.ghostSpeed(), + 0.5, pacman, GhostType.BLUE)); + ghosts.add(new Ghost(config.pinkGhostStartPosition(), config.ghostSpeed(), + 0.5, pacman, GhostType.PINK)); + ghosts.add(new Ghost(config.orangeGhostStartPosition(), config.ghostSpeed(), + 0.5, pacman, GhostType.ORANGE)); + } + + /** + * Updates the game state based on the elapsed time. + * Moves Pacman and ghosts, handles power mode duration, detects collisions, + * updates score, and processes pellet consumption. + * + * @param deltaSeconds the time elapsed since the last update (in seconds) + */ + public void update(double deltaSeconds) { pacman.move(board, deltaSeconds); /* Power mode */ @@ -43,44 +88,200 @@ public class GameState { powerModeDuration = Math.max(powerModeDuration, 0); /* updating and checking collisions with entities */ - for (Entity entity : entities) { - entity.move(board, deltaSeconds); - if (EntityUtils.hasCollided(pacman, entity) - && powerModeDuration == 0) { - lives -= 1; - } else if (EntityUtils.hasCollided(pacman, entity)) { - score += 100; + for (Ghost ghost : ghosts) { + ghost.move(board, deltaSeconds); + if (EntityUtils.hasCollided(pacman, ghost) + && powerModeDuration < 0.03) { + pacmanDied(); + } else if (EntityUtils.hasCollided(pacman, ghost)) { + ghostDied(ghost); } } - /* checking collisions with items */ - Items item = items.get(pacman.getPosition()); - if (item != null) { - switch (item) { - case PELLET -> score++; - case POWER_PELLET -> powerModeDuration = 10.0; - default -> {} + Position pacmanPos = pacman.getPosition(); + switch (board.getTile(pacmanPos.x(), pacmanPos.y())) { + case PELLET -> { + numberOfItemsLeft--; + score++; + board.setTile(pacmanPos.x(), pacmanPos.y(), TileType.EMPTY); } + case POWER_PELLET -> { + numberOfItemsLeft--; + powerModeDuration = config.powerModeDuration(); + board.setTile(pacmanPos.x(), pacmanPos.y(), TileType.EMPTY); + } + default -> {} + } + } + + /** + * Handles Pacman's death by decrementing lives, resetting Pacman's position + * and direction. Does not handle game over logic here. + */ + private void pacmanDied() { + lives--; + if (lives == 0) { + } + + pacman.setPosition(config.pacmanStartPosition()); + pacman.setDirection(Direction.NONE); + } + + /** + * Handles the event when a ghost dies during power mode. + * Resets the ghost's position based on its type and increments the score. + * + * @param ghost the ghost that died (must not be {@code null}) + */ + private void ghostDied(Ghost ghost) { + assert ghost != null; + + score += 100; + switch (ghost.getGhostType()) { + case RED -> ghost.setPosition(config.redGhostStartPosition()); + case BLUE -> ghost.setPosition(config.blueGhostStartPosition()); + case PINK -> ghost.setPosition(config.pinkGhostStartPosition()); + case ORANGE -> ghost.setPosition(config.orangeGhostStartPosition()); } } - /* Getters and setters */ - public Board getBoard() { return this.board; } - public Pacman getPacman() { return this.pacman; } - public List getEntities() { return this.entities; } - public Map getItems() { return this.items; } - public int getScore() { return this.score; } - public int getLives() { return this.lives; } + /** + * Checks if the player has won the game. + * Winning occurs if the game is over and there are still lives left. + * + * @return {@code true} if the player has won, {@code false} otherwise + */ + public boolean isWon() { + return GameOver() && lives != 0; + } - public void setBoard(Board newBoard) { this.board = newBoard; } - public void setPacman(Pacman newPacman) { this.pacman = newPacman; } - public void setScore(int newScore) { this.score = newScore; } - public void setLives(int newLives) { this.lives = newLives; } + /** + * Checks if the game is over. + * The game is over if all items are collected or if no lives remain. + * + * @return {@code true} if the game is over, {@code false} otherwise + */ + public boolean GameOver() { + return numberOfItemsLeft == 0 || lives == 0; + } - public void setItems(Map newItems) { - this.items = newItems; + /** + * Returns the current game board. + * + * @return the current board + */ + public Board getBoard() { + return this.board; } - public void setEntities(List newEntities) { - this.entities = newEntities; + + /** + * Returns the current Pacman entity. + * + * @return the Pacman + */ + public Pacman getPacman() { + return this.pacman; + } + + /** + * Returns the list of ghost entities. + * + * @return the ghosts + */ + public List getGhosts() { + return this.ghosts; + } + + /** + * Returns the current score. + * + * @return the score + */ + public int getScore() { + return this.score; + } + + /** + * Returns the number of remaining lives. + * + * @return the lives + */ + public int getLives() { + return this.lives; + } + + /** + * Returns the remaining duration of the power mode. + * + * @return the power mode duration in seconds + */ + public double getPowerModeDuration() { + return this.powerModeDuration; + } + + /** + * Sets the game board. + * + * @param newBoard the new board to set (must not be {@code null}) + * @throws IllegalArgumentException if {@code newBoard} is {@code null} + */ + public void setBoard(Board newBoard) { + if (newBoard == null) { + throw new IllegalArgumentException("newBoard must not be null"); + } + this.board = newBoard; + } + + /** + * Sets the Pacman entity. + * + * @param newPacman the new Pacman to set (must not be {@code null}) + * @throws IllegalArgumentException if {@code newPacman} is {@code null} + */ + public void setPacman(Pacman newPacman) { + if (newPacman == null) { + throw new IllegalArgumentException("newPacman must not be null"); + } + this.pacman = newPacman; + } + + /** + * Sets the current score. + * + * @param newScore the new score value + */ + public void setScore(int newScore) { + this.score = newScore; + } + + /** + * Sets the number of remaining lives. + * + * @param newLives the new number of lives + */ + public void setLives(int newLives) { + this.lives = newLives; + } + + /** + * Sets the list of ghost entities. + * + * @param newGhosts the new list of ghosts (must not be {@code null}) + * @throws IllegalArgumentException if {@code newGhosts} is {@code null} + */ + public void setEntities(List newGhosts) { + if (newGhosts == null) { + throw new IllegalArgumentException("newGhost must not be null"); + } + this.ghosts = newGhosts; + } + + /** + * Sets the remaining power mode duration. + * + * @param duration the new power mode duration in seconds + */ + public void setPowerMode(double duration) { + this.powerModeDuration = duration; } } diff --git a/pacman/model/src/main/java/com/gr15/pacman/model/GameStateBuilder.java b/pacman/model/src/main/java/com/gr15/pacman/model/GameStateBuilder.java deleted file mode 100644 index b9a64cd..0000000 --- a/pacman/model/src/main/java/com/gr15/pacman/model/GameStateBuilder.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.gr15.pacman.model; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.json.JSONTokener; - -import com.gr15.pacman.model.entities.Entity.Direction; -import com.gr15.pacman.model.Board.TileType; -import com.gr15.pacman.model.entities.Entity; -import com.gr15.pacman.model.entities.Ghost; -import com.gr15.pacman.model.entities.Items; -import com.gr15.pacman.model.entities.Pacman; - -/** - * Utility class to build a {@link GameState} object from a JSON configuration. - * - *

- * 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. - *

- */ -public class GameStateBuilder { - - /** - Parses a JSON input stream and constructs a {@link GameState} based on the configuration. - *

- * Expected JSON structure includes: - *

    - *
  • {@code pacmanSpawnPoint}: an array of two integers - * representing the X and Y coordinates.
  • - *
  • {@code board}: a 2D array of strings - * representing the board tiles ("W" for wall, "E" for empty).
  • - *
  • {@code pacman}: a JSON object containing properties - * such as {@code x}, {@code y}, {@code speed}, and {@code radius}.
  • - *
- * - * @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}. - * - *

- * 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 entities = new ArrayList<>(); - Map items = new HashMap<>(); - - return new GameState(board, pacman, entities, items); - } catch (JSONException jsonException) { - throw new RuntimeException("Failed to parse GameState json", jsonException); - } - } -} diff --git a/pacman/model/src/main/java/com/gr15/pacman/model/Position.java b/pacman/model/src/main/java/com/gr15/pacman/model/Position.java index 15c567a..55a95b4 100644 --- a/pacman/model/src/main/java/com/gr15/pacman/model/Position.java +++ b/pacman/model/src/main/java/com/gr15/pacman/model/Position.java @@ -2,14 +2,34 @@ package com.gr15.pacman.model; import java.util.List; +import com.gr15.pacman.model.Board.TileType; import com.gr15.pacman.model.entities.Entity.Direction; /** - * Position + * Represents a coordinate (x, y) on a 2D grid. + * Used to denote positions on the game board + * for entities such as Pacman and ghosts. + * + * This record provides utility methods for movement, bounds checking, + * distance calculation, neighboring positions. + * + * @param x the horizontal coordinate (column) + * @param y the vertical coordinate (row) */ public record Position(int x, int y) { + /** + * Returns a new {@code Position} offset from this one + * in the specified direction. + * + * @param dir the direction to offset by (must not be {@code null}) + * @return a new position one tile in the given direction + * @throws IllegalArgumentException if {@code dir} is {@code null} + */ public Position offset(Direction dir) { + if (dir == null) { + throw new IllegalArgumentException("dir must not be null"); + } return switch(dir) { case UP -> new Position(x, y-1); case DOWN -> new Position(x, y+1); @@ -18,21 +38,46 @@ public record Position(int x, int y) { default -> this; }; } + /** + * Checks if this position is within the bounds of the provided 2D board. + * + * @param board the tile board to check against (must not be {@code null}) + * @return {@code true} if the position is within the board, {@code false} otherwise + * @throws IllegalArgumentException if {@code board} is {@code null} + */ + public boolean inBounds(TileType[][] board) { + if (board == null) { + throw new IllegalArgumentException("board must not be null"); + } + return y >= 0 && y < board.length + && x >= 0 && x < board[y].length; + } + /** + * Calculates the Euclidean distance from this position to another. + * + * @param other the other position to compare to (must not be {@code null}) + * @return the straight-line distance between this position and the other + * @throws IllegalArgumentException if {@code other} is {@code null} + */ public double distance(Position other) { + if (other == null) { + throw new IllegalArgumentException("other must not be null"); + } return Math.hypot(other.x - x, other.y - y); } - public boolean inBounds(int width, int height) { - return x >= 0 && y >= 0 && x < width && y < height; - } - + /** + * Returns a list of the four neighbor positions (up, down, left, right). + * Diagonal neighbors are not included. + * + * @return a list of adjacent positions surrounding this one + */ public List neighbors() { return List.of( new Position(x, y+1), new Position(x, y-1), new Position(x-1, y), - new Position(x+1, y) - ); + new Position(x+1, y)); } } diff --git a/pacman/model/src/main/java/com/gr15/pacman/model/entities/Entity.java b/pacman/model/src/main/java/com/gr15/pacman/model/entities/Entity.java index 8d2a176..7cc1c07 100644 --- a/pacman/model/src/main/java/com/gr15/pacman/model/entities/Entity.java +++ b/pacman/model/src/main/java/com/gr15/pacman/model/entities/Entity.java @@ -7,23 +7,17 @@ import com.gr15.pacman.model.Position; * Represents an abstract entity with a position * and collision radius on a game board. * - *

- * 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. - *

+ * 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. - *

+ * useful for animation or collision detection.

* - *

- * 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 options = new ArrayList<>(); + for (Direction d : Direction.values()) { + if (d == Direction.NONE || d == getOpposite(dir)) { continue; }; + if (canMove(board, pos, d)) { + options.add(d); + } + } + + int randomIndex = (int)(Math.random() * count); // random int from 0 to count-1 + return options.get(randomIndex); + } + /** + * Helper function that predicts pacmans position a couple of tiles in advance + * @param board current board + * @param pac entity pacman + * @return a predicted position of pacman a couple of tiles ahead + */ + public Position predictedPacPos(Board board, Pacman pac){ + Direction dir = pac.getDirection(); + Position predicted = pac.getPosition(); // next tile for pacman + for(int i = 0; i < 3; i++){ + Position next = predicted.offset(dir); + if(next.inBounds(board.getTileBoard())){ //checks if next tile is in bounds + predicted = predicted.offset(dir); + } + } + return predicted; //returns 3 tiles ahead of pacman if 3 tiles ahead is in bounds otherwise the end of the row/column of where pacman is going. + } + /** + * This method is for when pacman eats a power-pallet and + * the ghosts get scared, the logic is mirrored with the chasePacman() function + * ie. move away from pacman when scared instead of towards him + * @param board current board + * @param pac entity pacman + * @return a Direction that is away from pacman, to flee. + */ + public Direction scaredGhost(Board board, Pacman pac){ // is currently not used need scared logic and rendering. + 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; + if(!isAtIntersection(board, ghostPos) && canMove(board,ghostPos, getDirection())){ + plannedDir = getDirection(); + } + else{ + int dx = pacX-ghostX; + int dy = pacY-ghostY; + if(dx > dy){ //prioritize they direction that is the furthest from pacman + if(pacX>ghostX && canMove(board,ghostPos,Direction.LEFT)){ return Direction.LEFT; } + else if(pacXghostY && canMove(board,ghostPos,Direction.UP)){ return Direction.UP; } + else if(pacY ghostY && canMove(board, ghostPos, Direction.UP)) return Direction.UP; + else if (pacY < ghostY && canMove(board, ghostPos, Direction.DOWN)) return Direction.DOWN; + else if (pacX > ghostX && canMove(board, ghostPos, Direction.LEFT)) return Direction.LEFT; + else if (pacX < ghostX && canMove(board, ghostPos, Direction.RIGHT)) return Direction.RIGHT; + } + + } + return plannedDir; + } + /** + * Helper function that returns if the entity is at an intersection or not + * @param board current board + * @param pos postition of the entity + * @return a boolean, that is true if there are more than 2 ways an entity can move to + */ + private boolean isAtIntersection(Board board, Position pos) { + int movableCount = 0; + for (Direction dir : Direction.values()) { + if (dir != Direction.NONE && board.isMovable(pos.offset(dir))) { + movableCount++; + } + } + return movableCount > 2; + } + /** + * Helper function to get the opposite direction of the given argument + * @param dir direction of the entity + * @return the opposite direction + */ + private Direction getOpposite(Direction dir) { + return switch (dir) { + case UP -> Direction.DOWN; + case DOWN -> Direction.UP; + case LEFT -> Direction.RIGHT; + case RIGHT -> Direction.LEFT; + default -> Direction.NONE; + }; + } + + public GhostType getGhostType() { + return this.ghostType; } } diff --git a/pacman/model/src/main/java/com/gr15/pacman/model/entities/Items.java b/pacman/model/src/main/java/com/gr15/pacman/model/entities/Items.java deleted file mode 100644 index d4ecc13..0000000 --- a/pacman/model/src/main/java/com/gr15/pacman/model/entities/Items.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.gr15.pacman.model.entities; - -/** - * Represents the different types of items that can appear in the game. - */ -public enum Items { - - /** - * A standard pellet. - */ - PELLET, - - /** - * A power pellet. - */ - POWER_PELLET -}; diff --git a/pacman/model/src/main/java/com/gr15/pacman/model/entities/Pacman.java b/pacman/model/src/main/java/com/gr15/pacman/model/entities/Pacman.java index cb15b06..25a9179 100644 --- a/pacman/model/src/main/java/com/gr15/pacman/model/entities/Pacman.java +++ b/pacman/model/src/main/java/com/gr15/pacman/model/entities/Pacman.java @@ -5,249 +5,18 @@ import com.gr15.pacman.model.Position; /** * Represents the Pacman character in the game. - * Handles movement logic including direction changes, wall collisions, - * snapping to tile centers, and updating position on the game board. */ public class Pacman extends Entity { - /* Javadoc for attributes, - * since movement is a little complex */ - - /** Movement speed in tiles per second. */ - private double speed; - - /** Current direction of movement. */ - private Direction currentDirection = Direction.NONE; - - /** Desired direction to change to, if possible. */ - private Direction nextDirection; - /** * Constructs a new Pacman instance. * * @param startPos the starting tile {@link Position} of Pacman. * @param speed the movement speed in tiles per second. - * @param startDir the initial movement {@link Direction}. * @param radius the radius of Pacman collision. */ - public Pacman(Position startPos, double speed, Direction startDir, double radius) { - super(startPos, radius); - this.speed = speed; - this.nextDirection = startDir; - } - - /** - * 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 game {@link Board}, used to determine valid movement. - * @param deltaSeconds time in seconds since the last update in seconds. - */ - @Override - public void move(Board board, double deltaSeconds) { - float subTileX = super.getSubTileX(); - float subTileY = super.getSubTileY(); - double distanceToMove = speed * deltaSeconds; - - Position currentPos = super.getPosition(); - boolean canMoveNext = canMove(board, currentPos, nextDirection); - boolean canContinue = canMove(board, currentPos, 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.01) { - 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.0001) { - /* 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; - - if (crossedTileBoundary(direction, newSubPrimary)) { - Position nextPos = currentPos.offset(direction); - if (board.isMovable(nextPos)) { - currentPos = nextPos; - subPrimary = directionSign > 0 ? 0.0 : 1.0; - } else { - 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 hit wall, set direction to none */ - if (canMoveNext) { - currentDirection = nextDirection; - nextDirection = Direction.NONE; - } - - /* Setting new position */ - super.setSubTileX(subTileX); - super.setSubTileY(subTileY); - super.setPosition(currentPos); - } - - /** - * Helper function that determines if Pacman can move - * in the specified direction. - * - * @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. - */ - private boolean canMove(Board board, Position pos, Direction dir) { - return dir != Direction.NONE && board.isMovable(pos.offset(dir)); + public Pacman(Position startPos, double speed, double radius) { + super(startPos, radius, speed); } - - /** - * 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) { - 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) { - return (dir == Direction.UP || dir == Direction.LEFT) ? -1 : 1; - } - - /** - * Helper function that determines if Pacman has crossed a tile boundary. - * - * @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. - */ - private boolean crossedTileBoundary(Direction dir, double newPrimary) { - 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); - } - - /** - * Helper function that snaps Pacman toward the center of the tile when not moving. - * - * @param distanceToMove the maximum distance Pacman can move. - */ - private void snapToCenter(double distanceToMove) { - float subTileX = super.getSubTileX(); - float subTileY =super.getSubTileY(); - double center = 0.5; - - subTileX = (float)snapAxisToCenter(subTileX, center, distanceToMove); - subTileY = (float)snapAxisToCenter(subTileY, center, distanceToMove); - - super.setSubTileX(subTileX); - super.setSubTileY(subTileY); - } - - /** - * 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; - } - } - - /** - * 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; } - - /** - * 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; } } -- cgit v1.2.3-70-g09d2