diff options
| author | mithe24 <mithe24@student.sdu.dk> | 2025-05-23 19:24:52 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-05-23 19:24:52 +0200 |
| commit | 6a01dc315b38a047d202b3924be5a84321db8226 (patch) | |
| tree | 68536b8d2829facd45a62eda1f099018b6c35781 | |
| parent | 8c8a4023c98e076ae6c635b36734dbe28782ed69 (diff) | |
| download | pacman-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 '')
30 files changed, 1549 insertions, 641 deletions
diff --git a/pacman/controller/src/main/java/com/gr15/pacman/controller/GameApp.java b/pacman/controller/src/main/java/com/gr15/pacman/controller/GameApp.java index 9c07798..2c76d88 100644 --- a/pacman/controller/src/main/java/com/gr15/pacman/controller/GameApp.java +++ b/pacman/controller/src/main/java/com/gr15/pacman/controller/GameApp.java @@ -42,15 +42,14 @@ public class GameApp public void start(Stage primaryStage) throws Exception { /* window properties */ primaryStage.setTitle("Pac-Man"); - primaryStage.setResizable(false); - primaryStage.setFullScreen(true); /* Adding main menu, and instantiate controller */ - viewManager.addView(ViewKeys.MAIN_MENU, mainMenuView); - viewManager.showView(ViewKeys.MAIN_MENU); + viewManager.addView(ViewKeys.MAIN_MENU_VIEW, mainMenuView); + viewManager.showView(ViewKeys.MAIN_MENU_VIEW); new MainMenuController(viewManager, mainMenuView); - Scene scene = new Scene(viewManager.getRoot(), 1920, 1080); + Scene scene = new Scene(viewManager.getRoot(), 1000, 1000); + primaryStage.setScene(scene); primaryStage.show(); } diff --git a/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/GameController.java b/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/GameController.java index 585247f..82c8cda 100644 --- a/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/GameController.java +++ b/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/GameController.java @@ -3,7 +3,9 @@ package com.gr15.pacman.controller.screen; import com.gr15.pacman.model.GameState; import com.gr15.pacman.model.entities.Entity.Direction; import com.gr15.pacman.view.ViewManager; +import com.gr15.pacman.view.screen.GameOverView; import com.gr15.pacman.view.screen.GameView; +import com.gr15.pacman.view.screen.YouWonView; import com.gr15.pacman.view.ViewManager.ViewKeys; import javafx.animation.AnimationTimer; @@ -41,14 +43,27 @@ public class GameController { * @param gameState the state of the game * @param gameView the view that renders the game * @param viewManager the manager responsible for switching views + * @throws IllegalArgumentException if GameState, gameView or viewManager is {@code null} */ public GameController(GameState gameState, GameView gameView, ViewManager viewManager) { - + if (gameState == null) { + throw new IllegalArgumentException("gameState must not be null"); + } + if (gameView == null) { + throw new IllegalArgumentException("gameView must not be null"); + } + if (viewManager == null) { + throw new IllegalArgumentException("viewManager must not be null"); + } this.viewManager = viewManager; this.gameState = gameState; this.gameView = gameView; + /* Removing potential unrelated views */ + viewManager.removeView(ViewKeys.GAME_OVER_VIEW); + viewManager.removeView(ViewKeys.YOU_WON_VIEW); + gameView.setOnKeyPressed(this::handleKeyEvent); gameLoop = new AnimationTimer() { @@ -59,7 +74,21 @@ public class GameController { lastUpdate = now; return; /* returning early, since no time have elapsed */ } - + + if (gameState.isWon()) { + YouWonView youWonView = new YouWonView(gameState.getScore()); + viewManager.addView(ViewKeys.YOU_WON_VIEW, youWonView); + viewManager.showView(ViewKeys.YOU_WON_VIEW); + new YouWonController(youWonView, viewManager); + stopGameLoop(); + } else if(gameState.GameOver()) { + GameOverView gameOverView = new GameOverView(gameState.getScore()); + viewManager.addView(ViewKeys.GAME_OVER_VIEW, gameOverView); + viewManager.showView(ViewKeys.GAME_OVER_VIEW); + new GameOverController(gameOverView, viewManager); + stopGameLoop(); + } + double deltaSeconds = (now - lastUpdate) / 1_000_000_000.0; lastUpdate = now; @@ -84,7 +113,7 @@ public class GameController { case PAGE_UP -> gameView.changeZoom(0.1); case PAGE_DOWN -> gameView.changeZoom(-0.1); case ESCAPE -> { - viewManager.showView(ViewKeys.PAUSE); + viewManager.showView(ViewKeys.PAUSE_VIEW); stopGameLoop(); } default -> {} diff --git a/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/GameOverController.java b/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/GameOverController.java new file mode 100644 index 0000000..6938b2e --- /dev/null +++ b/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/GameOverController.java @@ -0,0 +1,33 @@ +package com.gr15.pacman.controller.screen; + +import javafx.event.ActionEvent; + +import com.gr15.pacman.view.ViewManager; +import com.gr15.pacman.view.ViewManager.ViewKeys; +import com.gr15.pacman.view.screen.GameOverView; + +/** + * GameOverController + */ +public class GameOverController { + + private GameOverView gameOverView; + private ViewManager viewManager; + + public GameOverController(GameOverView gameOverView, ViewManager viewManager) { + if (gameOverView == null) { + throw new IllegalArgumentException("gameOverView must not be null"); + } + if (viewManager == null) { + throw new IllegalArgumentException("viewManager must not be null"); + } + this.gameOverView = gameOverView; + this.viewManager = viewManager; + + gameOverView.getMainMenuButton().setOnAction(this::mainMenu); + } + + private void mainMenu(ActionEvent event) { + viewManager.showView(ViewKeys.MAIN_MENU_VIEW); + } +} diff --git a/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/MainMenuController.java b/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/MainMenuController.java index 58f4d60..a0d1a59 100644 --- a/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/MainMenuController.java +++ b/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/MainMenuController.java @@ -1,7 +1,7 @@ package com.gr15.pacman.controller.screen; +import com.gr15.pacman.model.ConfigBuilder; import com.gr15.pacman.model.GameState; -import com.gr15.pacman.model.GameStateBuilder; import com.gr15.pacman.view.ViewManager; import com.gr15.pacman.view.screen.GameView; import com.gr15.pacman.view.screen.MainMenuView; @@ -13,11 +13,6 @@ import com.gr15.pacman.view.ViewManager.ViewKeys; /** * The {@code MainMenuController} handles interactions on the main menu screen. - * - * <p>This includes starting a new game, resuming an existing game, - * or exiting the application. - * It also manages setting up new - * game views and controllers when a new game is launched.</p> */ public class MainMenuController { @@ -27,20 +22,21 @@ public class MainMenuController { /** The main menu view UI. */ private final MainMenuView mainMenuView; - /** The active game view instance, created when starting or resuming a game. */ - private GameView gameView; - - /** The active game controller instance, used to control the game loop. */ - GameController gameController; - /** * Constructs a new {@code MainMenuController} with the given * view manager and main menu view. * * @param viewManager the manager responsible for view navigation * @param mainMenuView the view representing the main menu UI + * @throws IllegalArgumentException if viewManager or mainMenuView is {@code null} */ public MainMenuController(ViewManager viewManager, MainMenuView mainMenuView) { + if (viewManager == null) { + throw new IllegalArgumentException("viewManager must not be null"); + } + if (mainMenuView == null) { + throw new IllegalArgumentException("mainMenuView must not be null"); + } this.viewManager = viewManager; this.mainMenuView = mainMenuView; initializeButtons(); @@ -50,30 +46,11 @@ public class MainMenuController { * Initializes event handlers for the main menu buttons. * * <p>This includes setting actions for - * starting a new game, resuming a game, and exiting.</p> + * starting a new game, and exiting.</p> */ private void initializeButtons() { - mainMenuView.getResumeButton().setDisable(true); mainMenuView.getNewGameButton().setOnAction(this::startNewGame); mainMenuView.getExitButton().setOnAction(this::exitGame); - mainMenuView.getResumeButton().setOnAction(this::resumeGame); - } - - /** - * Handles the event when the "Resume" button is clicked. - * - * <p>This resumes the game by switching back to the game view - * and starting the game loop.</p> - * - * @param event the action event triggered by the button - */ - private void resumeGame(ActionEvent event) { - if (!viewManager.hasView(ViewKeys.GAME_VIEW) || gameController == null) { - startNewGame(event); - } - - viewManager.showView(ViewKeys.GAME_VIEW); - gameController.startGameLoop(); } /** @@ -85,30 +62,27 @@ public class MainMenuController { * @param event the action event triggered by the button */ private void startNewGame(ActionEvent event) { - GameState gameState = GameStateBuilder.fromJson( - this.getClass().getResourceAsStream("/testGameState.json")); + GameState gameState = new GameState(ConfigBuilder.fromJson( + this.getClass().getResourceAsStream("/testConfig.json"))); /* Creating new views */ - gameView = new GameView(gameState); + GameView gameView = new GameView(gameState); /* Creaing new controllers */ - gameController = new GameController(gameState, + GameController gameController = new GameController(gameState, gameView, viewManager); /* Removing potentiel old views */ viewManager.removeView(ViewKeys.GAME_VIEW); - viewManager.removeView(ViewKeys.PAUSE); + viewManager.removeView(ViewKeys.PAUSE_VIEW); /* Adding new views */ viewManager.addView(ViewKeys.GAME_VIEW, gameView); - /* Enabling resume button */ - mainMenuView.getResumeButton().setDisable(false); - /* Adding pause menu */ PauseView pauseView = new PauseView(); new PauseController(pauseView, gameController, viewManager); - viewManager.addView(ViewKeys.PAUSE, pauseView); + viewManager.addView(ViewKeys.PAUSE_VIEW, pauseView); /* Showing game view and starting game */ viewManager.showView(ViewKeys.GAME_VIEW); diff --git a/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/PauseController.java b/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/PauseController.java index 553455a..c9e1530 100644 --- a/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/PauseController.java +++ b/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/PauseController.java @@ -33,19 +33,23 @@ public class PauseController { * @param pauseView the pause screen UI * @param gameController the game controller to resume or control the game state * @param viewManager the view manager for switching between views + * @throws IllegalArgumentException if pauseView, gameController or viewManager is {@code null} */ public PauseController(PauseView pauseView, GameController gameController, ViewManager viewManager) { + if (pauseView == null) { + throw new IllegalArgumentException("pauseView must not be null"); + } + if (gameController == null) { + throw new IllegalArgumentException("gameController must not be null"); + } + if (viewManager == null) { + throw new IllegalArgumentException("viewManager must not be null"); + } this.gameController = gameController; this.pauseView = pauseView; this.viewManager = viewManager; - setupEventHandlers(); - } - /** - * Initializes event handlers for pause screen buttons and keyboard input. - */ - private void setupEventHandlers() { pauseView.getQuitButton().setOnAction(this::exitGame); pauseView.getMainMenuButton().setOnAction(this::quitToMainMenu); pauseView.getResumeButton().setOnAction(this::resumeGame); @@ -75,7 +79,7 @@ public class PauseController { * @param event the action event triggered by the button */ private void quitToMainMenu(ActionEvent event) { - viewManager.showView(ViewKeys.MAIN_MENU); + viewManager.showView(ViewKeys.MAIN_MENU_VIEW); } /** diff --git a/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/YouWonController.java b/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/YouWonController.java new file mode 100644 index 0000000..973584f --- /dev/null +++ b/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/YouWonController.java @@ -0,0 +1,34 @@ +package com.gr15.pacman.controller.screen; + +import com.gr15.pacman.view.ViewManager; +import com.gr15.pacman.view.ViewManager.ViewKeys; +import com.gr15.pacman.view.screen.YouWonView; + +import javafx.event.ActionEvent; + +/** + * YouWonController + */ +public class YouWonController { + + private YouWonView youWonView; + private ViewManager viewManager; + + public YouWonController(YouWonView youWonView, ViewManager viewManager) { + if (youWonView == null) { + throw new IllegalArgumentException("youWonView must not be null"); + } + if (viewManager == null) { + throw new IllegalArgumentException("viewManager must not be null"); + } + + this.youWonView = youWonView; + this.viewManager = viewManager; + + youWonView.getMainMenuButton().setOnAction(this::mainMenu); + } + + private void mainMenu(ActionEvent event) { + viewManager.showView(ViewKeys.MAIN_MENU_VIEW); + } +} diff --git a/pacman/controller/src/main/resources/testConfig.json b/pacman/controller/src/main/resources/testConfig.json new file mode 100644 index 0000000..1c07899 --- /dev/null +++ b/pacman/controller/src/main/resources/testConfig.json @@ -0,0 +1,41 @@ +{ + "powerModeDuration": 10.0, + "lives": 3, + "ghostSpeed": 2.5, + "numberOfItems": 36, + + "pacman": { + "x": 1, + "y": 1, + "speed": 3.0, + }, + "redGhost": { + "x": 7, + "y": 4 + }, + "blueGhost": { + "x": 7, + "y": 5 + }, + "pinkGhost": { + "x": 7, + "y": 6 + }, + "orangeGhost": { + "x": 7, + "y": 7 + }, + + "board": [ + ["W","W","W","W","W","W","W","W","W"], + ["W","p","p","p","P","p","p","p","W"], + ["E","p","W","W","p","W","W","p","E"], + ["W","p","W","W","p","W","W","p","W"], + ["W","p","W","W","p","W","W","p","W"], + ["W","P","p","p","P","p","p","P","W"], + ["W","p","W","W","p","W","W","p","W"], + ["E","p","W","W","p","W","W","p","E"], + ["W","p","p","p","P","p","p","p","W"], + ["W","W","W","W","W","W","W","W","W"] + ] +} diff --git a/pacman/controller/src/main/resources/testGameState.json b/pacman/controller/src/main/resources/testGameState.json deleted file mode 100644 index 48e333a..0000000 --- a/pacman/controller/src/main/resources/testGameState.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "pacman": { - "x": 1, - "y": 1, - "speed": 3.0, - "radius": 1.0 - }, - - "itmes": [], - "board": [ - ["W","W","W","W","W","W","W","W","W"], - ["W","E","E","E","E","E","E","E","W"], - ["E","E","W","W","E","W","W","E","E"], - ["W","E","W","W","E","W","W","E","W"], - ["W","E","W","W","E","W","W","E","W"], - ["W","E","E","E","E","E","E","E","W"], - ["W","E","W","W","E","W","W","E","W"], - ["E","E","W","W","E","W","W","E","E"], - ["W","E","E","E","E","E","E","E","W"], - ["W","W","W","W","W","W","W","W","W"] - ] -} 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; } } diff --git a/pacman/view/src/main/java/com/gr15/pacman/view/AnimatedSprite.java b/pacman/view/src/main/java/com/gr15/pacman/view/AnimatedSprite.java index 0e82a56..a5e5dd8 100644 --- a/pacman/view/src/main/java/com/gr15/pacman/view/AnimatedSprite.java +++ b/pacman/view/src/main/java/com/gr15/pacman/view/AnimatedSprite.java @@ -40,7 +40,6 @@ public class AnimatedSprite extends Sprite { * @param y the Y-coordinate of the sprite's top-left corner * @param width the width of the sprite * @param height the height of the sprite - * * @throws IllegalArgumentException if {@code frames} is null or empty */ public AnimatedSprite(Image[] frames, double x, double y, @@ -83,4 +82,3 @@ public class AnimatedSprite extends Sprite { } } } - diff --git a/pacman/view/src/main/java/com/gr15/pacman/view/ResourceManager.java b/pacman/view/src/main/java/com/gr15/pacman/view/ResourceManager.java index 3e7103e..d370178 100644 --- a/pacman/view/src/main/java/com/gr15/pacman/view/ResourceManager.java +++ b/pacman/view/src/main/java/com/gr15/pacman/view/ResourceManager.java @@ -57,10 +57,13 @@ public class ResourceManager { * the default "missing texture" {@link Image} is returned. * * @param path the path to the image resource, relative to the classpath - * @return the loaded {@link Image}, - * or a default image if the resource could not be found + * @return the loaded {@link Image}, or a default image if the resource could not be found + * @throws IllegalArgumentException if path is {@code null} */ public Image getTexture(String path) { + if (path == null) { + throw new IllegalArgumentException("path must not be null"); + } if (resources.containsKey(path)) { return resources.get(path); } else { diff --git a/pacman/view/src/main/java/com/gr15/pacman/view/Sprite.java b/pacman/view/src/main/java/com/gr15/pacman/view/Sprite.java index 74be172..258a8f3 100644 --- a/pacman/view/src/main/java/com/gr15/pacman/view/Sprite.java +++ b/pacman/view/src/main/java/com/gr15/pacman/view/Sprite.java @@ -40,8 +40,23 @@ public class Sprite { * @param y the Y-coordinate of the sprite's top-left corner * @param width the width of the sprite * @param height the height of the sprite + * @throws IllegalArgumentException if image is {@code null} or if + * - x is less than 0 + * - y is less than 0 + * - width is less than 0 + * - height is less than 0 */ public Sprite(Image image, double x, double y, double width, double height) { + if (image == null) { + throw new IllegalArgumentException("image must not be be null"); + } + if (x < 0 || y < 0) { + throw new IllegalArgumentException("x and y must be a positive number"); + } + if (width < 0 || height < 0) { + throw new IllegalArgumentException("width and height must be a positive number"); + } + this.image = image; this.x = x; this.y = y; @@ -59,8 +74,12 @@ public class Sprite { * The transformation for rotation is applied around the sprite's center.</p> * * @param gc the {@link GraphicsContext} to render the sprite to + * @trows IllegalArgumentException if gc is {@code null} */ public void render(GraphicsContext gc) { + if (gc == null) { + throw new IllegalArgumentException("gc must not be null"); + } /* Saving the current transformation */ gc.save(); @@ -141,8 +160,14 @@ public class Sprite { * Sets a new image for the sprite. * * @param newImage the new image to set + * @throws IllegalArgumentException if newImage is {@code null} */ - public void setImage(Image newImage) { this.image = newImage; } + public void setImage(Image newImage) { + if (newImage == null) { + throw new IllegalArgumentException("newImage must not be null"); + } + this.image = newImage; + } /** * Sets a new image for the sprite. diff --git a/pacman/view/src/main/java/com/gr15/pacman/view/ViewManager.java b/pacman/view/src/main/java/com/gr15/pacman/view/ViewManager.java index b648786..b77d37d 100644 --- a/pacman/view/src/main/java/com/gr15/pacman/view/ViewManager.java +++ b/pacman/view/src/main/java/com/gr15/pacman/view/ViewManager.java @@ -28,7 +28,13 @@ public class ViewManager { /** * Enumeration of all possible view keys used to identify different views. */ - public enum ViewKeys { MAIN_MENU, GAME_VIEW, PAUSE }; + public enum ViewKeys { + MAIN_MENU_VIEW, + GAME_VIEW, + PAUSE_VIEW, + GAME_OVER_VIEW, + YOU_WON_VIEW + }; /** * A map that stores views associated with their corresponding keys. @@ -51,15 +57,23 @@ public class ViewManager { * * @param key the key to identify the view. * @param view the view to be added. - * @throws IllegalArgumentException if a view with the same key already exists. + * @throws IllegalArgumentException if a view with the same key already exists or + * - key is {@code null} + * - view is {@code null} */ public void addView(ViewKeys key, BaseView view) { + if (key == null) { + throw new IllegalArgumentException("key must not be null"); + } + if (view == null) { + throw new IllegalArgumentException("view must not be null"); + } if (views.containsKey(key)) { throw new IllegalArgumentException( "View with key " + key + " already exists."); - } else { - views.put(key, view); } + + views.put(key, view); } /** @@ -67,9 +81,13 @@ public class ViewManager { * Calls {@code onExit} on the current view (if any) and {@code onEnter} on the new view. * * @param key the key of the view to be shown. - * @throws IllegalArgumentException if no view exists for the specified key. + * @throws IllegalArgumentException if no view exists for the specified key, + * or key is {@code null} */ public void showView(ViewKeys key) { + if (key == null) { + throw new IllegalArgumentException("key must not be null"); + } if (!views.containsKey(key)) { throw new IllegalArgumentException( "No view with key " + key + " exists."); @@ -90,8 +108,12 @@ public class ViewManager { * * @param key the key to check. * @return {@code true} if a view with the key exists, {@code false} otherwise. + * @throws IllegalArgumentException if key is {@code null} */ public boolean hasView(ViewKeys key) { + if (key == null) { + throw new IllegalArgumentException("key must not be null"); + } return views.containsKey(key); } @@ -99,8 +121,12 @@ public class ViewManager { * Removes the view associated with the specified key. * * @param key the key of the view to remove. + * @throws IllegalArgumentException if key is {@code null} */ public void removeView(ViewKeys key) { + if (key == null) { + throw new IllegalArgumentException("key must not be null"); + } views.remove(key); } diff --git a/pacman/view/src/main/java/com/gr15/pacman/view/screen/GameOverView.java b/pacman/view/src/main/java/com/gr15/pacman/view/screen/GameOverView.java new file mode 100644 index 0000000..2991bef --- /dev/null +++ b/pacman/view/src/main/java/com/gr15/pacman/view/screen/GameOverView.java @@ -0,0 +1,48 @@ +package com.gr15.pacman.view.screen; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; + +/** + * Simple Game Over screen with message and button. + */ +public class GameOverView + extends BaseView { + + private final VBox root = new VBox(); + + Label gameOverLabel = new Label("Game Over!"); + private final Button mainMenuButton = new Button("Return to Main Menu"); + private final Label scoreLabel; + + public GameOverView(int score) { + scoreLabel = new Label("Score: " + score); + + gameOverLabel.setTextFill(Color.RED); + gameOverLabel.setFont(Font.font("Arial", FontWeight.BOLD, 40)); + + scoreLabel.setTextFill(Color.WHITE); + scoreLabel.setFont(Font.font("Arial", FontWeight.NORMAL, 24)); + + mainMenuButton.setFont(Font.font("Arial", 18)); + + root.getChildren().addAll(gameOverLabel, scoreLabel, mainMenuButton); + this.getChildren().add(root); + } + + @Override + public void onEnter() { + /* No specific behavior */ + } + + @Override + public void onExit() { + /* No specific behavior */ + } + + public Button getMainMenuButton() { return mainMenuButton; } +} diff --git a/pacman/view/src/main/java/com/gr15/pacman/view/screen/GameView.java b/pacman/view/src/main/java/com/gr15/pacman/view/screen/GameView.java index 1c333d7..d79dc2c 100644 --- a/pacman/view/src/main/java/com/gr15/pacman/view/screen/GameView.java +++ b/pacman/view/src/main/java/com/gr15/pacman/view/screen/GameView.java @@ -9,11 +9,15 @@ import com.gr15.pacman.view.AnimatedSprite; import com.gr15.pacman.view.ResourceManager; import com.gr15.pacman.view.Sprite; -import javafx.geometry.Pos; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; +import javafx.scene.control.Label; import javafx.scene.image.Image; -import javafx.scene.layout.VBox; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; import javafx.scene.transform.Affine; /** @@ -28,22 +32,25 @@ public class GameView extends BaseView { /** The root layout container for this view. */ - private VBox root = new VBox(); + private BorderPane root = new BorderPane(); - /** Canvas used to draw the game graphics. */ + /** {@link Canvas} used to draw the game graphics. */ private Canvas canvas; + /** {@link HBox} used as layout container for HUD */ + private HBox hudPanel = new HBox(20); + /************************************************************* * CONSTANTS * *************************************************************/ - /** Logical width of the canvas for consistent rendering. */ - private static final double LOGICAL_WIDTH = 1920; + /** Target width of the {@link Canvas} for consistent rendering. */ + private static final double VIRTUAL_WIDTH = 1000; - /** Logical width of the canvas for consistent rendering. */ - private static final double LOGICAL_HEIGHT = 1080; + /** Target width of the {@link Canvas} for consistent rendering. */ + private static final double VIRTUAL_HEIGHT = 800; - /** Reference to the current game state for rendering entities and board. */ + /** Size of each tile in pixels. */ private static final int TILE_SIZE = 16; /************************************************************* @@ -53,7 +60,7 @@ public class GameView /** Reference to the {@link ResourceManager} singleton instance. */ private ResourceManager resourceManager = ResourceManager.getInstance(); - /** Reference to the current game state for rendering entities and board. */ + /** Reference to the current {@link GameState} for rendering entities and board. */ private final GameState gameState; /** Graphics context used for rendering on the {@link Canvas}. */ @@ -65,17 +72,54 @@ public class GameView /** Current zoom factor. */ private double currentZoom = 1; + /** Boolean for keeping track of power mode. Needed for changing sprites */ + private boolean powerMode = false; + /************************************************************* * SPRITES * *************************************************************/ - /** Pacman sprite for rendering the player's character. */ + /** {@link Sprite} for rendering {@link Pacman}. */ private AnimatedSprite pacman; - private Sprite redGhost; - private Sprite blueGhost; - private Sprite pinkGhost; - private Sprite orangeGhost; + /** {@link Sprite} for rendering red {@link Ghost}. */ + private Sprite redGhost = new Sprite(resourceManager + .getTexture("/gameAssets/redGhost.png"), + 0, 0, TILE_SIZE, TILE_SIZE); + + /** {@link Sprite} for rendering blue {@link Ghost}. */ + private Sprite blueGhost = new Sprite(resourceManager + .getTexture("/gameAssets/blueGhost.png"), + 0, 0, TILE_SIZE, TILE_SIZE); + + /** {@link Sprite} for rendering pink {@link Ghost}. */ + private Sprite pinkGhost = new Sprite(resourceManager + .getTexture("/gameAssets/pinkGhost.png"), + 0, 0, TILE_SIZE, TILE_SIZE); + + /** {@link Sprite} for rendering orange {@link Ghost}. */ + private Sprite orangeGhost = new Sprite(resourceManager + .getTexture("/gameAssets/orangeGhost.png"), + 0, 0, TILE_SIZE, TILE_SIZE); + + /************************************************************* + * UI ELEMENTS * + *************************************************************/ + + /** {@link Label} for displaying game score. */ + private Label scoreLabel = new Label("Score: 0"); + + /** {@link Label} for displaying game lives. */ + Label livesLabel = new Label("Lives: "); + + /** {@link Label} for displaying game time. */ + Label timeLabel = new Label("Time: "); + /* To keep track of time */ + private double time = 0; + + /************************************************************* + * CONSTRUCTOR * + *************************************************************/ /** * Constructs a new {@code GameView} with the given game state. @@ -86,15 +130,26 @@ public class GameView public GameView(GameState gameState) { this.gameState = gameState; - canvas = new Canvas(LOGICAL_WIDTH, LOGICAL_HEIGHT); + canvas = new Canvas(VIRTUAL_WIDTH, VIRTUAL_HEIGHT); gc = canvas.getGraphicsContext2D(); - root.setAlignment(Pos.TOP_CENTER); - root.getChildren().add(canvas); - getChildren().add(root); + scoreLabel.setTextFill(Color.WHITE); + scoreLabel.setFont(Font.font("Arial", FontWeight.BOLD, 16)); - /* Setting up animated sprites */ + livesLabel.setTextFill(Color.WHITE); + livesLabel.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + timeLabel.setTextFill(Color.WHITE); + timeLabel.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + + hudPanel.setStyle("-fx-background-color: red; -fx-padding: 10;"); + hudPanel.getChildren().addAll(scoreLabel, livesLabel, timeLabel); + + root.setCenter(canvas); + root.setTop(hudPanel); + this.getChildren().add(root); + + /* Setting up animated sprites */ Image[] pacmanFrames = { resourceManager.getTexture("/gameAssets/pacman1.png"), resourceManager.getTexture("/gameAssets/pacman2.png"), @@ -104,7 +159,7 @@ public class GameView } /************************************************************* - * METHODS * + * RENDER LOOP * *************************************************************/ /** @@ -119,9 +174,12 @@ public class GameView updateCamara(); renderBoard(); - for (Ghost ghost : gameState.getGhosts()) { - /* Update each ghost sprite */ - } + /* Updating hud */ + scoreLabel.setText("Score: " + gameState.getScore()); + livesLabel.setText("Lives: " + gameState.getLives()); + time += deltaSeconds; + int integerTime = (int)Math.round(time); + timeLabel.setText("Time: " + integerTime); /* Rotating pacman sprite */ Pacman pacmanEntity = gameState.getPacman(); @@ -132,10 +190,48 @@ public class GameView case RIGHT -> pacman.setRotation(90); default -> {} } + + /* Updating animated sprite */ pacman.update(deltaSeconds); renderEntity(pacmanEntity, pacman); + + for (Ghost ghost : gameState.getGhosts()) { + switch (ghost.getGhostType()) { + case RED -> renderEntity(ghost, redGhost); + case BLUE -> renderEntity(ghost, blueGhost); + case PINK -> renderEntity(ghost, pinkGhost); + case ORANGE -> renderEntity(ghost, orangeGhost); + } + } + + /* Updating sprite texture if in power mode */ + if (gameState.getPowerModeDuration() > 0 && !powerMode) { + powerMode = true; + redGhost.setImage(resourceManager + .getTexture("/gameAssets/scaredGhost.png")); + blueGhost.setImage(resourceManager + .getTexture("/gameAssets/scaredGhost.png")); + pinkGhost.setImage(resourceManager + .getTexture("/gameAssets/scaredGhost.png")); + orangeGhost.setImage(resourceManager + .getTexture("/gameAssets/scaredGhost.png")); + } else if (gameState.getPowerModeDuration() == 0 && powerMode) { + powerMode = false; + redGhost.setImage(resourceManager + .getTexture("/gameAssets/redGhost.png")); + blueGhost.setImage(resourceManager + .getTexture("/gameAssets/blueGhost.png")); + pinkGhost.setImage(resourceManager + .getTexture("/gameAssets/pinkGhost.png")); + orangeGhost.setImage(resourceManager + .getTexture("/gameAssets/orangeGhost.png")); + } } + /************************************************************* + * HELPER FUNCTIONS * + *************************************************************/ + /** * Updates the camera transform based on Pacman's position, * centering the camera and applying zoom. @@ -144,13 +240,14 @@ public class GameView camara.setToIdentity(); double screenWidth = canvas.getWidth(); double screenHeight = canvas.getHeight(); - double scaleX = (screenWidth / LOGICAL_WIDTH) * currentZoom; - double scaleY = (screenHeight / LOGICAL_HEIGHT) * currentZoom; + double scaleX = (screenWidth / VIRTUAL_WIDTH) * currentZoom; + double scaleY = (screenHeight / VIRTUAL_HEIGHT) * currentZoom; double scale = Math.min(scaleX, scaleY); - /* Define center of camara */ - double centerX = gameState.getPacman().getPositionX() * TILE_SIZE; - double centerY = gameState.getPacman().getPositionY() * TILE_SIZE; + /* Define center of camara. + * Could be changed to center on anything. */ + double centerX = gameState.getPacman().getX() * TILE_SIZE; + double centerY = gameState.getPacman().getY() * TILE_SIZE; camara.appendTranslation(screenWidth / 2, screenHeight / 2); camara.appendScale(scale, scale); @@ -178,6 +275,16 @@ public class GameView worldX, worldY, TILE_SIZE, TILE_SIZE ); } + case PELLET -> { + gc.drawImage(ResourceManager.getInstance() + .getTexture("/gameAssets/food.png"), + worldX, worldY, TILE_SIZE, TILE_SIZE); + } + case POWER_PELLET -> { + gc.drawImage(ResourceManager.getInstance() + .getTexture("/gameAssets/powerFood.png"), + worldX, worldY, TILE_SIZE, TILE_SIZE); + } case EMPTY -> {} } } @@ -192,8 +299,8 @@ public class GameView * @param sprite the sprite to use for rendering the entity */ private void renderEntity(Entity entity, Sprite sprite) { - double spriteX = entity.getPositionX() * TILE_SIZE; - double spriteY = entity.getPositionY() * TILE_SIZE; + double spriteX = entity.getX() * TILE_SIZE; + double spriteY = entity.getY() * TILE_SIZE; sprite.setX(spriteX - sprite.getWidth() / 2); sprite.setY(spriteY - sprite.getHeight() / 2); @@ -201,13 +308,17 @@ public class GameView sprite.render(gc); } + /************************************************************* + * METHODS * + *************************************************************/ + /** * Adjusts the current zoom level of the camera. * * @param deltaZoom the amount to change the zoom level by */ public void changeZoom(double deltaZoom) { - currentZoom += deltaZoom; + currentZoom = Math.max(currentZoom + deltaZoom, 1); } /** @@ -219,7 +330,7 @@ public class GameView } /** - * Called when this view is deactivated or switched away from. + * Called when this view is switched away from. */ @Override public void onExit() { @@ -231,7 +342,7 @@ public class GameView * * @return the root VBox */ - public VBox getRoot() { + public BorderPane getRoot() { return root; } } diff --git a/pacman/view/src/main/java/com/gr15/pacman/view/screen/MainMenuView.java b/pacman/view/src/main/java/com/gr15/pacman/view/screen/MainMenuView.java index 912ec7b..73286aa 100644 --- a/pacman/view/src/main/java/com/gr15/pacman/view/screen/MainMenuView.java +++ b/pacman/view/src/main/java/com/gr15/pacman/view/screen/MainMenuView.java @@ -6,7 +6,7 @@ import javafx.scene.layout.VBox; /** * Represents the main menu view of the application, containing UI elements - * such as buttons for starting a new game, resuming a game, and exiting. + * such as buttons for starting a new game, and exiting. * * This view extends {@link BaseView} and lays out the buttons vertically * centered using a {@link VBox}. @@ -24,21 +24,18 @@ public class MainMenuView /** Button to start a new game. */ private Button newGameButton = new Button("New Game"); - /** Button resume an existing game. */ - private Button resumeButton = new Button("Resume"); - /** Button to exit the game. */ private Button exitButton = new Button("Exit Game"); /** * Constructs a new {@code MainMenuView} and initializes the layout with - * three main buttons: Resume, New Game, and Exit Game. The layout is + * three main buttons: New Game, and Exit Game. The layout is * vertically centered. */ public MainMenuView() { root.setAlignment(Pos.CENTER); - root.getChildren().addAll(resumeButton, newGameButton, exitButton); - getChildren().add(root); + root.getChildren().addAll(newGameButton, exitButton); + this.getChildren().add(root); } /************************************************************* @@ -75,13 +72,6 @@ public class MainMenuView public Button getNewGameButton() { return this.newGameButton; } /** - * Returns the button used to resume an existing game. - * - * @return the resume button - */ - public Button getResumeButton() { return this.resumeButton; } - - /** * Returns the button used to exit the game. * * @return the exit button diff --git a/pacman/view/src/main/java/com/gr15/pacman/view/screen/PauseView.java b/pacman/view/src/main/java/com/gr15/pacman/view/screen/PauseView.java index fdf32b9..1832f25 100644 --- a/pacman/view/src/main/java/com/gr15/pacman/view/screen/PauseView.java +++ b/pacman/view/src/main/java/com/gr15/pacman/view/screen/PauseView.java @@ -38,7 +38,7 @@ public class PauseView public PauseView() { root.getChildren().addAll(resumeButton, mainMenuButton, quitButton); root.setAlignment(Pos.CENTER); - getChildren().add(root); + this.getChildren().add(root); } /** diff --git a/pacman/view/src/main/java/com/gr15/pacman/view/screen/YouWonView.java b/pacman/view/src/main/java/com/gr15/pacman/view/screen/YouWonView.java new file mode 100644 index 0000000..3b2f66a --- /dev/null +++ b/pacman/view/src/main/java/com/gr15/pacman/view/screen/YouWonView.java @@ -0,0 +1,42 @@ +package com.gr15.pacman.view.screen; + +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; + +public class YouWonView + extends BaseView { + + private VBox root = new VBox(20); + + private final Label winMessage = new Label("🎉 You Win! 🎉"); + private final Label finalScoreLabel; + private final Button mainMenuButton = new Button("Main Menu"); + + public YouWonView(int score) { + finalScoreLabel = new Label("Final Score: " + score); + + winMessage.setStyle("-fx-font-size: 36px; -fx-text-fill: white;"); + finalScoreLabel.setStyle("-fx-font-size: 20px; -fx-text-fill: white;"); + mainMenuButton.setPrefWidth(200); + + + + root.setAlignment(Pos.CENTER); + setStyle("-fx-background-color: black;"); + this.getChildren().addAll(winMessage, finalScoreLabel, mainMenuButton); + } + + @Override + public void onEnter() { + /* No specific behavior */ + } + + @Override + public void onExit() { + /* No specific behavior */ + } + + public Button getMainMenuButton() { return mainMenuButton; } +} diff --git a/pacman/view/src/main/resources/gameAssets/food.png b/pacman/view/src/main/resources/gameAssets/food.png Binary files differnew file mode 100644 index 0000000..ad1e1aa --- /dev/null +++ b/pacman/view/src/main/resources/gameAssets/food.png diff --git a/pacman/view/src/main/resources/gameAssets/powerFood.png b/pacman/view/src/main/resources/gameAssets/powerFood.png Binary files differnew file mode 100644 index 0000000..7e18c47 --- /dev/null +++ b/pacman/view/src/main/resources/gameAssets/powerFood.png |