diff --git a/src/main/java/uk/mgrove/ac/soton/comp1206/Launcher.java b/src/main/java/uk/mgrove/ac/soton/comp1206/Launcher.java index 94522b5..bb944c7 100644 --- a/src/main/java/uk/mgrove/ac/soton/comp1206/Launcher.java +++ b/src/main/java/uk/mgrove/ac/soton/comp1206/Launcher.java @@ -8,8 +8,10 @@ public class Launcher { /** * Launch the JavaFX Application, passing through the commandline arguments + * * @param args commandline arguments */ public static void main(String[] args) { App.main(args); - } \ No newline at end of file + } +} diff --git a/src/main/java/uk/mgrove/ac/soton/comp1206/component/GameBlock.java b/src/main/java/uk/mgrove/ac/soton/comp1206/component/GameBlock.java index e0a06a5..f0dba6e 100644 --- a/src/main/java/uk/mgrove/ac/soton/comp1206/component/GameBlock.java +++ b/src/main/java/uk/mgrove/ac/soton/comp1206/component/GameBlock.java @@ -181,4 +181,12 @@ public class GameBlock extends Canvas { value.bind(input); } + @Override + public String toString() { + return "GameBlock{" + + "x=" + x + + ", y=" + y + + ", value=" + value.get() + + '}'; + } } diff --git a/src/main/java/uk/mgrove/ac/soton/comp1206/game/Game.java b/src/main/java/uk/mgrove/ac/soton/comp1206/game/Game.java index 95176e3..bf02f6c 100644 --- a/src/main/java/uk/mgrove/ac/soton/comp1206/game/Game.java +++ b/src/main/java/uk/mgrove/ac/soton/comp1206/game/Game.java @@ -1,17 +1,29 @@ package uk.mgrove.ac.soton.comp1206.game; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import uk.mgrove.ac.soton.comp1206.component.GameBlock; +import java.util.*; + /** * The Game class handles the main logic, state and properties of the TetrECS game. Methods to manipulate the game state * and to handle actions made by the player should take place inside this class. */ public class Game { + /** + * Logger + */ private static final Logger logger = LogManager.getLogger(Game.class); + /** + * Random number generator + */ + private final Random random = new Random(); + /** * Number of rows */ @@ -27,6 +39,28 @@ public class Game { */ protected final Grid grid; + /** + * Current game piece player is using + */ + private GamePiece currentPiece; + + /** + * Player's current score + */ + protected IntegerProperty score = new SimpleIntegerProperty(0); + /** + * Player's current level + */ + protected IntegerProperty level = new SimpleIntegerProperty(0); + /** + * Player's number of remaining lives + */ + protected IntegerProperty lives = new SimpleIntegerProperty(3); + /** + * Player's current multiplier + */ + protected IntegerProperty multiplier = new SimpleIntegerProperty(1); + /** * Create a new game with the specified rows and columns. Creates a corresponding grid model. * @param cols number of columns @@ -48,11 +82,33 @@ public class Game { initialiseGame(); } + /** + * Get the next piece for the player to use in the game + * @return the next piece + */ + public GamePiece nextPiece() { + currentPiece = spawnPiece(); + logger.info("Next piece is: {}", currentPiece); + return currentPiece; + } + + /** + * Create a new game piece + * @return newly created piece + */ + public GamePiece spawnPiece() { + var maxPieces = GamePiece.PIECES; + var randomPieceNumber = random.nextInt(maxPieces); + logger.info("Picking random piece: {}", randomPieceNumber); + return GamePiece.createPiece(randomPieceNumber); + } + /** * Initialise a new game and set up anything that needs to be done at the start */ public void initialiseGame() { logger.info("Initialising game"); + nextPiece(); } /** @@ -64,15 +120,80 @@ public class Game { int x = gameBlock.getX(); int y = gameBlock.getY(); - //Get the new value for this block - int previousValue = grid.get(x,y); - int newValue = previousValue + 1; - if (newValue > GamePiece.PIECES) { - newValue = 0; + if (grid.canPlayPiece(currentPiece,x,y)) { + grid.playPiece(currentPiece,x,y); + afterPiece(); + nextPiece(); + } else { + // can't play the piece + } + } + + /** + * Handle additional processing after piece has been played - clear lines of blocks + */ + public void afterPiece() { + Set blocksToRemove = new HashSet<>(); + int linesToRemove = 0; + + for (var x=0; x < grid.getCols(); x++) { + List columnBlocksToRemove = new ArrayList<>(); + for (var y=0; y < grid.getRows(); y++) { + // if column isn't full then move to next column + if (grid.get(x,y) <= 0) break; + + columnBlocksToRemove.add(grid.getGridProperty(x,y)); + } + // if column is full then store blocks to reset + if (columnBlocksToRemove.size() == grid.getRows()) { + for (var block : columnBlocksToRemove) { + blocksToRemove.add(block); + linesToRemove++; + } + } } - //Update the grid with the new value - grid.set(x,y,newValue); + // do the same for rows + for (var y=0; y < grid.getRows(); y++) { + List rowBlocksToRemove = new ArrayList<>(); + for (var x=0; x < grid.getCols(); x++) { + // if row isn't full then move to next row + if (grid.get(x,y) <= 0) break; + + rowBlocksToRemove.add(grid.getGridProperty(x,y)); + } + // if row is full then store blocks to reset + if (rowBlocksToRemove.size() == grid.getCols()) { + for (var block : rowBlocksToRemove) { + blocksToRemove.add(block); + linesToRemove++; + } + } + } + + // update score and multiplier + score(linesToRemove,blocksToRemove.size()); + + // reset blocks that need resetting + for (var block : blocksToRemove) { + block.set(0); + } + } + + /** + * Update the score, multiplier, and level depending on the number of lines and blocks that have been cleared + * @param lines number of lines cleared + * @param blocks number of blocks cleared + */ + private void score(int lines, int blocks) { + var addScore = lines * blocks * 10 * multiplier.get(); + score.set(score.get() + addScore); + + if (lines > 0) multiplier.set(multiplier.get() + 1); + else multiplier.set(1); + + // set level + level.set((int) Math.floor((double) score.get() / 1000)); } /** @@ -99,5 +220,95 @@ public class Game { return rows; } + /** + * Get the player's current level + * @return current level + */ + public int getLevel() { + return level.get(); + } + /** + * Set the player's current level + */ + public void setLevel(int level) { + this.level.set(level); + } + + /** + * Get the player's current level property + * @return player's current level property + */ + public IntegerProperty levelProperty() { + return level; + } + + /** + * Get the player's remaining lives + * @return number of remaining lives + */ + public int getLives() { + return lives.get(); + } + + /** + * Set the player's remaining lives + */ + public void setLives(int lives) { + this.lives.set(lives); + } + + /** + * Get the player's remaining lives property + * @return player's remaining lives property + */ + public IntegerProperty livesProperty() { + return lives; + } + + /** + * Get the player's current multiplier + * @return current multiplier + */ + public int getMultiplier() { + return multiplier.get(); + } + + /** + * Set the player's current multiplier + */ + public void setMultiplier(int multiplier) { + this.multiplier.set(multiplier); + } + + /** + * Get the player's current multiplier property + * @return player's current multiplier property + */ + public IntegerProperty multiplierProperty() { + return multiplier; + } + + /** + * Get the player's current score + * @return current score + */ + public int getScore() { + return score.get(); + } + + /** + * Set the player's current score + */ + public void setScore(int score) { + this.score.set(score); + } + + /** + * Get the player's current score property + * @return player's current score property + */ + public IntegerProperty scoreProperty() { + return score; + } } diff --git a/src/main/java/uk/mgrove/ac/soton/comp1206/game/Grid.java b/src/main/java/uk/mgrove/ac/soton/comp1206/game/Grid.java index b47717a..6304e86 100644 --- a/src/main/java/uk/mgrove/ac/soton/comp1206/game/Grid.java +++ b/src/main/java/uk/mgrove/ac/soton/comp1206/game/Grid.java @@ -2,6 +2,9 @@ package uk.mgrove.ac.soton.comp1206.game; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import uk.mgrove.ac.soton.comp1206.ui.GameWindow; /** * The Grid is a model which holds the state of a game board. It is made up of a set of Integer values arranged in a 2D @@ -16,6 +19,11 @@ import javafx.beans.property.SimpleIntegerProperty; */ public class Grid { + /** + * Logger + */ + private static final Logger logger = LogManager.getLogger(Grid.class); + /** * The number of columns in this grid */ @@ -87,6 +95,58 @@ public class Grid { } } + /** + * Check whether piece can be placed in grid at given location + * @param piece piece to check + * @param placeX x-coordinate to check + * @param placeY y-coordinate to check + * @return whether piece can be placed in this location + */ + public boolean canPlayPiece(GamePiece piece, int placeX, int placeY) { + logger.info("Checking if piece {} can be played at {},{}", piece, placeX, placeY); + + int[][] blocks = piece.getBlocks(); + + for (var blockX = 0; blockX < blocks.length; blockX++) { + for (var blockY = 0; blockY < blocks.length; blockY++) { + // check if this block can be placed on grid + var blockValue = blocks[blockX][blockY]; + var x = placeX + blockX - 1; + var y = placeY + blockY - 1; + var gridValue = get(x,y); + if (blockValue != 0 && gridValue != 0) { + logger.info("Unable to place block due to conflict at {},{}", x, y); + return false; + } + } + } + + return true; + } + + /** + * Play a piece at given location in the grid by updating the grid with the piece blocks + * @param piece piece to place + * @param placeX x-coordinate to check + * @param placeY y-coordinate to check + */ + public void playPiece(GamePiece piece, int placeX, int placeY) { + logger.info("Playing piece {} at {},{}", piece, placeX, placeY); + int value = piece.getValue(); + int[][] blocks = piece.getBlocks(); + + // return if piece can't be played + if (!canPlayPiece(piece, placeX, placeY)) return; + + for (var blockX = 0; blockX < blocks.length; blockX++) { + for (var blockY = 0; blockY < blocks.length; blockY++) { + if (blocks[blockX][blockY] > 0) { + set(placeX + blockX - 1, placeY + blockY - 1, value); + } + } + } + } + /** * Get the number of columns in this game * @return number of columns diff --git a/src/main/java/uk/mgrove/ac/soton/comp1206/scene/ChallengeScene.java b/src/main/java/uk/mgrove/ac/soton/comp1206/scene/ChallengeScene.java index a7a8fc2..319cdfa 100644 --- a/src/main/java/uk/mgrove/ac/soton/comp1206/scene/ChallengeScene.java +++ b/src/main/java/uk/mgrove/ac/soton/comp1206/scene/ChallengeScene.java @@ -8,6 +8,8 @@ import uk.mgrove.ac.soton.comp1206.component.GameBoard; import uk.mgrove.ac.soton.comp1206.game.Game; import uk.mgrove.ac.soton.comp1206.ui.GamePane; import uk.mgrove.ac.soton.comp1206.ui.GameWindow; +import uk.mgrove.ac.soton.comp1206.ui.StatsMenu; +import uk.mgrove.ac.soton.comp1206.util.Multimedia; /** * The Single Player challenge scene. Holds the UI for the single player challenge mode in the game. @@ -49,7 +51,12 @@ public class ChallengeScene extends BaseScene { var board = new GameBoard(game.getGrid(),gameWindow.getWidth()/2,gameWindow.getWidth()/2); mainPane.setCenter(board); - //Handle block on gameboard grid being clicked + var statsMenu = new StatsMenu(game.scoreProperty(),game.levelProperty(),game.livesProperty(),game.multiplierProperty()); + mainPane.setRight(statsMenu); + + Multimedia.playMusic("music/game.wav"); + + //Handle block on game board grid being clicked board.setOnBlockClick(this::blockClicked); } @@ -62,7 +69,7 @@ public class ChallengeScene extends BaseScene { } /** - * Setup the game object and model + * Set up the game object and model */ public void setupGame() { logger.info("Starting a new challenge"); diff --git a/src/main/java/uk/mgrove/ac/soton/comp1206/scene/MenuScene.java b/src/main/java/uk/mgrove/ac/soton/comp1206/scene/MenuScene.java index 9d4260c..647274c 100644 --- a/src/main/java/uk/mgrove/ac/soton/comp1206/scene/MenuScene.java +++ b/src/main/java/uk/mgrove/ac/soton/comp1206/scene/MenuScene.java @@ -8,6 +8,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import uk.mgrove.ac.soton.comp1206.ui.GamePane; import uk.mgrove.ac.soton.comp1206.ui.GameWindow; +import uk.mgrove.ac.soton.comp1206.util.Multimedia; /** * The main menu of the game. Provides a gateway to the rest of the game. @@ -52,6 +53,8 @@ public class MenuScene extends BaseScene { var button = new Button("Play"); mainPane.setCenter(button); + Multimedia.playMusic("music/menu.mp3"); + //Bind the button action to the startGame method in the menu button.setOnAction(this::startGame); } diff --git a/src/main/java/uk/mgrove/ac/soton/comp1206/ui/StatsMenu.java b/src/main/java/uk/mgrove/ac/soton/comp1206/ui/StatsMenu.java new file mode 100644 index 0000000..8b59516 --- /dev/null +++ b/src/main/java/uk/mgrove/ac/soton/comp1206/ui/StatsMenu.java @@ -0,0 +1,49 @@ +package uk.mgrove.ac.soton.comp1206.ui; + +import javafx.beans.property.IntegerProperty; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +/** + * Stats menu class to show basic stats about game status + */ +public class StatsMenu extends VBox { + + /** + * Player's current score + */ + private final Text score = new Text("0"); + /** + * Player's current level + */ + private final Text level = new Text("0"); + /** + * Player's remaining lives + */ + private final Text lives = new Text("3"); + /** + * Player's current multiplier + */ + private final Text multiplier = new Text("1"); + + /** + * Initialise the menu by adding basic stats and binding them to properties + * @param score score property + * @param level level property + * @param lives lives property + * @param multiplier multiplier property + */ + public StatsMenu(IntegerProperty score, IntegerProperty level, IntegerProperty lives, IntegerProperty multiplier) { + this.score.textProperty().bind(score.asString()); + this.level.textProperty().bind(level.asString()); + this.lives.textProperty().bind(lives.asString()); + this.multiplier.textProperty().bind(multiplier.asString()); + + var scoreHeader = new Text("Score"); + var levelHeader = new Text("Level"); + var livesHeader = new Text("Lives"); + var multiplierHeader = new Text("Multiplier"); + getChildren().addAll(scoreHeader,this.score,levelHeader,this.level,livesHeader,this.lives,multiplierHeader,this.multiplier); + } + +} diff --git a/src/main/java/uk/mgrove/ac/soton/comp1206/util/Multimedia.java b/src/main/java/uk/mgrove/ac/soton/comp1206/util/Multimedia.java new file mode 100644 index 0000000..abbd990 --- /dev/null +++ b/src/main/java/uk/mgrove/ac/soton/comp1206/util/Multimedia.java @@ -0,0 +1,41 @@ +package uk.mgrove.ac.soton.comp1206.util; + +import javafx.scene.media.Media; +import javafx.scene.media.MediaPlayer; + +public class Multimedia { + + /** + * Media player for game music + */ + private static MediaPlayer musicPlayer; + /** + * Media player for sound effects + */ + private static MediaPlayer audioPlayer; + + /** + * Play sound effect from file + * @param filePath file path of audio file + */ + public static void playAudio(String filePath) { + if (audioPlayer != null) audioPlayer.stop(); + var media = new Media(Multimedia.class.getResource("/" + filePath).toExternalForm()); + audioPlayer = new MediaPlayer(media); + audioPlayer.play(); + } + + /** + * Play background music from file + * @param filePath file path of audio file + */ + public static void playMusic(String filePath) { + if (musicPlayer != null) musicPlayer.stop(); + var media = new Media(Multimedia.class.getResource("/" + filePath).toExternalForm()); + musicPlayer = new MediaPlayer(media); + musicPlayer.setAutoPlay(true); + musicPlayer.setCycleCount(MediaPlayer.INDEFINITE); + musicPlayer.play(); + } + +}