Updated for 2023

This commit is contained in:
Oli
2023-03-10 10:58:11 +00:00
commit 0cccefdb9c
52 changed files with 2036 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
package uk.ac.soton.comp1206;
import javafx.application.Application;
import javafx.stage.Stage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.ac.soton.comp1206.ui.GameWindow;
/**
* JavaFX Application class
*/
public class App extends Application {
/**
* Base resolution width
*/
private final int width = 800;
/**
* Base resolution height
*/
private final int height = 600;
private static App instance;
private static final Logger logger = LogManager.getLogger(App.class);
private Stage stage;
/**
* Start the game
* @param args commandline arguments
*/
public static void main(String[] args) {
logger.info("Starting client");
launch();
}
/**
* Called by JavaFX with the primary stage as a parameter. Begins the game by opening the Game Window
* @param stage the default stage, main window
*/
@Override
public void start(Stage stage) {
instance = this;
this.stage = stage;
//Open game window
openGame();
}
/**
* Create the GameWindow with the specified width and height
*/
public void openGame() {
logger.info("Opening game window");
//Change the width and height in this class to change the base rendering resolution for all game parts
var gameWindow = new GameWindow(stage,width,height);
//Display the GameWindow
stage.show();
}
/**
* Shutdown the game
*/
public void shutdown() {
logger.info("Shutting down");
System.exit(0);
}
/**
* Get the singleton App instance
* @return the app
*/
public static App getInstance() {
return instance;
}
}

View File

@@ -0,0 +1,17 @@
package uk.ac.soton.comp1206;
/**
* This Launcher class is used to allow the game to be built into a shaded jar file which then loads JavaFX. This
* Launcher is used when running as a shaded jar file.
*/
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);
}
}

View File

@@ -0,0 +1,184 @@
package uk.ac.soton.comp1206.component;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.canvas.Canvas;
import javafx.scene.paint.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* The Visual User Interface component representing a single block in the grid.
*
* Extends Canvas and is responsible for drawing itself.
*
* Displays an empty square (when the value is 0) or a coloured square depending on value.
*
* The GameBlock value should be bound to a corresponding block in the Grid model.
*/
public class GameBlock extends Canvas {
private static final Logger logger = LogManager.getLogger(GameBlock.class);
/**
* The set of colours for different pieces
*/
public static final Color[] COLOURS = {
Color.TRANSPARENT,
Color.DEEPPINK,
Color.RED,
Color.ORANGE,
Color.YELLOW,
Color.YELLOWGREEN,
Color.LIME,
Color.GREEN,
Color.DARKGREEN,
Color.DARKTURQUOISE,
Color.DEEPSKYBLUE,
Color.AQUA,
Color.AQUAMARINE,
Color.BLUE,
Color.MEDIUMPURPLE,
Color.PURPLE
};
private final GameBoard gameBoard;
private final double width;
private final double height;
/**
* The column this block exists as in the grid
*/
private final int x;
/**
* The row this block exists as in the grid
*/
private final int y;
/**
* The value of this block (0 = empty, otherwise specifies the colour to render as)
*/
private final IntegerProperty value = new SimpleIntegerProperty(0);
/**
* Create a new single Game Block
* @param gameBoard the board this block belongs to
* @param x the column the block exists in
* @param y the row the block exists in
* @param width the width of the canvas to render
* @param height the height of the canvas to render
*/
public GameBlock(GameBoard gameBoard, int x, int y, double width, double height) {
this.gameBoard = gameBoard;
this.width = width;
this.height = height;
this.x = x;
this.y = y;
//A canvas needs a fixed width and height
setWidth(width);
setHeight(height);
//Do an initial paint
paint();
//When the value property is updated, call the internal updateValue method
value.addListener(this::updateValue);
}
/**
* When the value of this block is updated,
* @param observable what was updated
* @param oldValue the old value
* @param newValue the new value
*/
private void updateValue(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
paint();
}
/**
* Handle painting of the block canvas
*/
public void paint() {
//If the block is empty, paint as empty
if(value.get() == 0) {
paintEmpty();
} else {
//If the block is not empty, paint with the colour represented by the value
paintColor(COLOURS[value.get()]);
}
}
/**
* Paint this canvas empty
*/
private void paintEmpty() {
var gc = getGraphicsContext2D();
//Clear
gc.clearRect(0,0,width,height);
//Fill
gc.setFill(Color.WHITE);
gc.fillRect(0,0, width, height);
//Border
gc.setStroke(Color.BLACK);
gc.strokeRect(0,0,width,height);
}
/**
* Paint this canvas with the given colour
* @param colour the colour to paint
*/
private void paintColor(Paint colour) {
var gc = getGraphicsContext2D();
//Clear
gc.clearRect(0,0,width,height);
//Colour fill
gc.setFill(colour);
gc.fillRect(0,0, width, height);
//Border
gc.setStroke(Color.BLACK);
gc.strokeRect(0,0,width,height);
}
/**
* Get the column of this block
* @return column number
*/
public int getX() {
return x;
}
/**
* Get the row of this block
* @return row number
*/
public int getY() {
return y;
}
/**
* Get the current value held by this block, representing it's colour
* @return value
*/
public int getValue() {
return this.value.get();
}
/**
* Bind the value of this block to another property. Used to link the visual block to a corresponding block in the Grid.
* @param input property to bind the value to
*/
public void bind(ObservableValue<? extends Number> input) {
value.bind(input);
}
}

View File

@@ -0,0 +1,130 @@
package uk.ac.soton.comp1206.component;
import javafx.beans.NamedArg;
/**
* Represents a row and column representation of a block in the grid. Holds the x (column) and y (row).
*
* Useful for use in a set or list or other form of collection.
*/
public class GameBlockCoordinate {
/**
* Represents the column
*/
private final int x;
/**
* Represents the row
*/
private final int y;
/**
* A hash is computed to enable comparisons between this and other GameBlockCoordinates.
*/
private int hash = 0;
/**
* Create a new GameBlockCoordinate which stores a row and column reference to a block
* @param x column
* @param y row
*/
public GameBlockCoordinate(@NamedArg("x") int x, @NamedArg("y") int y) {
this.x = x;
this.y = y;
}
/**
* Return the column (x)
* @return column number
*/
public int getX() {
return x;
}
/**
* Return the row (y)
* @return the row number
*/
public int getY() {
return y;
}
/**
* Add a row and column reference to this one and return a new GameBlockCoordinate
* @param x additional columns
* @param y additional rows
* @return a new GameBlockCoordinate with the result of the addition
*/
public GameBlockCoordinate add(int x, int y) {
return new GameBlockCoordinate(
getX() + x,
getY() + y);
}
/**
* Add another GameBlockCoordinate to this one, returning a new GameBlockCoordinate
* @param point point to add
* @return a new GameBlockCoordinate with the result of the addition
*/
public GameBlockCoordinate add(GameBlockCoordinate point) {
return add(point.getX(), point.getY());
}
/** Subtract a row and column reference to this one and return a new GameBlockCoordinate
* @param x columns to remove
* @param y rows to remove
* @return a new GameBlockCoordinate with the result of the subtraction
*/
public GameBlockCoordinate subtract(int x, int y) {
return new GameBlockCoordinate(
getX() - x,
getY() - y);
}
/**
* Subtract another GameBlockCoordinate to this one, returning a new GameBlockCoordinate
* @param point point to subtract
* @return a new GameBlockCoordinate with the result of the subtraction
*/
public GameBlockCoordinate subtract(GameBlockCoordinate point) {
return subtract(point.getX(), point.getY());
}
/**
* Compare this GameBlockCoordinate to another GameBlockCoordinate
* @param obj other object to compare to
* @return true if equal, otherwise false
*/
@Override public boolean equals(Object obj) {
if (obj == this) return true;
if (obj instanceof GameBlockCoordinate) {
GameBlockCoordinate other = (GameBlockCoordinate) obj;
return getX() == other.getX() && getY() == other.getY();
} else return false;
}
/**
* Calculate a hash code of this GameBlockCoordinate, used for comparisons
* @return hash code
*/
@Override public int hashCode() {
if (hash == 0) {
long bits = 7L;
bits = 31L * bits + Double.doubleToLongBits(getX());
bits = 31L * bits + Double.doubleToLongBits(getY());
hash = (int) (bits ^ (bits >> 32));
}
return hash;
}
/**
* Return a string representation of this GameBlockCoordinate
* @return string representation
*/
@Override public String toString() {
return "GameBlockCoordinate [x = " + getX() + ", y = " + getY() + "]";
}
}

View File

@@ -0,0 +1,175 @@
package uk.ac.soton.comp1206.component;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.ac.soton.comp1206.event.BlockClickedListener;
import uk.ac.soton.comp1206.game.Grid;
/**
* A GameBoard is a visual component to represent the visual GameBoard.
* It extends a GridPane to hold a grid of GameBlocks.
*
* The GameBoard can hold an internal grid of it's own, for example, for displaying an upcoming block. It also be
* linked to an external grid, for the main game board.
*
* The GameBoard is only a visual representation and should not contain game logic or model logic in it, which should
* take place in the Grid.
*/
public class GameBoard extends GridPane {
private static final Logger logger = LogManager.getLogger(GameBoard.class);
/**
* Number of columns in the board
*/
private final int cols;
/**
* Number of rows in the board
*/
private final int rows;
/**
* The visual width of the board - has to be specified due to being a Canvas
*/
private final double width;
/**
* The visual height of the board - has to be specified due to being a Canvas
*/
private final double height;
/**
* The grid this GameBoard represents
*/
final Grid grid;
/**
* The blocks inside the grid
*/
GameBlock[][] blocks;
/**
* The listener to call when a specific block is clicked
*/
private BlockClickedListener blockClickedListener;
/**
* Create a new GameBoard, based off a given grid, with a visual width and height.
* @param grid linked grid
* @param width the visual width
* @param height the visual height
*/
public GameBoard(Grid grid, double width, double height) {
this.cols = grid.getCols();
this.rows = grid.getRows();
this.width = width;
this.height = height;
this.grid = grid;
//Build the GameBoard
build();
}
/**
* Create a new GameBoard with it's own internal grid, specifying the number of columns and rows, along with the
* visual width and height.
*
* @param cols number of columns for internal grid
* @param rows number of rows for internal grid
* @param width the visual width
* @param height the visual height
*/
public GameBoard(int cols, int rows, double width, double height) {
this.cols = cols;
this.rows = rows;
this.width = width;
this.height = height;
this.grid = new Grid(cols,rows);
//Build the GameBoard
build();
}
/**
* Get a specific block from the GameBoard, specified by it's row and column
* @param x column
* @param y row
* @return game block at the given column and row
*/
public GameBlock getBlock(int x, int y) {
return blocks[x][y];
}
/**
* Build the GameBoard by creating a block at every x and y column and row
*/
protected void build() {
logger.info("Building grid: {} x {}",cols,rows);
setMaxWidth(width);
setMaxHeight(height);
setGridLinesVisible(true);
blocks = new GameBlock[cols][rows];
for(var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
createBlock(x,y);
}
}
}
/**
* Create a block at the given x and y position in the GameBoard
* @param x column
* @param y row
*/
protected GameBlock createBlock(int x, int y) {
var blockWidth = width / cols;
var blockHeight = height / rows;
//Create a new GameBlock UI component
GameBlock block = new GameBlock(this, x, y, blockWidth, blockHeight);
//Add to the GridPane
add(block,x,y);
//Add to our block directory
blocks[x][y] = block;
//Link the GameBlock component to the corresponding value in the Grid
block.bind(grid.getGridProperty(x,y));
//Add a mouse click handler to the block to trigger GameBoard blockClicked method
block.setOnMouseClicked((e) -> blockClicked(e, block));
return block;
}
/**
* Set the listener to handle an event when a block is clicked
* @param listener listener to add
*/
public void setOnBlockClick(BlockClickedListener listener) {
this.blockClickedListener = listener;
}
/**
* Triggered when a block is clicked. Call the attached listener.
* @param event mouse event
* @param block block clicked on
*/
private void blockClicked(MouseEvent event, GameBlock block) {
logger.info("Block clicked: {}", block);
if(blockClickedListener != null) {
blockClickedListener.blockClicked(block);
}
}
}

View File

@@ -0,0 +1,16 @@
package uk.ac.soton.comp1206.event;
import uk.ac.soton.comp1206.component.GameBlock;
/**
* The Block Clicked listener is used to handle the event when a block in a GameBoard is clicked. It passes the
* GameBlock that was clicked in the message
*/
public interface BlockClickedListener {
/**
* Handle a block clicked event
* @param block the block that was clicked
*/
public void blockClicked(GameBlock block);
}

View File

@@ -0,0 +1,13 @@
package uk.ac.soton.comp1206.event;
/**
* The Communications Listener is used for listening to messages received by the communicator.
*/
public interface CommunicationsListener {
/**
* Handle an incoming message received by the Communicator
* @param communication the message that was received
*/
public void receiveCommunication(String communication);
}

View File

@@ -0,0 +1,103 @@
package uk.ac.soton.comp1206.game;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.ac.soton.comp1206.component.GameBlock;
/**
* 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 {
private static final Logger logger = LogManager.getLogger(Game.class);
/**
* Number of rows
*/
protected final int rows;
/**
* Number of columns
*/
protected final int cols;
/**
* The grid model linked to the game
*/
protected final Grid grid;
/**
* Create a new game with the specified rows and columns. Creates a corresponding grid model.
* @param cols number of columns
* @param rows number of rows
*/
public Game(int cols, int rows) {
this.cols = cols;
this.rows = rows;
//Create a new grid model to represent the game state
this.grid = new Grid(cols,rows);
}
/**
* Start the game
*/
public void start() {
logger.info("Starting game");
initialiseGame();
}
/**
* Initialise a new game and set up anything that needs to be done at the start
*/
public void initialiseGame() {
logger.info("Initialising game");
}
/**
* Handle what should happen when a particular block is clicked
* @param gameBlock the block that was clicked
*/
public void blockClicked(GameBlock gameBlock) {
//Get the position of this block
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;
}
//Update the grid with the new value
grid.set(x,y,newValue);
}
/**
* Get the grid model inside this game representing the game state of the board
* @return game grid model
*/
public Grid getGrid() {
return grid;
}
/**
* Get the number of columns in this game
* @return number of columns
*/
public int getCols() {
return cols;
}
/**
* Get the number of rows in this game
* @return number of rows
*/
public int getRows() {
return rows;
}
}

View File

@@ -0,0 +1,224 @@
package uk.ac.soton.comp1206.game;
/**
* Instances of GamePiece Represents the model of a specific Game Piece with it's block makeup.
*
* The GamePiece class also contains a factory for producing a GamePiece of a particular shape, as specified by it's
* number.
*/
public class GamePiece {
/**
* The total number of pieces in this game
*/
public static final int PIECES = 15;
/**
* The 2D grid representation of the shape of this piece
*/
private int[][] blocks;
/**
* The value of this piece
*/
private final int value;
/**
* The name of this piece
*/
private final String name;
/**
* Create a new GamePiece of the specified piece number
* @param piece piece number
* @return the created GamePiece
*/
public static GamePiece createPiece(int piece) {
switch (piece) {
//Line
case 0 -> {
int[][] blocks = {{0, 0, 0}, {1, 1, 1}, {0, 0, 0}};
return new GamePiece("Line", blocks, 1);
}
//C
case 1 -> {
int[][] blocks = {{0, 0, 0}, {1, 1, 1}, {1, 0, 1}};
return new GamePiece("C", blocks, 2);
}
//Plus
case 2 -> {
int[][] blocks = {{0, 1, 0}, {1, 1, 1}, {0, 1, 0}};
return new GamePiece("Plus", blocks, 3);
}
//Dot
case 3 -> {
int[][] blocks = {{0, 0, 0}, {0, 1, 0}, {0, 0, 0}};
return new GamePiece("Dot", blocks, 4);
}
//Square
case 4 -> {
int[][] blocks = {{1, 1, 0}, {1, 1, 0}, {0, 0, 0}};
return new GamePiece("Square", blocks, 5);
}
//L
case 5 -> {
int[][] blocks = {{0, 0, 0}, {1, 1, 1}, {0, 0, 1}};
return new GamePiece("L", blocks, 6);
}
//J
case 6 -> {
int[][] blocks = {{0, 0, 1}, {1, 1, 1}, {0, 0, 0}};
return new GamePiece("J", blocks, 7);
}
//S
case 7 -> {
int[][] blocks = {{0, 0, 0}, {0, 1, 1}, {1, 1, 0}};
return new GamePiece("S", blocks, 8);
}
//Z
case 8 -> {
int[][] blocks = {{1, 1, 0}, {0, 1, 1}, {0, 0, 0}};
return new GamePiece("Z", blocks, 9);
}
//T
case 9 -> {
int[][] blocks = {{1, 0, 0}, {1, 1, 0}, {1, 0, 0}};
return new GamePiece("T", blocks, 10);
}
//X
case 10 -> {
int[][] blocks = {{1, 0, 1}, {0, 1, 0}, {1, 0, 1}};
return new GamePiece("X", blocks, 11);
}
//Corner
case 11 -> {
int[][] blocks = {{0, 0, 0}, {1, 1, 0}, {1, 0, 0}};
return new GamePiece("Corner", blocks, 12);
}
//Inverse Corner
case 12 -> {
int[][] blocks = {{1, 0, 0}, {1, 1, 0}, {0, 0, 0}};
return new GamePiece("Inverse Corner", blocks, 13);
}
//Diagonal
case 13 -> {
int[][] blocks = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
return new GamePiece("Diagonal", blocks, 14);
}
//Double
case 14 -> {
int[][] blocks = {{0, 1, 0}, {0, 1, 0}, {0, 0, 0}};
return new GamePiece("Double", blocks, 15);
}
}
//Not a valid piece number
throw new IndexOutOfBoundsException("No such piece: " + piece);
}
/**
* Create a new GamePiece of the specified piece number and rotation
* @param piece piece number
* @param rotation number of times to rotate
* @return the created GamePiece
*/
public static GamePiece createPiece(int piece, int rotation) {
var newPiece = createPiece(piece);
newPiece.rotate(rotation);
return newPiece;
}
/**
* Create a new GamePiece with the given name, block makeup and value. Should not be called directly, only via the
* factory.
* @param name name of the piece
* @param blocks block makeup of the piece
* @param value the value of this piece
*/
private GamePiece(String name, int[][] blocks, int value) {
this.name = name;
this.blocks = blocks;
this.value = value;
//Use the shape of the block to create a grid with either 0 (empty) or the value of this shape for each block.
for(int x = 0; x < blocks.length; x++) {
for (int y = 0; y < blocks[x].length; y++) {
if(blocks[x][y] == 0) continue;
blocks[x][y] = value;
}
}
}
/**
* Get the value of this piece
* @return piece value
*/
public int getValue() {
return value;
}
/**
* Get the block makeup of this piece
* @return 2D grid of the blocks representing the piece shape
*/
public int[][] getBlocks() {
return blocks;
}
/**
* Rotate this piece the given number of rotations
* @param rotations number of rotations
*/
public void rotate(int rotations) {
for(int rotated = 0; rotated < rotations; rotated ++) {
rotate();
}
}
/**
* Rotate this piece exactly once by rotating it's 3x3 grid
*/
public void rotate() {
int[][] rotated = new int[blocks.length][blocks[0].length];
rotated[2][0] = blocks[0][0];
rotated[1][0] = blocks[0][1];
rotated[0][0] = blocks[0][2];
rotated[2][1] = blocks[1][0];
rotated[1][1] = blocks[1][1];
rotated[0][1] = blocks[1][2];
rotated[2][2] = blocks[2][0];
rotated[1][2] = blocks[2][1];
rotated[0][2] = blocks[2][2];
blocks = rotated;
}
/**
* Return the string representation of this piece
* @return the name of this piece
*/
public String toString() {
return this.name;
}
}

View File

@@ -0,0 +1,106 @@
package uk.ac.soton.comp1206.game;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
/**
* 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
* arrow, with rows and columns.
*
* Each value inside the Grid is an IntegerProperty can be bound to enable modification and display of the contents of
* the grid.
*
* The Grid contains functions related to modifying the model, for example, placing a piece inside the grid.
*
* The Grid should be linked to a GameBoard for it's display.
*/
public class Grid {
/**
* The number of columns in this grid
*/
private final int cols;
/**
* The number of rows in this grid
*/
private final int rows;
/**
* The grid is a 2D arrow with rows and columns of SimpleIntegerProperties.
*/
private final SimpleIntegerProperty[][] grid;
/**
* Create a new Grid with the specified number of columns and rows and initialise them
* @param cols number of columns
* @param rows number of rows
*/
public Grid(int cols, int rows) {
this.cols = cols;
this.rows = rows;
//Create the grid itself
grid = new SimpleIntegerProperty[cols][rows];
//Add a SimpleIntegerProperty to every block in the grid
for(var y = 0; y < rows; y++) {
for(var x = 0; x < cols; x++) {
grid[x][y] = new SimpleIntegerProperty(0);
}
}
}
/**
* Get the Integer property contained inside the grid at a given row and column index. Can be used for binding.
* @param x column
* @param y row
* @return the IntegerProperty at the given x and y in this grid
*/
public IntegerProperty getGridProperty(int x, int y) {
return grid[x][y];
}
/**
* Update the value at the given x and y index within the grid
* @param x column
* @param y row
* @param value the new value
*/
public void set(int x, int y, int value) {
grid[x][y].set(value);
}
/**
* Get the value represented at the given x and y index within the grid
* @param x column
* @param y row
* @return the value
*/
public int get(int x, int y) {
try {
//Get the value held in the property at the x and y index provided
return grid[x][y].get();
} catch (ArrayIndexOutOfBoundsException e) {
//No such index
return -1;
}
}
/**
* Get the number of columns in this game
* @return number of columns
*/
public int getCols() {
return cols;
}
/**
* Get the number of rows in this game
* @return number of rows
*/
public int getRows() {
return rows;
}
}

View File

@@ -0,0 +1,123 @@
package uk.ac.soton.comp1206.network;
import com.neovisionaries.ws.client.*;
import javafx.scene.control.Alert;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.ac.soton.comp1206.event.CommunicationsListener;
import java.util.ArrayList;
import java.util.List;
/**
* Uses web sockets to talk to a web socket server and relays communication to attached listeners
*
* YOU DO NOT NEED TO WORRY ABOUT THIS CLASS! Leave it be :-)
*/
public class Communicator {
private static final Logger logger = LogManager.getLogger(Communicator.class);
/**
* Attached communication listeners listening to messages on this Communicator. Each will be sent any messages.
*/
private final List<CommunicationsListener> handlers = new ArrayList<>();
private WebSocket ws = null;
/**
* Create a new communicator to the given web socket server
*
* @param server server to connect to
*/
public Communicator(String server) {
try {
var socketFactory = new WebSocketFactory();
//Connect to the server
ws = socketFactory.createSocket(server);
ws.connect();
logger.info("Connected to " + server);
//When a message is received, call the receive method
ws.addListener(new WebSocketAdapter() {
@Override
public void onTextMessage(WebSocket websocket, String message) throws Exception {
Communicator.this.receive(websocket, message);
}
@Override
public void onPingFrame(WebSocket webSocket, WebSocketFrame webSocketFrame) throws Exception {
logger.info("Ping? Pong!");
}
});
//Error handling
ws.addListener(new WebSocketAdapter() {
@Override
public void onTextMessage(WebSocket websocket, String message) throws Exception {
if(message.startsWith("ERROR")) {
logger.error(message);
}
}
@Override
public void handleCallbackError(WebSocket webSocket, Throwable throwable) throws Exception {
logger.error("Callback Error:" + throwable.getMessage());
throwable.printStackTrace();
}
@Override
public void onError(WebSocket webSocket, WebSocketException e) throws Exception {
logger.error("Error:" + e.getMessage());
e.printStackTrace();
}
});
} catch (Exception e){
logger.error("Socket error: " + e.getMessage());
e.printStackTrace();
Alert error = new Alert(Alert.AlertType.ERROR,"Unable to communicate with the TetrECS server\n\n" + e.getMessage() + "\n\nPlease ensure you are connected to the VPN");
error.showAndWait();
System.exit(1);
}
}
/** Send a message to the server
*
* @param message Message to send
*/
public void send(String message) {
logger.info("Sending message: " + message);
ws.sendText(message);
}
/**
* Add a new listener to receive messages from the server
* @param listener the listener to add
*/
public void addListener(CommunicationsListener listener) {
this.handlers.add(listener);
}
/**
* Clear all current listeners
*/
public void clearListeners() {
this.handlers.clear();
}
/** Receive a message from the server. Relay to any attached listeners
*
* @param websocket the socket
* @param message the message that was received
*/
private void receive(WebSocket websocket, String message) {
logger.info("Received: " + message);
for(CommunicationsListener handler : handlers) {
handler.receiveCommunication(message);
}
}
}

View File

@@ -0,0 +1,56 @@
package uk.ac.soton.comp1206.scene;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import uk.ac.soton.comp1206.ui.GamePane;
import uk.ac.soton.comp1206.ui.GameWindow;
/**
* A Base Scene used in the game. Handles common functionality between all scenes.
*/
public abstract class BaseScene {
protected final GameWindow gameWindow;
protected GamePane root;
protected Scene scene;
/**
* Create a new scene, passing in the GameWindow the scene will be displayed in
* @param gameWindow the game window
*/
public BaseScene(GameWindow gameWindow) {
this.gameWindow = gameWindow;
}
/**
* Initialise this scene. Called after creation
*/
public abstract void initialise();
/**
* Build the layout of the scene
*/
public abstract void build();
/**
* Create a new JavaFX scene using the root contained within this scene
* @return JavaFX scene
*/
public Scene setScene() {
var previous = gameWindow.getScene();
Scene scene = new Scene(root, previous.getWidth(), previous.getHeight(), Color.BLACK);
scene.getStylesheets().add(getClass().getResource("/style/game.css").toExternalForm());
this.scene = scene;
return scene;
}
/**
* Get the JavaFX scene contained inside
* @return JavaFX scene
*/
public Scene getScene() {
return this.scene;
}
}

View File

@@ -0,0 +1,83 @@
package uk.ac.soton.comp1206.scene;
import javafx.scene.layout.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.ac.soton.comp1206.component.GameBlock;
import uk.ac.soton.comp1206.component.GameBoard;
import uk.ac.soton.comp1206.game.Game;
import uk.ac.soton.comp1206.ui.GamePane;
import uk.ac.soton.comp1206.ui.GameWindow;
/**
* The Single Player challenge scene. Holds the UI for the single player challenge mode in the game.
*/
public class ChallengeScene extends BaseScene {
private static final Logger logger = LogManager.getLogger(MenuScene.class);
protected Game game;
/**
* Create a new Single Player challenge scene
* @param gameWindow the Game Window
*/
public ChallengeScene(GameWindow gameWindow) {
super(gameWindow);
logger.info("Creating Challenge Scene");
}
/**
* Build the Challenge window
*/
@Override
public void build() {
logger.info("Building " + this.getClass().getName());
setupGame();
root = new GamePane(gameWindow.getWidth(),gameWindow.getHeight());
var challengePane = new StackPane();
challengePane.setMaxWidth(gameWindow.getWidth());
challengePane.setMaxHeight(gameWindow.getHeight());
challengePane.getStyleClass().add("menu-background");
root.getChildren().add(challengePane);
var mainPane = new BorderPane();
challengePane.getChildren().add(mainPane);
var board = new GameBoard(game.getGrid(),gameWindow.getWidth()/2,gameWindow.getWidth()/2);
mainPane.setCenter(board);
//Handle block on gameboard grid being clicked
board.setOnBlockClick(this::blockClicked);
}
/**
* Handle when a block is clicked
* @param gameBlock the Game Block that was clocked
*/
private void blockClicked(GameBlock gameBlock) {
game.blockClicked(gameBlock);
}
/**
* Setup the game object and model
*/
public void setupGame() {
logger.info("Starting a new challenge");
//Start new game
game = new Game(5, 5);
}
/**
* Initialise the scene and start the game
*/
@Override
public void initialise() {
logger.info("Initialising Challenge");
game.start();
}
}

View File

@@ -0,0 +1,75 @@
package uk.ac.soton.comp1206.scene;
import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.layout.*;
import javafx.scene.text.Text;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.ac.soton.comp1206.ui.GamePane;
import uk.ac.soton.comp1206.ui.GameWindow;
/**
* The main menu of the game. Provides a gateway to the rest of the game.
*/
public class MenuScene extends BaseScene {
private static final Logger logger = LogManager.getLogger(MenuScene.class);
/**
* Create a new menu scene
* @param gameWindow the Game Window this will be displayed in
*/
public MenuScene(GameWindow gameWindow) {
super(gameWindow);
logger.info("Creating Menu Scene");
}
/**
* Build the menu layout
*/
@Override
public void build() {
logger.info("Building " + this.getClass().getName());
root = new GamePane(gameWindow.getWidth(),gameWindow.getHeight());
var menuPane = new StackPane();
menuPane.setMaxWidth(gameWindow.getWidth());
menuPane.setMaxHeight(gameWindow.getHeight());
menuPane.getStyleClass().add("menu-background");
root.getChildren().add(menuPane);
var mainPane = new BorderPane();
menuPane.getChildren().add(mainPane);
//Awful title
var title = new Text("TetrECS");
title.getStyleClass().add("title");
mainPane.setTop(title);
//For now, let us just add a button that starts the game. I'm sure you'll do something way better.
var button = new Button("Play");
mainPane.setCenter(button);
//Bind the button action to the startGame method in the menu
button.setOnAction(this::startGame);
}
/**
* Initialise the menu
*/
@Override
public void initialise() {
}
/**
* Handle when the Start Game button is pressed
* @param event event
*/
private void startGame(ActionEvent event) {
gameWindow.startChallenge();
}
}

View File

@@ -0,0 +1,94 @@
package uk.ac.soton.comp1206.ui;
import javafx.geometry.Pos;
import javafx.scene.layout.*;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* The Game Pane is a special pane which will scale anything inside it to the screen and maintain the aspect ratio.
*
* Drawing will be scaled appropriately.
*
* This takes the worry about the layout out and will allow the game to scale to any resolution easily.
*
* It uses the width and height given which should match the main window size. This will be the base drawing resolution,
* but will be scaled up or down as the window is resized.
*
* You should not need to modify this class
*/
public class GamePane extends StackPane {
private static final Logger logger = LogManager.getLogger(GamePane.class);
private final int width;
private final int height;
private double scalar = 1;
private final boolean autoScale = true;
/**
* Create a new scalable GamePane with the given drawing width and height.
* @param width width
* @param height height
*/
public GamePane(int width, int height) {
super();
this.width = width;
this.height = height;
getStyleClass().add("gamepane");
setAlignment(Pos.TOP_LEFT);
}
/**
* Update the scalar being used by this draw pane
* @param scalar scalar
*/
protected void setScalar(double scalar) {
this.scalar = scalar;
}
/**
* Use a Graphics Transformation to scale everything inside this pane. Padding is added to the edges to maintain
* the correct aspect ratio and keep the display centred.
*/
@Override
public void layoutChildren() {
super.layoutChildren();
if(!autoScale) {
return;
}
//Work out the scale factor height and width
var scaleFactorHeight = getHeight() / height;
var scaleFactorWidth = getWidth() / width;
//Work out whether to scale by width or height
if (scaleFactorHeight > scaleFactorWidth) {
setScalar(scaleFactorWidth);
} else {
setScalar(scaleFactorHeight);
}
//Set up the scale
Scale scale = new Scale(scalar,scalar);
//Get the parent width and height
var parentWidth = getWidth();
var parentHeight = getHeight();
//Get the padding needed on the top and left
var paddingLeft = (parentWidth - (width * scalar)) / 2.0;
var paddingTop = (parentHeight - (height * scalar)) / 2.0;
//Perform the transformation
Translate translate = new Translate(paddingLeft, paddingTop);
scale.setPivotX(0);
scale.setPivotY(0);
getTransforms().setAll(translate, scale);
}
}

View File

@@ -0,0 +1,163 @@
package uk.ac.soton.comp1206.ui;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.ac.soton.comp1206.App;
import uk.ac.soton.comp1206.network.Communicator;
import uk.ac.soton.comp1206.scene.*;
/**
* The GameWindow is the single window for the game where everything takes place. To move between screens in the game,
* we simply change the scene.
*
* The GameWindow has methods to launch each of the different parts of the game by switching scenes. You can add more
* methods here to add more screens to the game.
*/
public class GameWindow {
private static final Logger logger = LogManager.getLogger(GameWindow.class);
private final int width;
private final int height;
private final Stage stage;
private BaseScene currentScene;
private Scene scene;
final Communicator communicator;
/**
* Create a new GameWindow attached to the given stage with the specified width and height
* @param stage stage
* @param width width
* @param height height
*/
public GameWindow(Stage stage, int width, int height) {
this.width = width;
this.height = height;
this.stage = stage;
//Setup window
setupStage();
//Setup resources
setupResources();
//Setup default scene
setupDefaultScene();
//Setup communicator
communicator = new Communicator("ws://ofb-labs.soton.ac.uk:9700");
//Go to menu
startMenu();
}
/**
* Setup the font and any other resources we need
*/
private void setupResources() {
logger.info("Loading resources");
//We need to load fonts here due to the Font loader bug with spaces in URLs in the CSS files
Font.loadFont(getClass().getResourceAsStream("/style/Orbitron-Regular.ttf"),32);
Font.loadFont(getClass().getResourceAsStream("/style/Orbitron-Bold.ttf"),32);
Font.loadFont(getClass().getResourceAsStream("/style/Orbitron-ExtraBold.ttf"),32);
}
/**
* Display the main menu
*/
public void startMenu() {
loadScene(new MenuScene(this));
}
/**
* Display the single player challenge
*/
public void startChallenge() { loadScene(new ChallengeScene(this)); }
/**
* Setup the default settings for the stage itself (the window), such as the title and minimum width and height.
*/
public void setupStage() {
stage.setTitle("TetrECS");
stage.setMinWidth(width);
stage.setMinHeight(height + 20);
stage.setOnCloseRequest(ev -> App.getInstance().shutdown());
}
/**
* Load a given scene which extends BaseScene and switch over.
* @param newScene new scene to load
*/
public void loadScene(BaseScene newScene) {
//Cleanup remains of the previous scene
cleanup();
//Create the new scene and set it up
newScene.build();
currentScene = newScene;
scene = newScene.setScene();
stage.setScene(scene);
//Initialise the scene when ready
Platform.runLater(() -> currentScene.initialise());
}
/**
* Setup the default scene (an empty black scene) when no scene is loaded
*/
public void setupDefaultScene() {
this.scene = new Scene(new Pane(),width,height, Color.BLACK);
stage.setScene(this.scene);
}
/**
* When switching scenes, perform any cleanup needed, such as removing previous listeners
*/
public void cleanup() {
logger.info("Clearing up previous scene");
communicator.clearListeners();
}
/**
* Get the current scene being displayed
* @return scene
*/
public Scene getScene() {
return scene;
}
/**
* Get the width of the Game Window
* @return width
*/
public int getWidth() {
return this.width;
}
/**
* Get the height of the Game Window
* @return height
*/
public int getHeight() {
return this.height;
}
/**
* Get the communicator
* @return communicator
*/
public Communicator getCommunicator() {
return communicator;
}
}