Initial commit

This commit is contained in:
2023-03-24 15:48:54 +00:00
commit d77738eb38
26 changed files with 1957 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
package uk.ac.soton.comp1206;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
/**
* JavaFX App
*/
public class App extends Application {
@Override
public void start(Stage stage) {
var javaVersion = SystemInfo.javaVersion();
var javafxVersion = SystemInfo.javafxVersion();
var label = new Label("Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".");
var scene = new Scene(new StackPane(label), 640, 480);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}

View File

@@ -0,0 +1,13 @@
package uk.ac.soton.comp1206;
public class SystemInfo {
public static String javaVersion() {
return System.getProperty("java.version");
}
public static String javafxVersion() {
return System.getProperty("javafx.version");
}
}

View File

@@ -0,0 +1,252 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.animation.*;
import javafx.beans.property.DoubleProperty;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.SplitPane;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Window;
import javafx.util.Duration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.App;
import uk.mgrove.ac.soton.comp1206.network.Communicator;
import java.util.Arrays;
/**
* Base window class, providing template from which all windows should extend
*/
public abstract class BaseWindow extends Window {
protected static final Logger logger = LogManager.getLogger(ChatWindow.class);
protected final App app;
protected final Communicator communicator;
/**
* Duration over which menus should slide open/closed
*/
protected final static Duration menuTransitionDuration = Duration.millis(300);
/**
* Duration over which buttons should rotate - used for menu open/close buttons
*/
protected final static Duration buttonRotationDuration = Duration.millis(150);
/**
* Duration over which windows should fade in
*/
protected final static Duration windowFadeInDuration = Duration.millis(500);
@FXML
protected VBox rightPanel;
@FXML
protected VBox leftPanel;
@FXML
protected SplitPane mainSplitPane;
@FXML
protected ImageView showLeftPanel;
@FXML
protected ImageView showRightPanel;
@FXML
protected ImageView hideLeftPanel;
@FXML
protected ImageView hideRightPanel;
@FXML
protected StackPane mainStackPane;
protected Parent root;
protected Node rightPanelPane;
protected Node leftPanelPane;
protected double[] mainSplitPaneDividerPositions;
protected boolean[] sidePanelsVisible;
/**
* Initialise the window with an FXML template
* @param app app to use
* @param communicator communicator to use
* @param fxmlPath path to FXML file
*/
public BaseWindow(App app, Communicator communicator, String fxmlPath) {
this.app = app;
this.communicator = communicator;
//Load the Chat Window GUI
try {
//Instead of building this GUI programmatically, we are going to use FXML
var loader = new FXMLLoader(getClass().getResource(fxmlPath));
//Link the GUI in the FXML to this class
loader.setController(this);
root = loader.load();
} catch (Exception e) {
//Handle any exceptions with loading the FXML
logger.error("Unable to read file: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
root.setOpacity(0);
fadeInWindow();
//We are the login window
setScene(new Scene(root));
rightPanelPane = mainSplitPane.getItems().get(2);
leftPanelPane = mainSplitPane.getItems().get(0);
mainSplitPaneDividerPositions = mainSplitPane.getDividerPositions();
sidePanelsVisible = new boolean[2];
Arrays.fill(sidePanelsVisible, Boolean.TRUE);
mainStackPane.getChildren().removeAll(showLeftPanel, showRightPanel);
}
/**
* Fade the root node from invisible to visible
*/
protected void fadeInWindow() {
FadeTransition ft = new FadeTransition(windowFadeInDuration, root);
ft.setFromValue(0);
ft.setToValue(1);
ft.play();
}
/**
* Hide a given pane in a ScrollPane - used to hide menus
*
* @param panelPane the pane to hide
* @param dividerPositionProperty the property for the appropriate divider's position
* @param hideButton button to hide the pane
* @param showButton button to show the pane
* @param panelIndex index of the panel in local info stores (e.g. list of open/closed menus)
*/
protected void hidePanel(Pane panelPane, DoubleProperty dividerPositionProperty, Node hideButton, Node showButton, int panelIndex) {
panelPane.setMinWidth(0);
int finalDividerPosition = panelIndex == 0 ? 0 : 1;
KeyValue keyValue = new KeyValue(dividerPositionProperty, finalDividerPosition, Interpolator.EASE_BOTH);
Timeline hidePanel = new Timeline(new KeyFrame(menuTransitionDuration, keyValue));
RotateTransition rotateButton = new RotateTransition(buttonRotationDuration, hideButton);
rotateButton.setFromAngle(0);
rotateButton.setToAngle(panelIndex == 0 ? 180 : -180);
rotateButton.setOnFinished((event2) -> {
mainStackPane.getChildren().add(showButton);
mainStackPane.getChildren().remove(hideButton);
hideButton.setRotate(0);
});
hidePanel.play();
hidePanel.setOnFinished((event) -> {
rotateButton.play();
sidePanelsVisible[panelIndex] = false;
mainSplitPane.getItems().remove(panelPane);
if (panelIndex == 0 && sidePanelsVisible[1])
mainSplitPane.setDividerPosition(0, mainSplitPaneDividerPositions[1]);
});
}
/**
* Show a given pane in a ScrollPane - used to show menus
*
* @param panelPane the pane to show
* @param hideButton button to hide the pane
* @param showButton button to show the pane
* @param panelIndex index of the panel in local info stores (e.g. list of open/closed menus)
*/
protected void showPanel(Pane panelPane, Node hideButton, Node showButton, int panelIndex) {
if (panelIndex == 0) mainSplitPane.getItems().add(0, panelPane);
else mainSplitPane.getItems().add(panelPane);
DoubleProperty dividerPositionProperty = mainSplitPane.getDividers().get(panelIndex == 1 && !sidePanelsVisible[0] ? 0 : panelIndex).positionProperty();
int initialDividerPosition = panelIndex == 0 ? 0 : 1;
mainSplitPane.setDividerPosition(panelIndex == 1 && !sidePanelsVisible[0] ? 0 : panelIndex, initialDividerPosition);
KeyValue keyValue = new KeyValue(dividerPositionProperty, mainSplitPaneDividerPositions[panelIndex], Interpolator.EASE_BOTH);
Timeline showPanel = new Timeline(new KeyFrame(menuTransitionDuration, keyValue));
RotateTransition rotateButton = new RotateTransition(buttonRotationDuration, showButton);
rotateButton.setFromAngle(0);
rotateButton.setToAngle(panelIndex == 0 ? -180 : 180);
rotateButton.setOnFinished((event2) -> {
mainStackPane.getChildren().add(hideButton);
mainStackPane.getChildren().remove(showButton);
showButton.setRotate(0);
});
showPanel.play();
showPanel.setOnFinished((event1) -> {
rotateButton.play();
sidePanelsVisible[panelIndex] = true;
panelPane.setMinWidth(Region.USE_COMPUTED_SIZE);
if (panelIndex == 0 && sidePanelsVisible[1])
mainSplitPane.setDividerPosition(1, mainSplitPaneDividerPositions[1]);
});
}
/**
* Toggle the visibility of the right-hand sidebar
*/
protected void toggleRightPanel() {
if (sidePanelsVisible[1]) {
DoubleProperty positionProperty;
if (sidePanelsVisible[0]) {
mainSplitPaneDividerPositions[1] = mainSplitPane.getDividerPositions()[1];
positionProperty = mainSplitPane.getDividers().get(1).positionProperty();
} else {
mainSplitPaneDividerPositions[1] = mainSplitPane.getDividerPositions()[0];
positionProperty = mainSplitPane.getDividers().get(0).positionProperty();
}
hidePanel(rightPanel, positionProperty, hideRightPanel, showRightPanel, 1);
} else {
showPanel(rightPanel, hideRightPanel, showRightPanel, 1);
}
}
/**
* Toggle the visibility of the right-hand sidebar
*/
protected void toggleLeftPanel() {
if (sidePanelsVisible[0]) {
mainSplitPaneDividerPositions[0] = mainSplitPane.getDividerPositions()[0];
if (sidePanelsVisible[1]) mainSplitPaneDividerPositions[1] = mainSplitPane.getDividerPositions()[1];
hidePanel(leftPanel, mainSplitPane.getDividers().get(0).positionProperty(), hideLeftPanel, showLeftPanel, 0);
} else {
if (sidePanelsVisible[1]) mainSplitPaneDividerPositions[1] = mainSplitPane.getDividerPositions()[0];
showPanel(leftPanel, hideLeftPanel, showLeftPanel, 0);
}
}
/**
* Handle mouse click to toggle visibility of right-hand sidebar
*
* @param event mouse clicked
*/
public void toggleRightPanel(MouseEvent event) {
toggleRightPanel();
}
/**
* Handle mouse click to toggle visibility of right-hand sidebar
*
* @param event mouse clicked
*/
public void toggleLeftPanel(MouseEvent event) {
toggleLeftPanel();
}
}

View File

@@ -0,0 +1,262 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.animation.FadeTransition;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.util.Duration;
import uk.mgrove.ac.soton.comp1206.App;
import uk.mgrove.ac.soton.comp1206.network.Communicator;
import uk.mgrove.ac.soton.comp1206.utility.Utility;
import java.awt.event.ActionEvent;
import java.util.Arrays;
/**
* Chat window for displaying chat interface
*/
public class ChatWindow extends BaseWindow {
private final static Duration messageFadeInDuration = Duration.millis(150);
@FXML
private TextField usersSearchField;
@FXML
private Text myUsernameText;
@FXML
private TextField myUsernameInput;
@FXML
private ScrollPane messagesContainer;
@FXML
private Label noMessagesYetText;
@FXML
private VBox messages;
@FXML
private TextField messageToSend;
@FXML
private CheckBox audioEnabled;
@FXML
private HBox myUsernameInputContainer;
@FXML
private HBox myUsernameTextContainer;
@FXML
private VBox myUsernameContainer;
@FXML
private final UserList userList;
@FXML
private VBox rightPanel;
@FXML
private VBox leftPanel;
@FXML
private SplitPane mainSplitPane;
@FXML
private ImageView showLeftPanel;
@FXML
private ImageView showRightPanel;
@FXML
private ImageView hideLeftPanel;
@FXML
private ImageView hideRightPanel;
@FXML
private StackPane mainStackPane;
@FXML
private Button openGame;
private boolean messagesReceived;
private boolean scrollToBottom;
private boolean lastMessageFromMe;
/**
* Initialise the chat window
* @param app the app
* @param communicator the communicator
*/
public ChatWindow(App app, Communicator communicator) {
super(app, communicator, "/chat.fxml");
//Link the communicator to this window
communicator.setWindow(this);
getScene().addPostLayoutPulseListener(this::scrollToBottom);
StringProperty userSearchProperty = new SimpleStringProperty("");
userList = new UserList(userSearchProperty);
Platform.runLater(() -> rightPanel.getChildren().add(userList));
usersSearchField.textProperty().bindBidirectional(userSearchProperty);
usersSearchField.setOnKeyReleased((event) -> {
// refresh list of people
userList.updateDisplayedUsers();
});
hideMyUsernameInput(false);
rightPanelPane = mainSplitPane.getItems().get(2);
leftPanelPane = mainSplitPane.getItems().get(0);
mainSplitPaneDividerPositions = mainSplitPane.getDividerPositions();
sidePanelsVisible = new boolean[2];
Arrays.fill(sidePanelsVisible, Boolean.TRUE);
mainStackPane.getChildren().removeAll(showLeftPanel, showRightPanel);
messagesReceived = false;
myUsernameInput.textProperty().bindBidirectional(app.usernameProperty());
myUsernameText.textProperty().bind(app.usernameProperty());
audioEnabled.selectedProperty().bindBidirectional(Utility.audioEnabledProperty());
openGame.setOnAction((e) -> {
app.openGame();
});
communicator.addListener(this::receiveMessage);
Platform.runLater(() -> messageToSend.requestFocus());
}
/**
* Handle what happens when the user presses enter on the message to send field
*
* @param event key pressed
*/
@FXML
protected void handleUsernameKeypress(KeyEvent event) {
if (event.getCode() != KeyCode.ENTER) return;
// hide input
hideMyUsernameInput(true);
messageToSend.requestFocus();
}
/**
* Handle what happens when the user presses enter on the message to send field
*
* @param event key pressed
*/
@FXML
protected void handleMessageToSendKeypress(KeyEvent event) {
if (event.getCode() != KeyCode.ENTER) return;
sendCurrentMessage(messageToSend.getText());
}
/**
* Handle what happens when the user presses the send message button
*
* @param event button clicked
*/
@FXML
protected void handleSendMessage(ActionEvent event) {
String message = messageToSend.getText();
if (message.isBlank()) return;
sendCurrentMessage(message);
}
/**
* Handle event to show username input when button pressed
*
* @param event button clicked
*/
public void showMyUsernameInput(MouseEvent event) {
showMyUsernameInput();
}
/**
* Handle show username input
*/
public void showMyUsernameInput() {
myUsernameContainer.getChildren().remove(myUsernameTextContainer);
myUsernameContainer.getChildren().add(myUsernameInputContainer);
myUsernameInput.requestFocus();
myUsernameInput.end();
}
/**
* Handle event to hide username input when button pressed
*
* @param event button clicked
*/
public void hideMyUsernameInput(MouseEvent event) {
hideMyUsernameInput(true);
}
/**
* Handle hide username input, with option to only remove input container and not add text container
*
* @param showTextContainer add text container to window
*/
private void hideMyUsernameInput(boolean showTextContainer) {
myUsernameContainer.getChildren().remove(myUsernameInputContainer);
if (showTextContainer) {
myUsernameContainer.getChildren().add(myUsernameTextContainer);
messageToSend.requestFocus();
}
}
/**
* Handle an incoming message from the Communicator
*
* @param text The message that has been received, in the form User:Message
*/
public void receiveMessage(String text) {
if (!text.contains(":")) return;
if (!messagesReceived) {
messages.getChildren().remove(noMessagesYetText);
messagesReceived = true;
}
var components = text.split(":");
if (components.length < 2) return;
var username = components[0];
var message = String.join(":", Arrays.copyOfRange(components, 1, components.length));
userList.addUser(username);
Message receivedMessage = new Message(app, username, message);
receivedMessage.setOpacity(0);
FadeTransition fadeMessage = new FadeTransition(messageFadeInDuration, receivedMessage);
fadeMessage.setToValue(1);
messages.getChildren().add(receivedMessage);
fadeMessage.play();
if (messagesContainer.getVvalue() == 1.0f) scrollToBottom = true;
if (!lastMessageFromMe) Utility.playAudio("incoming.mp3");
else lastMessageFromMe = false;
}
/**
* Scroll to bottom of messages list.
*/
private void scrollToBottom() {
if (!scrollToBottom) return;
messagesContainer.setVvalue(1.0);
scrollToBottom = false;
}
/**
* Send an outgoing message from the Chat window.
*
* @param text The text of the message to send to the Communicator
*/
private void sendCurrentMessage(String text) {
if (!text.isBlank()) {
if (text.strip().equals("/game")) app.openGame();
else if (text.matches("/nick (.+)$")) app.setUsername(text.substring(6));
else {
communicator.send(app.getUsername() + ":" + text);
lastMessageFromMe = true;
}
messageToSend.clear();
}
}
/**
* Unhide this window
*/
public void show() {
super.show();
}
}

View File

@@ -0,0 +1,67 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.scene.control.Hyperlink;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import uk.mgrove.ac.soton.comp1206.App;
import uk.mgrove.ac.soton.comp1206.utility.Utility;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
/**
* Class for a message, which displays the message in a formatted way
*/
public class Message extends TextFlow {
private final App app;
private final String username;
private final String message;
private final LocalDateTime received;
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
/**
* Initialise the message with basic info
* @param app the app
* @param username username of user who sent the message
* @param message the message contents
*/
public Message(App app, String username, String message) {
this.app = app;
this.username = username;
this.message = message;
received = LocalDateTime.now();
build();
}
/**
* Build the message
*/
public void build() {
Text timeField = new Text("[" + formatter.format(received) + "] ");
timeField.getStyleClass().add("timestamp");
Text usernameField = new Text(username + ": ");
usernameField.getStyleClass().add("username");
getChildren().addAll(timeField,usernameField);
Matcher urlMatcher = Utility.getUrlPattern().matcher(message);
var previousMatchEnd = 0;
while (urlMatcher.find()) {
if (urlMatcher.start() > previousMatchEnd) {
getChildren().add(new Text(message.substring(previousMatchEnd, urlMatcher.start())));
}
var url = message.substring(urlMatcher.start(), urlMatcher.end());
var urlField = new Hyperlink(url);
urlField.setOnAction((event) -> app.getHostServices().showDocument(url));
getChildren().add(urlField);
previousMatchEnd = urlMatcher.end();
}
if (message.length() > previousMatchEnd) {
getChildren().add(new Text(message.substring(previousMatchEnd)));
}
getStyleClass().add("message-container");
}
}

View File

@@ -0,0 +1,15 @@
package uk.mgrove.ac.soton.comp1206.ui;
/**
* Interface for listeners that handle new messages being received by the Communicator class
* @see uk.mgrove.ac.soton.comp1206.network.Communicator
*/
public interface MessageListener {
/**
* Receive a message
* @param message the message to receive
*/
void receiveMessage(String message);
}

View File

@@ -0,0 +1,58 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.animation.FadeTransition;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
/**
* Class for a user, which is an element to be displayed in the chat window
*/
public class User extends VBox {
private final String username;
private final IntegerProperty messageCount;
/**
* Initialise the user with a username
* @param username username of user
*/
public User(String username) {
setSpacing(2);
setPadding(new Insets(0, 12,0,12));
setFillWidth(true);
this.username = username;
this.messageCount = new SimpleIntegerProperty(1);
var messageCountLabel = new Label("");
messageCountLabel.getStyleClass().add("italic");
messageCountLabel.setWrapText(true);
messageCountLabel.textProperty().bind(Bindings.concat(messageCount.asString()," message(s)"));
var usernameLabel = new Label(username);
usernameLabel.setWrapText(true);
getChildren().addAll(
usernameLabel,
messageCountLabel
);
}
/**
* Get property for number of messages sent by user
* @return property
*/
public IntegerProperty messageCountProperty() {
return messageCount;
}
/**
* Get user's username
* @return username
*/
public String getUsername() {
return username;
}
}

View File

@@ -0,0 +1,81 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.animation.FadeTransition;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
/**
* List of users to be displayed in chat window
*/
public class UserList extends ScrollPane {
private final Map<String,IntegerProperty> userMessageCounts = new HashMap<>();
private final VBox container;
private final ArrayList<User> userElements = new ArrayList<>();
private final StringProperty userSearchProperty;
/**
* Initialise the list of users
* @param userSearchProperty property for content of users search field
*/
public UserList(StringProperty userSearchProperty) {
setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
setHbarPolicy(ScrollBarPolicy.NEVER);
getStyleClass().add("user-list");
setFitToWidth(true);
container = new VBox();
container.setSpacing(12);
container.setFillWidth(true);
setContent(container);
this.userSearchProperty = userSearchProperty;
}
/**
* Add user to list of users
* @param username username of user to add
*/
public void addUser(String username) {
if (userMessageCounts.containsKey(username)) {
var property = userMessageCounts.get(username);
property.set(property.get() + 1);
} else {
var newUser = new User(username);
userMessageCounts.put(username, newUser.messageCountProperty());
newUser.setOpacity(0);
FadeTransition fadeMessage = new FadeTransition(Duration.millis(150), newUser);
fadeMessage.setToValue(1);
userElements.add(newUser);
fadeMessage.play();
sortUserElements();
updateDisplayedUsers();
}
}
private void sortUserElements() {
userElements.sort(Comparator.comparing(User::getUsername));
}
/**
* Update the list of users displayed
*/
public void updateDisplayedUsers() {
if (userSearchProperty.get().isBlank()) {
container.getChildren().setAll(userElements);
} else {
container.getChildren().setAll(userElements.stream().filter((user) -> user.getUsername().toLowerCase().contains(userSearchProperty.get().toLowerCase())).toList());
}
}
}

View File

@@ -0,0 +1,14 @@
package uk.mgrove.ac.soton.comp1206.ui;
/**
* Interface for listeners that handle a game block being clicked
*/
public interface BlockClickedListener {
/**
* Handle game block being clicked
* @param block block that was clicked
*/
void blockClicked(GameBlock block);
}

View File

@@ -0,0 +1,114 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import java.util.concurrent.ThreadLocalRandom;
/**
* Class for a game block, which is a single block with a colour that can be changed
*/
public class GameBlock extends Canvas {
private final int x;
private final int y;
private final Color[] colours;
private final double width;
private final double height;
private final IntegerProperty value;
private final boolean showStrokes;
private final boolean roundedCorners;
/**
* Initialise the game block with a set of available colours, a set size,
* a set location in the grid, and outlines around the block
* @param colours available colours
* @param x column index of location in grid
* @param y row index of location in grid
* @param width block width
* @param height block height
*/
public GameBlock(Color[] colours, int x, int y, double width, double height) {
this(colours,x,y,width,height,true,false);
}
/**
* Initialise the game block with a set of available colours, a set size,
* a set location in the grid, and optional outlines around the block
* @param colours available colours
* @param x column index of location in grid
* @param y row index of location in grid
* @param width block width
* @param height block height
* @param showStrokes whether block should have an outline
* @param roundedCorners whether block should have rounded corners
*/
public GameBlock(Color[] colours, int x, int y, double width, double height, boolean showStrokes, boolean roundedCorners) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.colours = colours;
this.showStrokes = showStrokes;
this.roundedCorners = roundedCorners;
value = new SimpleIntegerProperty();
setWidth(width);
setHeight(height);
value.addListener(((observableValue, oldValue, newValue) -> paint()));
paint();
}
/**
* Paint the block - i.e. draw it
*/
public void paint() {
GraphicsContext gc = getGraphicsContext2D();
gc.clearRect(0,0,width,height);
if (value.get() >= 0) {
// fill in colour block
gc.setFill(colours[value.get()]);
gc.setStroke(Color.BLACK);
if (roundedCorners) {
gc.fillRoundRect(0,0,width,height,16,16);
if (showStrokes) gc.strokeRoundRect(0,0,width,height,16,16);
} else {
gc.fillRect(0,0,width,height);
if (showStrokes) gc.strokeRect(0,0,width,height);
}
}
}
/**
* Get the x-coordinate of this block in the grid
* @return x-coordinate
*/
public int getX() {
return x;
}
/**
* Get the y-coordinate of this block in the grid
* @return y-coordinate
*/
public int getY() {
return y;
}
/**
* Get the value property for binding, which holds the current colour value
* @return value property
*/
public IntegerProperty valueProperty() {
return value;
}
}

View File

@@ -0,0 +1,63 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import java.util.ArrayList;
import java.util.List;
/**
* Game grid class, which displays a grid of game blocks for the game
*/
public class GameGrid extends GridPane {
private final List<BlockClickedListener> listeners = new ArrayList<>();
/**
* Initialise game grid with an existing grid, a set size, and a set of available colours
* @param colours available colours
* @param grid existing grid to use
* @param width game grid width
* @param height game grid height
*/
public GameGrid(Color[] colours, Grid grid, int width, int height) {
var cols = grid.getCols();
var rows = grid.getRows();
setMaxWidth(width);
setMaxHeight(height);
setGridLinesVisible(true);
// create each internal game block
for (var y=0; y < rows; y++) {
for (var x=0; x < cols; x++) {
var block = new GameBlock(colours, x, y, (double) width/cols, (double) height/rows);
add(block,x,y);
block.valueProperty().bind(grid.getGridProperty(x,y));
// handle when game block is clicked
block.setOnMouseClicked((e) -> blockClicked(block));
}
}
}
/**
* Add listener for blocks being clicked
* @param listener listener
*/
public void addListener(BlockClickedListener listener) {
this.listeners.add(listener);
}
/**
* Handle block being clicked by calling all appropriate listeners
* @param block block that was clicked
*/
public void blockClicked(GameBlock block) {
for (BlockClickedListener listener : listeners) {
listener.blockClicked(block);
}
}
}

View File

@@ -0,0 +1,238 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.SplitPane;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.App;
import uk.mgrove.ac.soton.comp1206.network.Communicator;
import uk.mgrove.ac.soton.comp1206.utility.Utility;
import java.util.*;
/**
* Game window which will display mini-game for users to play
*/
public class GameWindow extends BaseWindow {
private static final Logger logger = LogManager.getLogger(GameWindow.class);
private static final int maxColourChanges = 30;
private final Grid grid;
private final Random random = new Random();
private final Color[] colours;
private final IntegerProperty currentColour = new SimpleIntegerProperty();
private final IntegerProperty score = new SimpleIntegerProperty(0);
private Timer gameTimer;
private int colourChangesCount;
private final List<String> scores = new ArrayList<>();
@FXML
private BorderPane gameBoardContainer;
@FXML
private Text currentScoreField;
@FXML
private VBox currentColourContainer;
@FXML
private VBox highScoresContainer;
@FXML
private VBox gameCoverContainer;
@FXML
private Text gameCoverTitle;
@FXML
private ProgressBar progressBar;
/**
* Initialise the game window
*
* @param app the app
* @param communicator the communicator
*/
public GameWindow(App app, Communicator communicator) {
super(app, communicator, "/game.fxml");
colours = new Color[]{
Color.web("0x005C84"),
Color.web("0x74C9E5"),
Color.web("0x4BB694"),
Color.web("0xC1D100"),
Color.web("0xEF7D00"),
Color.web("0xE73037"),
Color.web("0xD5007F"),
Color.web("0x8D3970")
};
grid = new Grid(6, 6);
currentScoreField.textProperty().bind(score.asString());
var currentColourBlock = new GameBlock(colours, 0, 0, 100, 100, false, true);
currentColourContainer.getChildren().add(currentColourBlock);
currentColourBlock.valueProperty().bind(currentColour);
communicator.addListener((message) -> {
if (!message.startsWith("SCORES")) return;
Platform.runLater(() -> this.receiveScore(message));
});
communicator.send("SCORES");
}
/**
* Events that occur at regular intervals to maintain the game: update the current colour and handle game ending
*/
public void gameLoop() {
if (colourChangesCount < maxColourChanges) {
var randomColour = random.nextInt(colours.length);
logger.info("Game loop - Colour chosen: {}", randomColour);
currentColour.set(randomColour);
colourChangesCount++;
progressBar.setProgress((double) (maxColourChanges - colourChangesCount + 1) / maxColourChanges);
} else {
progressBar.setProgress(0);
endGame();
}
}
/**
* Handle game block being clicked - adjust score appropriately
*
* @param block block that was clicked
*/
public void blockClicked(GameBlock block) {
var gridBlock = grid.getGridProperty(block.getX(), block.getY());
if (currentColour.get() == gridBlock.get()) {
logger.info("{} is {}, scoring 5 points", currentColour.get(), gridBlock.getValue());
score.set(score.get() + 5);
gridBlock.set(random.nextInt(colours.length));
} else {
logger.info("{} is not {}, losing 1 point", currentColour.get(), gridBlock.getValue());
if (score.get() > 0) {
score.set(score.get() - 1);
}
}
}
/**
* Receive and store scores from server
* @param message message containing scores
*/
private void receiveScore(String message) {
scores.clear();
for (var line : message.split("\n")) {
if (!line.equals("SCORES")) {
int splitIndex = line.lastIndexOf("=");
try {
String[] info = {line.substring(0,splitIndex), line.substring(splitIndex+1)};
scores.add(info[0] + ": " + info[1]);
} catch (Exception ignored) {
logger.error("Couldn't load score from message {}",message);
}
}
}
highScoresContainer.getChildren().clear();
for (var i=0; i < Math.min(scores.size(),5); i++) {
highScoresContainer.getChildren().add(new Text(scores.get(i)));
}
}
/**
* Show game over screen
*/
private void showGameOver() {
gameCoverTitle.setText("Game Over!");
Platform.runLater(() -> gameBoardContainer.setCenter(gameCoverContainer));
// TODO: include a timer bar
}
/**
* Start a new game
*/
private void startGame() {
colourChangesCount = 0;
score.set(0);
// set up grid with random values
for (int y = 0; y < grid.getRows(); y++) {
for (int x = 0; x < grid.getCols(); x++) {
grid.set(x, y, random.nextInt(colours.length));
}
}
currentColour.set(random.nextInt(colours.length));
var gameGrid = new GameGrid(colours, grid, 400, 350);
gameGrid.addListener(this::blockClicked);
gameBoardContainer.setCenter(gameGrid);
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
gameLoop();
}
};
gameTimer = new Timer("Timer");
gameTimer.schedule(timerTask, 0, 2000);
}
/**
* Start game when button clicked
* @param event button clicked
*/
public void startGame(ActionEvent event) {
startGame();
}
/**
* Return to the chat window when button clicked
* @param event button clicked
*/
public void returnToChat(ActionEvent event) {
stopGameTimer();
app.setGameOpen(false);
app.returnToChat(this);
}
/**
* Stop the game
*/
private void endGame() {
// game finished
stopGameTimer();
currentColour.set(-1);
communicator.send(String.format("SCORE %s %d",app.getUsername(),score.get()));
communicator.send("SCORES");
showGameOver();
}
/**
* Stop the game timer
*/
public void stopGameTimer() {
if (gameTimer != null) gameTimer.cancel();
}
}

View File

@@ -0,0 +1,79 @@
package uk.mgrove.ac.soton.comp1206.ui;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
/**
* Grid class, used to store a grid of game blocks for the game
*/
public class Grid {
private final int cols;
private final int rows;
final SimpleIntegerProperty[][] grid;
/**
* Initialise the grid
* @param cols number of columns in the grid
* @param rows number of rows in the grid
*/
public Grid(int cols, int rows) {
this.cols = cols;
this.rows = rows;
grid = new SimpleIntegerProperty[cols][rows];
for (var y=0; y < rows; y++) {
for (var x=0; x < cols; x++) {
grid[x][y] = new SimpleIntegerProperty(0);
}
}
}
/**
* Get the property relating to a specific element in the grid
* @param x column index
* @param y row index
* @return the property relating to the element at the given location
*/
public IntegerProperty getGridProperty(int x, int y) {
return grid[x][y];
}
/**
* Set the value of a specific element in the grid
* @param x column index
* @param y row index
* @param value the value to set
*/
public void set(int x, int y, int value) {
grid[x][y].set(value);
}
/**
* Get the value of a specific element in the grid
* @param x column index
* @param y row index
* @return the value of the element
*/
public int get(int x, int y) {
return grid[x][y].get();
}
/**
* Get number of columns in the grid
* @return number of columns
*/
public int getCols() {
return cols;
}
/**
* Get number of rows in the grid
* @return number of rows
*/
public int getRows() {
return rows;
}
}