summaryrefslogtreecommitdiff
path: root/pacman/model/src/main/java/com
diff options
context:
space:
mode:
authormithe24 <mithe24@student.sdu.dk>2025-05-23 19:24:52 +0200
committerGitHub <noreply@github.com>2025-05-23 19:24:52 +0200
commit6a01dc315b38a047d202b3924be5a84321db8226 (patch)
tree68536b8d2829facd45a62eda1f099018b6c35781 /pacman/model/src/main/java/com
parent8c8a4023c98e076ae6c635b36734dbe28782ed69 (diff)
downloadpacman-6a01dc315b38a047d202b3924be5a84321db8226.tar.gz
pacman-6a01dc315b38a047d202b3924be5a84321db8226.zip
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
Diffstat (limited to '')
-rw-r--r--pacman/model/src/main/java/com/gr15/pacman/model/Board.java114
-rw-r--r--pacman/model/src/main/java/com/gr15/pacman/model/ConfigBuilder.java142
-rw-r--r--pacman/model/src/main/java/com/gr15/pacman/model/GameConfig.java72
-rw-r--r--pacman/model/src/main/java/com/gr15/pacman/model/GameState.java295
-rw-r--r--pacman/model/src/main/java/com/gr15/pacman/model/GameStateBuilder.java118
-rw-r--r--pacman/model/src/main/java/com/gr15/pacman/model/Position.java59
-rw-r--r--pacman/model/src/main/java/com/gr15/pacman/model/entities/Entity.java265
-rw-r--r--pacman/model/src/main/java/com/gr15/pacman/model/entities/EntityUtils.java42
-rw-r--r--pacman/model/src/main/java/com/gr15/pacman/model/entities/Ghost.java222
-rw-r--r--pacman/model/src/main/java/com/gr15/pacman/model/entities/Items.java17
-rw-r--r--pacman/model/src/main/java/com/gr15/pacman/model/entities/Pacman.java235
11 files changed, 1077 insertions, 504 deletions
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.
+ * <p>
+ * 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.
+ * <p>
+ * Expected JSON tile symbols:
+ * <ul>
+ * <li>"W" - Wall</li>
+ * <li>"E" - Empty</li>
+ * <li>"p" - Pellet</li>
+ * <li>"P" - Power Pellet</li>
+ * </ul>
+ *
+ * @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<Entity> entities;
- private Map<Position, Items> items;
+ /** The list of ghost entities that act as AI opponents. */
+ private List<Ghost> 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<Entity> entities, Map<Position, Items> 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<Entity> getEntities() { return this.entities; }
- public Map<Position, Items> 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<Position, Items> newItems) {
- this.items = newItems;
+ /**
+ * Returns the current game board.
+ *
+ * @return the current board
+ */
+ public Board getBoard() {
+ return this.board;
}
- public void setEntities(List<Entity> 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<Ghost> 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<Ghost> 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.
- *
- * <p>
- * 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.
- * </p>
- */
-public class GameStateBuilder {
-
- /**
- Parses a JSON input stream and constructs a {@link GameState} based on the configuration.
- * <p>
- * Expected JSON structure includes:
- * <ul>
- * <li>{@code pacmanSpawnPoint}: an array of two integers
- * representing the X and Y coordinates.</li>
- * <li>{@code board}: a 2D array of strings
- * representing the board tiles ("W" for wall, "E" for empty).</li>
- * <li>{@code pacman}: a JSON object containing properties
- * such as {@code x}, {@code y}, {@code speed}, and {@code radius}.</li>
- * </ul>
- *
- * @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}.
- *
- * <p>
- * 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<Entity> entities = new ArrayList<>();
- Map<Position, Items> 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<Position> 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.
*
- * <p>
- * This class stores both tile-based {@link Position}
+ * <p> This class stores both tile-based {@link Position}
* and fine-grained sub-tile coordinates
- * to allow smooth movement within and across grid tiles.
- * </p>
+ * to allow smooth movement within and across grid tiles. </p>
*
- * <p>
- * The full world coordinates are computed by combining
+ * <p> 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.
- * </p>
+ * useful for animation or collision detection. </p>
*
- * <p>
- * Subclasses must implement the {@code move} method
- * to define entity-specific behavior over time.
- * </p>
+ * <p> Handles movement logic including direction changes, wall collisions,
+ * snapping to tile centers, and updating position on the game board. </p>
*/
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;
*
* <p>This class is final and cannot be instantiated.</p>
*/
-
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<Direction> 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(pacX<ghostX && canMove(board,ghostPos,Direction.RIGHT)){ return Direction.RIGHT; }
+ 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 (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; }
}