Finished online scores leaderboard

This commit is contained in:
Alexander Berry-Roe 2024-04-24 15:08:51 +01:00
parent e55dace14d
commit 389e0fb41f
19 changed files with 389 additions and 75 deletions

2
META-INF/MANIFEST.MF Normal file
View File

@ -0,0 +1,2 @@
Manifest-Version: 1.0

View File

@ -160,6 +160,9 @@ public class GameBlock extends Canvas {
gc.strokeRect(0,0,width,height);
}
/**
* Paint the blocks on mouse hover
*/
public void paintHover() {
GraphicsContext gc = getGraphicsContext2D();
gc.setFill(Color.color(1.0, 1.0, 1.0, 0.5));
@ -198,10 +201,17 @@ public class GameBlock extends Canvas {
value.bind(input);
}
/**
* Paint the center grid square with a grey circle
* @param centre
*/
public void setPaintCentre(boolean centre) {
this.centre = centre;
}
/**
* Used to set a circle in the center of the grid
*/
private void fillCenter(){
GraphicsContext gc = getGraphicsContext2D();
gc.setFill(Color.color(1.0, 1.0, 1.0, 0.5));

View File

@ -0,0 +1,47 @@
package uk.ac.soton.comp1206.component;
import javafx.application.Platform;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.ListChangeListener;
import javafx.geometry.Pos;
import javafx.scene.layout.VBox;
import javafx.util.Pair;
import javafx.scene.text.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.ac.soton.comp1206.scene.ScoresScene;
public class Leaderboard extends VBox {
Logger logger = LogManager.getLogger(ScoresScene.class);
private SimpleListProperty<Pair<String, Integer>> leaderboard;
public Leaderboard(SimpleListProperty<Pair<String, Integer>> leaderboard) {
this.leaderboard = leaderboard;
this.leaderboard.addListener((ListChangeListener<Pair<String, Integer>>) c -> {
Platform.runLater(() -> updateLeaderBoard(c));
});
}
private void updateLeaderBoard(ListChangeListener.Change<? extends Pair<String, Integer>> change) {
logger.info("Updating leaderboard");
if (change.next()) {
if (change.wasPermutated() || change.wasReplaced() || change.wasRemoved()) {
super.getChildren().clear();
}
if (change.wasAdded()) {
for (Pair<String, Integer> pair : change.getAddedSubList()) {
var text = new Text(pair.getKey() + ": " + pair.getValue());
text.getStyleClass().add("leaderboard");
super.getChildren().add(text);
}
}
}
}
}

View File

@ -4,6 +4,10 @@ import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
public class Timer extends Rectangle {
/**
* Setup basic timer colour and height
*/
public Timer() {
this.setHeight(25);
this.setFill(Color.GREEN);

View File

@ -1,5 +1,8 @@
package uk.ac.soton.comp1206.event;
public interface GameLoopHandler {
/**
* Used for game loop events
*/
public void gameLoop(int time);;
}

View File

@ -3,5 +3,9 @@ package uk.ac.soton.comp1206.event;
import javafx.scene.input.KeyEvent;
public interface KeyPressListener {
/**
* key pressed event
* @param e
*/
public void keyPressed(KeyEvent e);
}

View File

@ -1,5 +1,8 @@
package uk.ac.soton.comp1206.event;
public interface LooseALife {
/**
* Used to signal the UI when a life is lost
*/
public void loose();
}

View File

@ -1,5 +1,8 @@
package uk.ac.soton.comp1206.event;
public interface OutOfLives {
/**
* Sent when out of lives to signal game over
*/
public void OutOfLives();
}

View File

@ -162,48 +162,77 @@ public class Game {
return rows;
}
/**
* Create a new random piece
* @return the new piece
*/
public GamePiece spawnPiece() {
Random rand = new Random();
return GamePiece.createPiece(rand.nextInt(15));
}
/**
* Handles after Piece actions
*/
public void afterPiece () {
this.restartGameLoop();
this.incrementNextPieces();
}
/**
*
*/
public void incrementNextPieces() {
this.score.set(this.score.get() + grid.clearLines());
this.currentPiece = this.nextPiece;
this.nextPiece = this.spawnPiece();
}
/**
* Get current score
* @return InterProperty score
*/
public IntegerProperty getScore() {
return score;
}
/**
* Get number of remaining lives
* @return InterProperty lives
*/
public IntegerProperty getLives() {
return lives;
}
/**
* Get the level number
* @return
*/
public IntegerProperty getLevel() {
return level;
}
/**
* get the piece currently being played
* @return the piece
*/
public GamePiece getCurrentGamePiece() {
return currentPiece;
}
/**
* get the piece that is going to be played next
* @return the piece
*/
public GamePiece getNextGamePiece() {
return nextPiece;
}
/**
* Swap the current and next game pieces
*/
public void swapNextPieces() {
GamePiece tmpPiece = this.nextPiece;
this.nextPiece = this.currentPiece;
@ -211,18 +240,27 @@ public class Game {
}
/**
* Starts the game loop
*/
public void startGameLoop() {
logger.info("Starting game loop");
this.nextLoop = this.executor.schedule(this::gameLoop, getTimerDelay(), TimeUnit.MILLISECONDS);
this.gameLoopHandler.gameLoop(this.getTimerDelay());
}
/**
* Restarts the game loop
*/
public void restartGameLoop() {
logger.info("Restarting game loop");
this.nextLoop.cancel(false);
this.startGameLoop();
}
/**
* Stops the game loop
*/
public void killGameLoop() {
logger.info("Killing game loop");
this.nextLoop.cancel(false);
@ -244,18 +282,34 @@ public class Game {
return;
}
public int getTimerDelay(){
/**
* Calculate the get the timer delay (the amount of time the player has to play the piece)
* @return the time in milliseconds
*/
protected int getTimerDelay(){
return Math.max(12000 - 500 * this.getLevel().get(), 2500);
}
/**
* Setup event handler for gameLoop
* @param gameLoopHandler
*/
public void setOnGameLoop(GameLoopHandler gameLoopHandler) {
this.gameLoopHandler = gameLoopHandler;
}
/**
* Setup event handler for out of lives event
* @param outOfLives
*/
public void setOnOutOfLives(OutOfLives outOfLives) {
this.outOfLives = outOfLives;
}
/**
* Setup event handler for a loss of life event, when the player looses a life
* @param looseALife
*/
public void setOnLossALife(LooseALife looseALife) {
this.looseALife = looseALife;
}

View File

@ -109,6 +109,13 @@ public class Grid {
return rows;
}
/**
* Checks whether the piece can be placed on the gird
* @param gamePiece
* @param pieceX
* @param pieceY
* @return true if piece can be played
*/
public boolean canPlayPiece(GamePiece gamePiece, int pieceX, int pieceY){
//Calculate the offset due to middle piece
pieceX --;
@ -133,6 +140,12 @@ public class Grid {
return true;
}
/**
* Places piece on the grid
* @param gamePiece
* @param pieceX
* @param pieceY
*/
public void addPiece(GamePiece gamePiece, int pieceX, int pieceY){
pieceX --;
pieceY --;
@ -146,6 +159,10 @@ public class Grid {
}
}
/**
* Checks which lines can be cleared
* @return number of blocks cleared
*/
public int clearLines() {
//List of cords to clear
HashSet<int[]> cordsToRemove = new HashSet<>();
@ -193,6 +210,9 @@ public class Grid {
return cordsToRemove.size();
}
/**
* Clears the whole board
*/
public void clear() {
for(int x = 0; x < cols; x++) {
for(int y = 0; y < rows; y++) {

View File

@ -1,7 +1,10 @@
package uk.ac.soton.comp1206.network;
import com.neovisionaries.ws.client.*;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.scene.control.Alert;
import javafx.util.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.ac.soton.comp1206.event.CommunicationsListener;
@ -120,4 +123,5 @@ public class Communicator {
}
}
}

View File

@ -9,7 +9,9 @@ import uk.ac.soton.comp1206.ui.GameWindow;
* A Base Scene used in the game. Handles common functionality between all scenes.
*/
public abstract class BaseScene {
/**
* The game window
*/
protected final GameWindow gameWindow;
protected GamePane root;

View File

@ -198,11 +198,17 @@ public class ChallengeScene extends BaseScene {
}
/**
* Handling loss of life events
*/
private void lossOfLife() {
this.game.incrementNextPieces();
this.updateNextPieces();
}
/**
* When the game is over
*/
private void gameOver() {
//This is required so that it runs on the javaFX thread and not in the gameLoop thread
Platform.runLater(() -> {
@ -211,12 +217,18 @@ public class ChallengeScene extends BaseScene {
});
}
/**
* Handle the next piece being clicked
* @param mouseEvent
*/
private void nextPieceClicked(MouseEvent mouseEvent) {
this.rotateNext();
}
//Handle when next piece 2 is clicked
/**
* Handle when the second next piece is clicked
* @param mouseEvent
*/
private void nextPiece2Clicked(MouseEvent mouseEvent) {
this.swampNextPieces();
}
@ -233,7 +245,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");
@ -242,6 +254,10 @@ public class ChallengeScene extends BaseScene {
game = new Game(5, 5);
}
/**
*
* @param time
*/
protected void gameLoop(int time) {
logger.info("Game loop");
var timeline = new Timeline(
@ -250,6 +266,9 @@ public class ChallengeScene extends BaseScene {
timeline.play();
}
/**
* Renders the next piece on the UI
*/
public void updateNextPieces() {
this.nextPiece.setPiece(this.game.getCurrentGamePiece());
this.nextPiece2.setPiece(this.game.getNextGamePiece());
@ -275,23 +294,43 @@ public class ChallengeScene extends BaseScene {
}
});
this.scene.setOnMouseClicked(e -> {
if(e.isSecondaryButtonDown()) {
logger.info("Secondary button pressed");
this.rotateNext();
}
});
}
/**
*
* @return
*/
public GameBoard getNextPiece() {
return nextPiece;
}
/**
* Swap the next pieces in the queue
*/
public void swampNextPieces() {
this.game.swapNextPieces();
this.updateNextPieces();
}
/**
* Rotates the piece that is next up
*/
public void rotateNext() {
this.game.getCurrentGamePiece().rotate();
this.updateNextPieces();
}
//Keyboard control functions
/**
* Handles key presses for the game
* @param keyEvent
*/
public void handleKeyPressed(KeyEvent keyEvent) {
logger.info("Handling key pressed");
if(keyEvent.getCode() == KeyCode.SPACE || keyEvent.getCode() == KeyCode.R) {
@ -299,8 +338,15 @@ public class ChallengeScene extends BaseScene {
this.updateNextPieces();
} else if(keyEvent.getCode() == KeyCode.ENTER) {
}
} else if(keyEvent.getCode() == KeyCode.Q || keyEvent.getCode() == KeyCode.OPEN_BRACKET || keyEvent.getCode() == KeyCode.Z) {
logger.info("Rotating left");
this.game.getCurrentGamePiece().rotate();
this.updateNextPieces();
return;
} else if(keyEvent.getCode() == KeyCode.E || keyEvent.getCode() == KeyCode.C || keyEvent.getCode() == KeyCode.CLOSE_BRACKET);
logger.info("Rotating right");
this.game.getCurrentGamePiece().rotate(3);
this.updateNextPieces();
}
}

View File

@ -25,11 +25,17 @@ public class HowToPlay extends BaseScene {
}
/**
* Initialise user interface, add event listeners.
*/
@Override
public void initialise() {
this.scene.setOnKeyPressed(e -> this.gameWindow.startMenu());
}
/**
* Build the user interface
*/
@Override
public void build() {
logger.info("Building how to play scene");

View File

@ -1,62 +0,0 @@
package uk.ac.soton.comp1206.scene;
import javafx.geometry.Pos;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javafx.scene.text.*;
import uk.ac.soton.comp1206.ui.GamePane;
import uk.ac.soton.comp1206.ui.GameWindow;
public class LeaderBoard extends BaseScene {
Logger logger = LogManager.getLogger(LeaderBoard.class);
/**
* Create a new scene, passing in the GameWindow the scene will be displayed in
*
* @param gameWindow the game window
*/
public LeaderBoard(GameWindow gameWindow) {
super(gameWindow);
}
@Override
public void initialise() {
this.scene.setOnKeyPressed(e -> this.gameWindow.startMenu());
}
@Override
public void build() {
logger.info("Building " + this.getClass().getName());
root = new GamePane(gameWindow.getWidth(), gameWindow.getHeight());
var leaderboardPane = new StackPane();
leaderboardPane.setMaxWidth(gameWindow.getWidth());
leaderboardPane.setMaxHeight(gameWindow.getHeight());
leaderboardPane.getStyleClass().add("menu-background");
root.getChildren().add(leaderboardPane);
var leaderboardBox = new VBox();
leaderboardPane.getChildren().add(leaderboardBox);
// Set alignment of leaderboardBox to center
leaderboardBox.setAlignment(Pos.TOP_CENTER);
ImageView title = new ImageView(new Image("file:src/main/resources/images/TetrECS.png"));
title.setPreserveRatio(true);
title.setFitWidth(gameWindow.getWidth() - 100);
leaderboardBox.getChildren().add(title);
Text highScore = new Text("High Scores");
highScore.getStyleClass().add("big-heading");
leaderboardBox.getChildren().add(highScore);
var localScoreHeading = new Text("Local Scores");
localScoreHeading.getStyleClass().add("heading");
leaderboardBox.getChildren().add(localScoreHeading);
}
}

View File

@ -90,6 +90,11 @@ public class MenuScene extends BaseScene {
leaderboardButton.setFocusTraversable(false);
menuBox.getChildren().add(leaderboardButton);
var settingsButton = new Button("Settings");
settingsButton.getStyleClass().add("menu-item");
settingsButton.setFocusTraversable(false);
menuBox.getChildren().add(settingsButton);
var quitButton = new Button("Quit");
quitButton.getStyleClass().add("menu-item");
quitButton.setFocusTraversable(false);
@ -98,6 +103,7 @@ public class MenuScene extends BaseScene {
//Bind the button action to the startGame method in the menu
singlePlayerButton.setOnAction(this::startGame);
multiPlayerButton.setOnAction(this::startGame);

View File

@ -0,0 +1,160 @@
package uk.ac.soton.comp1206.scene;
import javafx.application.Platform;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.control.ListView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.util.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javafx.scene.text.*;
import uk.ac.soton.comp1206.network.Communicator;
import uk.ac.soton.comp1206.ui.GamePane;
import uk.ac.soton.comp1206.ui.GameWindow;
import uk.ac.soton.comp1206.component.Leaderboard;
import java.util.concurrent.CompletableFuture;
public class ScoresScene extends BaseScene {
Logger logger = LogManager.getLogger(ScoresScene.class);
private SimpleListProperty<Pair<String, Integer>> localScores = new SimpleListProperty<>(FXCollections.observableArrayList());
private SimpleListProperty<Pair<String, Integer>> onlineScores = new SimpleListProperty<>(FXCollections.observableArrayList());
private Leaderboard onlineLeaderboard = new Leaderboard(onlineScores);
/**
* Create a new scene, passing in the GameWindow the scene will be displayed in
*
* @param gameWindow the game window
*/
public ScoresScene(GameWindow gameWindow) {
super(gameWindow);
}
@Override
public void initialise() {
this.onlineScores = this.getOnlineScores();
this.scene.setOnKeyPressed(e -> this.gameWindow.startMenu());
}
@Override
public void build() {
logger.info("Building " + this.getClass().getName());
root = new GamePane(gameWindow.getWidth(), gameWindow.getHeight());
var leaderboardPane = new StackPane();
leaderboardPane.setMaxWidth(gameWindow.getWidth());
leaderboardPane.setMaxHeight(gameWindow.getHeight());
leaderboardPane.getStyleClass().add("menu-background");
root.getChildren().add(leaderboardPane);
var leaderboardBox = new VBox();
leaderboardPane.getChildren().add(leaderboardBox);
// Set alignment of leaderboardBox to center
leaderboardBox.setAlignment(Pos.TOP_CENTER);
ImageView title = new ImageView(new Image("file:src/main/resources/images/TetrECS.png"));
title.setPreserveRatio(true);
title.setFitWidth(gameWindow.getWidth() - 100);
leaderboardBox.getChildren().add(title);
Text highScore = new Text("High Scores");
highScore.getStyleClass().add("big-heading");
leaderboardBox.getChildren().add(highScore);
var leaderboardHbox = new HBox();
leaderboardHbox.setMinWidth(gameWindow.getWidth());
leaderboardHbox.setAlignment(Pos.TOP_CENTER);
leaderboardBox.getChildren().add(leaderboardHbox);
var localScoreBox = new VBox();
localScoreBox.setAlignment(Pos.CENTER);
localScoreBox.setPrefWidth(gameWindow.getWidth() / 2);
leaderboardHbox.getChildren().add(localScoreBox);
var onlineScoreBox = new VBox();
onlineScoreBox.setAlignment(Pos.CENTER);
onlineScoreBox.setPrefWidth(gameWindow.getWidth() / 2);
leaderboardHbox.getChildren().add(onlineScoreBox);
var localScoreHeading = new Text("Local Scores");
localScoreHeading.getStyleClass().add("heading");
localScoreBox.getChildren().add(localScoreHeading);
var onlineScoreHeading = new Text("Online Scores");
onlineScoreHeading.getStyleClass().add("heading");
onlineScoreBox.getChildren().add(onlineScoreHeading);
onlineScoreBox.getChildren().add(onlineLeaderboard);
onlineLeaderboard.setAlignment(Pos.CENTER);
}
/**
* Fetches online scores from the server
* @return
*/
private SimpleListProperty<Pair<String, Integer>> getOnlineScores() {
Communicator communicator = gameWindow.getCommunicator();
CompletableFuture<SimpleListProperty<Pair<String, Integer>>> futureScores = new CompletableFuture<>();
communicator.addListener(response -> {
if (response.startsWith("HISCORES")) {
logger.info("HISCORES received");
String scoreString = response.substring("HISCORES".length()).trim();
futureScores.complete(parseHighScores(scoreString, onlineScores
));
}
});
communicator.send("HISCORES");
//Wait for response
try {
return futureScores.get();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* Takes a string of high scores and converts it
* @param scoresString
* @return SimpleListProperty of high scores
*/
private SimpleListProperty<Pair<String, Integer>> parseHighScores(String scoresString, SimpleListProperty<Pair<String, Integer>> highScoresList) {
String[] lines = scoresString.split("\n");
for (String line : lines) {
String[] parts = line.split(":");
if (parts.length == 2) {
String name = parts[0].trim();
int score = Integer.parseInt(parts[1].trim());
highScoresList.add(new Pair<>(name, score));
}
}
return highScoresList;
}
}

View File

@ -74,7 +74,7 @@ public class GameWindow {
}
public void showLeaderBoard() {
loadScene(new LeaderBoard(this));
loadScene(new ScoresScene(this));
}
/**

View File

@ -164,6 +164,8 @@
.leaderboard {
-fx-font-size: 16px;
-fx-fill: yellow;
-fx-font-family: 'Orbitron';
}
.gameBox {