[FEAT] Show other players' boards in multiplayer

This commit is contained in:
2023-04-22 05:39:25 +01:00
parent cc205f5c6e
commit f10cdec09b
12 changed files with 271 additions and 35 deletions

View File

@@ -70,7 +70,8 @@ public class Chat extends VBox {
messagesContainer.setContent(messages);
messagesContainer.setFitToWidth(true);
messagesContainer.setFitToHeight(true);
messagesContainer.setPrefHeight(320);
messagesContainer.setPrefHeight(280);
messagesContainer.setMaxHeight(280);
messagesContainer.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
messagesContainer.setVvalue(1.0);
messagesContainer.getStyleClass().add("scroller");

View File

@@ -1,5 +1,7 @@
package uk.mgrove.ac.soton.comp1206.component;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
@@ -199,6 +201,14 @@ public class GameBoard extends GridPane {
return block;
}
/**
* Get grid value property
* @return properties for value of block at given location in grid
*/
public IntegerProperty getGridProperty(int x, int y) {
return grid.getGridProperty(x,y);
}
/**
* Set the listener to handle an event when a block is clicked
* @param listener listener to add
@@ -305,4 +315,20 @@ public class GameBoard extends GridPane {
currentPiece = newPiece;
}
/**
* Get current game piece
* @return current piece
*/
public GamePiece getCurrentPiece() {
return currentPiece;
}
/**
* Set whether board should be focussable
* @param enableFocus whether board should be focussable
*/
public void enableFocus(boolean enableFocus) {
this.enableFocus = enableFocus;
}
}

View File

@@ -33,6 +33,7 @@ public class Leaderboard extends ScoresList {
title = new Text(titleText);
title.getStyleClass().add("heading");
getChildren().add(title);
setSpacing(4);
reveal();
scores.addListener((ListChangeListener<? super Pair<String,Pair<Integer, Integer>>>) change -> Platform.runLater(() -> {

View File

@@ -0,0 +1,18 @@
package uk.mgrove.ac.soton.comp1206.event;
import javafx.beans.property.SimpleIntegerProperty;
import uk.mgrove.ac.soton.comp1206.component.GameBoard;
/**
* Listener for player game boards
*/
public interface PlayerGameBoardListener {
/**
* Action the listener with details of the board
* @param username player's username
* @param gridProperties properties for board grid contents
*/
void add(String username, SimpleIntegerProperty[][] gridProperties);
}

View File

@@ -4,6 +4,7 @@ import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleMapProperty;
import javafx.util.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -96,7 +97,12 @@ public class Game {
/**
* Listener for showing scores scene
*/
private ShowScoresSceneListener showScoresListener;
protected ShowScoresSceneListener showScoresListener;
/**
* Listener for when new player board is added
*/
protected PlayerGameBoardListener playerBoardAddedListener;
/**
* Create a new game with the specified rows and columns. Creates a corresponding grid model.
@@ -167,7 +173,7 @@ public class Game {
/**
* Game loop - ongoing time-based functionality
*/
private void gameLoop() {
protected void gameLoop() {
logger.info("Executing game loop");
setLives(getLives() - 1);
nextPiece();
@@ -512,6 +518,22 @@ public class Game {
return null;
}
/**
* Get the player boards property - always null in this class
* @return property for player boards
*/
public SimpleMapProperty<String, SimpleIntegerProperty[][]> playerBoardsProperty() {
return null;
}
/**
* Set listener for when new player board is added
* @param listener
*/
public void setOnPlayerBoardAdded(PlayerGameBoardListener listener) {
this.playerBoardAddedListener = listener;
}
/**
* Set listener for game failure - but in this class does nothing
* @param listener listener to set

View File

@@ -207,7 +207,7 @@ public class Grid {
* @param y y-coordinate of piece centre
*/
public void previewPiece(GamePiece piece, int x, int y) {
if (!canPlayPiece(piece, x, y)) return;
if (piece == null || !canPlayPiece(piece, x, y)) return;
logger.info("Previewing piece {} at {},{}", piece, x, y);
int value = piece.getValue();

View File

@@ -1,16 +1,25 @@
package uk.mgrove.ac.soton.comp1206.game;
import javafx.application.Platform;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleMapProperty;
import javafx.collections.FXCollections;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.util.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.event.GameFailureListener;
import uk.mgrove.ac.soton.comp1206.network.Communicator;
import uk.mgrove.ac.soton.comp1206.util.Multimedia;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
@@ -33,15 +42,20 @@ public class MultiplayerGame extends Game {
private final BlockingQueue<GamePiece> pieceQueue = new LinkedBlockingQueue<>();
/**
* Scores for the leaderboard
* Scores for the leaderboard, with structure username, score, lives
*/
private final SimpleListProperty<Pair<String,Pair<Integer,Integer>>> leaderboardScores = new SimpleListProperty<>(FXCollections.observableArrayList(new ArrayList<>()));
private final SimpleListProperty<Pair<String,Pair<Integer,Integer>>> leaderboardScores = new SimpleListProperty<>(FXCollections.observableArrayList());
/**
* Listener for game failing
*/
private GameFailureListener gameFailureListener;
/**
* Mapping of usernames to player boards
*/
private final SimpleMapProperty<String, SimpleIntegerProperty[][]> playerBoards = new SimpleMapProperty<>(FXCollections.observableHashMap());
/**
* Create a new game with the specified rows and columns. Creates a corresponding grid model.
*
@@ -125,11 +139,34 @@ public class MultiplayerGame extends Game {
break;
}
}
} else if (message.startsWith("BOARD ")) {
var info = message.replaceFirst("BOARD ", "").split(":");
var gridValues = info[1].split(" ");
if (playerBoards.containsKey(info[0])) {
logger.info("Updating board for: {}", info[0]);
for (var x = 0; x < playerBoards.get(info[0]).length; x++) {
for (var y = 0; y < playerBoards.get(info[0])[0].length; y++) {
playerBoards.get(info[0])[x][y].set(Integer.parseInt(gridValues[playerBoards.get(info[0])[0].length * x + y]));
}
}
} else {
logger.info("Adding board for: {}", info[0]);
var dimensions = (int) Math.sqrt(gridValues.length);
SimpleIntegerProperty[][] newProperties = new SimpleIntegerProperty[dimensions][dimensions];
for (var x = 0; x < dimensions; x++) {
for (var y = 0; y < dimensions; y++) {
newProperties[x][y] = new SimpleIntegerProperty(Integer.parseInt(gridValues[grid.getCols() * x + y]));
}
}
playerBoards.put(info[0], newProperties);
if (playerBoardAddedListener != null) playerBoardAddedListener.add(info[0], newProperties);
}
logger.info("Player boards: {}", playerBoards.get());
}
}
/**
* Parse high scores from a string to list property
* Parse high scores from a string to list property, with structure username, score, lives
* @param data string to parse from
* @return list property containing scores loaded
*/
@@ -141,7 +178,7 @@ public class MultiplayerGame extends Game {
while (scanner.hasNextLine()) {
var line = scanner.nextLine();
if (line.matches("^.+:[0-9]+:([0-9]+|DEAD)$")) {
if (line.matches("^.+:[0-9]+:((-?[0-9]+)|DEAD)$")) {
var info = line.split(":");
var lives = info[2].equals("DEAD") ? -1 : Integer.parseInt(info[2]);
scores.add(new Pair<>(info[0], new Pair<>(Integer.valueOf(info[1]), lives)));
@@ -162,15 +199,41 @@ public class MultiplayerGame extends Game {
}
/**
* End game
* Get the player boards property
* @return property for player boards
*/
@Override
public void endGame() {
super.endGame();
public SimpleMapProperty<String, SimpleIntegerProperty[][]> playerBoardsProperty() {
return playerBoards;
}
/**
* Notify server that player is dead
*/
private void die() {
logger.info("Sending die message to server");
communicator.send("DIE");
}
/**
* Game loop - ongoing time-based functionality
*/
@Override
protected void gameLoop() {
logger.info("Executing game loop");
setLives(getLives() - 1);
nextPiece();
setMultiplier(1);
Multimedia.playAudio("sounds/lifelose.wav");
if(getLives() < 0) {
die();
logger.info("Ending game");
endGame();
if (showScoresListener != null) Platform.runLater(() -> showScoresListener.show(this));
}
else scheduleGameLoop();
}
/**
* Update the score, multiplier, and level depending on the number of lines and blocks that have been cleared
* @param lines number of lines cleared

View File

@@ -78,6 +78,11 @@ public class ChallengeScene extends BaseScene {
*/
protected final IntegerProperty highScore = new SimpleIntegerProperty(0);
/**
* Main stack pane for scene
*/
protected StackPane challengePane;
/**
* Create a new Single Player challenge scene
* @param gameWindow the Game Window
@@ -98,7 +103,7 @@ public class ChallengeScene extends BaseScene {
root = new GamePane(gameWindow.getWidth(),gameWindow.getHeight());
var challengePane = new StackPane();
challengePane = new StackPane();
challengePane.setMaxWidth(gameWindow.getWidth());
challengePane.setMaxHeight(gameWindow.getHeight());
challengePane.getStyleClass().add("menu-background");
@@ -163,9 +168,21 @@ public class ChallengeScene extends BaseScene {
switch (event.getCode()) {
case ESCAPE -> returnToMenu();
case ENTER, X -> game.dropPiece(board.getXFocus(),board.getYFocus());
case SPACE, R -> game.swapPieces();
case OPEN_BRACKET, Q, Z -> game.rotateCurrentPieceAnticlockwise();
case CLOSE_BRACKET, E, C -> game.rotateCurrentPiece();
case SPACE, R -> {
game.swapPieces();
game.getGrid().clearPreview();
game.getGrid().previewPiece(board.getCurrentPiece(), board.getXFocus(), board.getYFocus());
}
case OPEN_BRACKET, Q, Z -> {
game.rotateCurrentPieceAnticlockwise();
game.getGrid().clearPreview();
game.getGrid().previewPiece(board.getCurrentPiece(), board.getXFocus(), board.getYFocus());
}
case CLOSE_BRACKET, E, C -> {
game.rotateCurrentPiece();
game.getGrid().clearPreview();
game.getGrid().previewPiece(board.getCurrentPiece(), board.getXFocus(), board.getYFocus());
}
case LEFT, A -> board.changeXFocus(-1);
case RIGHT, D -> board.changeXFocus(1);
case DOWN, S -> board.changeYFocus(1);

View File

@@ -79,6 +79,11 @@ public class LobbyScene extends BaseScene {
*/
private final HBox channelFunctionButtons = new HBox();
/**
* Timer to request list of channels from server
*/
private Timer listChannelsRequest;
/**
* Create a new scene, passing in the GameWindow the scene will be displayed in
*
@@ -143,7 +148,7 @@ public class LobbyScene extends BaseScene {
gameWindow.getCommunicator().addListener(this::handleCommunicatorMessage);
var listChannelsRequest = new Timer("Request list of channels from communicator");
listChannelsRequest = new Timer("Request list of channels from communicator");
listChannelsRequest.schedule(new TimerTask() {
@Override
public void run() {
@@ -227,6 +232,7 @@ public class LobbyScene extends BaseScene {
});
} else if (message.equals("HOST")) {
logger.info("User is host of current channel");
if (!isChannelHost) {
isChannelHost = true;
Platform.runLater(() -> {
logger.info("Current channel host status: {}", isChannelHost);
@@ -235,6 +241,7 @@ public class LobbyScene extends BaseScene {
startGame.setOnMouseClicked((event) -> gameWindow.getCommunicator().send("START"));
channelFunctionButtons.getChildren().add(0, startGame);
});
}
} else if (message.matches("^NICK .+:.+$")) {
Platform.runLater(() -> {
var usernames = message.replaceFirst("NICK ", "").split(":");
@@ -255,7 +262,10 @@ public class LobbyScene extends BaseScene {
});
} else if (message.equals("START")) {
logger.info("Starting game");
Platform.runLater(gameWindow::startMultiplayerGame);
Platform.runLater(() -> {
listChannelsRequest.cancel();
gameWindow.startMultiplayerGame();
});
}
}

View File

@@ -1,9 +1,16 @@
package uk.mgrove.ac.soton.comp1206.scene;
import javafx.application.Platform;
import javafx.beans.property.MapProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleMapProperty;
import javafx.collections.FXCollections;
import javafx.geometry.Pos;
import javafx.scene.control.Separator;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.component.*;
@@ -30,6 +37,16 @@ public class MultiplayerScene extends ChallengeScene {
*/
private Leaderboard leaderboard;
/**
* Other players' live boards
*/
private MapProperty<String, SimpleIntegerProperty[][]> playerBoards = new SimpleMapProperty<>(FXCollections.observableHashMap());
/**
* Grid of other players' live boards
*/
private final GridPane playerBoardGrid = new GridPane();
/**
* Create a new multiplayer challenge scene
*
@@ -50,6 +67,36 @@ public class MultiplayerScene extends ChallengeScene {
game = new MultiplayerGame(5, 5, gameWindow.getCommunicator());
game.setOnGameFail(() -> Platform.runLater(gameWindow::startMenu));
game.setOnPlayerBoardAdded((username, gridProperties) -> {
Platform.runLater(() -> {
var newGameBoard = new GameBoard(gridProperties.length, gridProperties[0].length, 100, 100);
newGameBoard.enableFocus(false);
for (var x = 0; x < gridProperties.length; x++) {
for (var y = 0; y < gridProperties[0].length; y++) {
newGameBoard.getGridProperty(x,y).bindBidirectional(gridProperties[x][y]);
}
}
var leaderboardScores = game.leaderboardScoresProperty();
var boardGridDimensions = (int) Math.sqrt(leaderboardScores.size());
var playerIndex = -1;
for (var player : leaderboardScores) {
playerIndex++;
if (player.getKey().equals(username)) break;
}
var rowIndex = playerIndex / boardGridDimensions;
var columnIndex = playerIndex - rowIndex * boardGridDimensions;
var newBoardTitle = new Text(username);
newBoardTitle.setTextAlignment(TextAlignment.CENTER);
newBoardTitle.getStyleClass().add("heading");
logger.debug("Creating board for: {} at row index: {}, column index: {}", username, rowIndex, columnIndex);
var newBoardContainer = new VBox(newBoardTitle, newGameBoard);
newBoardContainer.setAlignment(Pos.CENTER);
newBoardContainer.setSpacing(4);
playerBoardGrid.add(newBoardContainer, 1, 1);
});
});
gameWindow.getCommunicator().send("SCORES");
}
@@ -63,12 +110,32 @@ public class MultiplayerScene extends ChallengeScene {
leftMenu.setSpacing(8);
leftMenu.setMaxWidth(200);
leftMenu.setPrefWidth(200);
var viewOtherBoards = new Text("See others");
viewOtherBoards.getStyleClass().add("channelItem");
viewOtherBoards.setOnMouseClicked((event) -> {
Platform.runLater(() -> {
var returnButton = new Text("Back");
returnButton.getStyleClass().add("channelItem");
var playerBoardTitle = new Text("All Boards");
playerBoardTitle.getStyleClass().add("title");
playerBoardTitle.setTextAlignment(TextAlignment.CENTER);
var playerBoardGridContainer = new VBox(returnButton, playerBoardTitle, playerBoardGrid);
playerBoardGridContainer.setAlignment(Pos.CENTER);
playerBoardGridContainer.setFillWidth(true);
playerBoardGridContainer.setPrefHeight(Double.MAX_VALUE);
playerBoardGridContainer.setSpacing(12);
playerBoardGridContainer.getStyleClass().add("overlay");
returnButton.setOnMouseClicked((returnEvent) -> Platform.runLater(() -> challengePane.getChildren().remove(playerBoardGridContainer)));
playerBoardGrid.setAlignment(Pos.CENTER);
challengePane.getChildren().add(playerBoardGridContainer);
});
});
chat = new Chat(gameWindow.getCommunicator(), true);
chat.setChatFocusTraversable(false);
leaderboard = new Leaderboard("Leaderboard");
var chatTitle = new Text("Chat");
chatTitle.getStyleClass().add("heading");
leftMenu.getChildren().addAll(leaderboard, new Separator(), chatTitle, chat);
leftMenu.getChildren().addAll(viewOtherBoards, leaderboard, new Separator(), chatTitle, chat);
mainPane.setLeft(leftMenu);
leaderboard.bindLeaderboardScores(game.leaderboardScoresProperty());
mainPane.requestFocus();

View File

@@ -299,6 +299,7 @@ public class ScoresScene extends BaseScene {
userNamePrompt.getChildren().addAll(userNameInput, userNameSubmit);
highScorePromptContainer = new VBox(usernameTitle, userNamePrompt);
highScorePromptContainer.setSpacing(20);
highScorePromptContainer.setAlignment(Pos.CENTER);
scoresPane.getChildren().addAll(highScorePromptContainer);
StackPane.setAlignment(highScorePromptContainer, Pos.CENTER);
}
@@ -338,6 +339,7 @@ public class ScoresScene extends BaseScene {
* @param score high score to save
*/
private void saveHighScore(String username, int score) {
if (!game.getClass().equals(MultiplayerGame.class)) {
logger.info("Saving high score: {} for user: {}", score, username);
for (var i = 0; i <= localScores.getSize(); i++) {
@@ -350,6 +352,7 @@ public class ScoresScene extends BaseScene {
// so while it won't get stored, this isn't an issue
writeScores("scores.txt", localScores);
}
}
private void writeOnlineScore(String username, int score) {
logger.info("Saving online high score: {} for user: {}", score, username);

View File

@@ -6,6 +6,10 @@
-fx-background-color: black;
}
.overlay {
-fx-background-color: rgba(0,0,0,0.7);
}
Text {
-fx-fill: white;
-fx-font-family: 'Orbitron';
@@ -18,6 +22,9 @@ Label {
.menu-background {
-fx-background-image: url("../images/1.jpg");
-fx-background-size: cover;
}
BorderPane {
-fx-padding: 20;
}
@@ -195,6 +202,7 @@ Label {
}
TextField, .text-field {
-fx-font-family: 'Orbitron';
-fx-border-color: white;
-fx-border-width: 1px;
-fx-background-color: rgba(0,0,0,0.5);