[FEAT] Add basic game logic and audio

This commit is contained in:
2023-03-28 14:59:45 +01:00
parent ab1c38cb2c
commit afdbf75158
8 changed files with 391 additions and 10 deletions

View File

@@ -8,8 +8,10 @@ public class Launcher {
/** /**
* Launch the JavaFX Application, passing through the commandline arguments * Launch the JavaFX Application, passing through the commandline arguments
*
* @param args commandline arguments * @param args commandline arguments
*/ */
public static void main(String[] args) { public static void main(String[] args) {
App.main(args); App.main(args);
} }
}

View File

@@ -181,4 +181,12 @@ public class GameBlock extends Canvas {
value.bind(input); value.bind(input);
} }
@Override
public String toString() {
return "GameBlock{" +
"x=" + x +
", y=" + y +
", value=" + value.get() +
'}';
}
} }

View File

@@ -1,17 +1,29 @@
package uk.mgrove.ac.soton.comp1206.game; 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.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.component.GameBlock; 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 * 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. * and to handle actions made by the player should take place inside this class.
*/ */
public class Game { public class Game {
/**
* Logger
*/
private static final Logger logger = LogManager.getLogger(Game.class); private static final Logger logger = LogManager.getLogger(Game.class);
/**
* Random number generator
*/
private final Random random = new Random();
/** /**
* Number of rows * Number of rows
*/ */
@@ -27,6 +39,28 @@ public class Game {
*/ */
protected final Grid grid; 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. * Create a new game with the specified rows and columns. Creates a corresponding grid model.
* @param cols number of columns * @param cols number of columns
@@ -48,11 +82,33 @@ public class Game {
initialiseGame(); 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 * Initialise a new game and set up anything that needs to be done at the start
*/ */
public void initialiseGame() { public void initialiseGame() {
logger.info("Initialising game"); logger.info("Initialising game");
nextPiece();
} }
/** /**
@@ -64,15 +120,80 @@ public class Game {
int x = gameBlock.getX(); int x = gameBlock.getX();
int y = gameBlock.getY(); int y = gameBlock.getY();
//Get the new value for this block if (grid.canPlayPiece(currentPiece,x,y)) {
int previousValue = grid.get(x,y); grid.playPiece(currentPiece,x,y);
int newValue = previousValue + 1; afterPiece();
if (newValue > GamePiece.PIECES) { nextPiece();
newValue = 0; } else {
// can't play the piece
}
} }
//Update the grid with the new value /**
grid.set(x,y,newValue); * Handle additional processing after piece has been played - clear lines of blocks
*/
public void afterPiece() {
Set<IntegerProperty> blocksToRemove = new HashSet<>();
int linesToRemove = 0;
for (var x=0; x < grid.getCols(); x++) {
List<IntegerProperty> 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++;
}
}
}
// do the same for rows
for (var y=0; y < grid.getRows(); y++) {
List<IntegerProperty> 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; 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;
}
} }

View File

@@ -2,6 +2,9 @@ package uk.mgrove.ac.soton.comp1206.game;
import javafx.beans.property.IntegerProperty; import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty; 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 * 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 { public class Grid {
/**
* Logger
*/
private static final Logger logger = LogManager.getLogger(Grid.class);
/** /**
* The number of columns in this grid * 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 * Get the number of columns in this game
* @return number of columns * @return number of columns

View File

@@ -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.game.Game;
import uk.mgrove.ac.soton.comp1206.ui.GamePane; import uk.mgrove.ac.soton.comp1206.ui.GamePane;
import uk.mgrove.ac.soton.comp1206.ui.GameWindow; 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. * 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); var board = new GameBoard(game.getGrid(),gameWindow.getWidth()/2,gameWindow.getWidth()/2);
mainPane.setCenter(board); 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); 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() { public void setupGame() {
logger.info("Starting a new challenge"); logger.info("Starting a new challenge");

View File

@@ -8,6 +8,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.ui.GamePane; import uk.mgrove.ac.soton.comp1206.ui.GamePane;
import uk.mgrove.ac.soton.comp1206.ui.GameWindow; 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. * 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"); var button = new Button("Play");
mainPane.setCenter(button); mainPane.setCenter(button);
Multimedia.playMusic("music/menu.mp3");
//Bind the button action to the startGame method in the menu //Bind the button action to the startGame method in the menu
button.setOnAction(this::startGame); button.setOnAction(this::startGame);
} }

View File

@@ -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);
}
}

View File

@@ -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();
}
}