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

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
*.iml
.DS_Store
.classpath
.factorypath
.idea
.idea/
.project
.settings
.settings/
scores.txt
target
target/

142
pom.xml Normal file
View File

@ -0,0 +1,142 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>uk.ac.soton.comp1206</groupId>
<artifactId>tetrecs</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>19</maven.compiler.source>
<maven.compiler.target>19</maven.compiler.target>
<javafx.version>21-ea+5</javafx.version>
</properties>
<profiles>
<profile>
<id>shade</id>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>${javafx.version}</version>
<classifier>win</classifier>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>${javafx.version}</version>
<classifier>mac</classifier>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>${javafx.version}</version>
<classifier>linux</classifier>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-media</artifactId>
<version>${javafx.version}</version>
<classifier>win</classifier>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-media</artifactId>
<version>${javafx.version}</version>
<classifier>mac</classifier>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-media</artifactId>
<version>${javafx.version}</version>
<classifier>linux</classifier>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<shadedArtifactAttached>true</shadedArtifactAttached>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>uk.ac.soton.comp1206.Launcher</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-media</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>com.neovisionaries</groupId>
<artifactId>nv-websocket-client</artifactId>
<version>2.14</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.5.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>19</source>
<target>19</target>
<release>19</release>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<mainClass>uk.ac.soton.comp1206/uk.ac.soton.comp1206.App</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,16 @@
module uk.ac.soton.comp1206 {
requires java.scripting;
requires javafx.controls;
requires javafx.fxml;
requires javafx.media;
requires org.apache.logging.log4j;
requires nv.websocket.client;
opens uk.ac.soton.comp1206.ui to javafx.fxml;
exports uk.ac.soton.comp1206;
exports uk.ac.soton.comp1206.ui;
exports uk.ac.soton.comp1206.network;
exports uk.ac.soton.comp1206.scene;
exports uk.ac.soton.comp1206.event;
exports uk.ac.soton.comp1206.component;
exports uk.ac.soton.comp1206.game;
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout
pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n" />
</Console>
</Appenders>
<Loggers>
<Root level="debug" additivity="false">
<AppenderRef ref="console" />
</Root>
</Loggers>
</Configuration>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,209 @@
.gamepane {
-fx-background-color: black;
}
.intro {
-fx-background-color: black;
}
.menu-background {
-fx-background-image: url("../images/1.jpg");
-fx-background-size: cover;
}
.challenge-background {
-fx-background-image: url("../images/2.jpg");
-fx-background-size: cover;
}
.menu {
-fx-padding: 10;
}
.menuItem {
-fx-fill: white;
-fx-font-family: 'Orbitron';
-fx-font-size: 32px;
-fx-font-weight: 700;
-fx-border-color: black;
-fx-stroke: black;
-fx-effect: dropshadow(gaussian, black, 1, 1.0, 1, 1);
}
.menuItem:hover {
-fx-fill: yellow;
}
.menuItem.selected {
-fx-fill: yellow;
}
.bigtitle {
-fx-fill: yellow;
-fx-font-family: 'Orbitron';
-fx-font-size: 64px;
-fx-font-weight: 900;
-fx-border-color: black;
-fx-stroke: black;
-fx-effect: dropshadow(gaussian, black, 1, 1.0, 1, 1);
}
.title {
-fx-fill: white;
-fx-font-family: 'Orbitron';
-fx-font-size: 32px;
-fx-font-weight: 900;
-fx-border-color: black;
-fx-stroke: black;
-fx-effect: dropshadow(gaussian, black, 1, 1.0, 1, 1);
}
.heading {
-fx-fill: white;
-fx-font-family: 'Orbitron';
-fx-font-size: 20px;
-fx-font-weight: 700;
-fx-border-color: black;
-fx-stroke: black;
-fx-effect: dropshadow(gaussian, black, 1, 1.0, 1, 1);
}
.score {
-fx-fill: yellow;
-fx-font-family: 'Orbitron';
-fx-font-size: 36px;
-fx-font-weight: 700;
-fx-border-color: black;
-fx-stroke: black;
-fx-text-alignment: center;
-fx-effect: dropshadow(gaussian, black, 1, 1.0, 1, 1);
}
.level {
-fx-fill: #ff6600;
-fx-font-family: 'Orbitron';
-fx-font-size: 24px;
-fx-font-weight: 700;
-fx-border-color: black;
-fx-stroke: black;
-fx-text-alignment: center;
-fx-effect: dropshadow(gaussian, black, 1, 1.0, 1, 1);
}
.hiscore {
-fx-fill: orange;
-fx-font-family: 'Orbitron';
-fx-font-size: 24px;
-fx-font-weight: 700;
-fx-border-color: black;
-fx-stroke: black;
-fx-text-alignment: center;
-fx-effect: dropshadow(gaussian, black, 1, 1.0, 1, 1);
}
.lives {
-fx-fill: yellow;
-fx-font-family: 'Orbitron';
-fx-font-size: 36px;
-fx-font-weight: 700;
-fx-border-color: black;
-fx-stroke: black;
-fx-text-alignment: center;
-fx-effect: dropshadow(gaussian, black, 1, 1.0, 1, 1);
}
.scorelist {
-fx-font-size: 20px;
-fx-font-family: 'Orbitron';
}
.scoreitem {
}
.scorer {
}
.myscore {
-fx-font-weight: 700;
}
.deadscore {
-fx-strikethrough: true;
}
.points {
}
.channelList {
}
.channelItem {
-fx-fill: white;
-fx-font-family: 'Orbitron';
-fx-font-size: 16px;
-fx-font-weight: 700;
-fx-border-color: black;
-fx-stroke: black;
}
.channelItem.selected {
-fx-fill: yellow;
}
.leaderboard {
-fx-font-size: 16px;
}
.gameBox {
-fx-padding: 10;
-fx-background-color: rgba(0, 0, 0, 0.5);
-fx-border-color: white;
-fx-border-width: 1;
}
.scroller {
-fx-background-color: transparent;
}
.scroller .viewport {
-fx-background-color: transparent;
}
.messages {
-fx-background-color: transparent;
-fx-font-size: 12px;
-fx-font-family: 'Orbitron';
}
.messages Text {
-fx-fill: white;
}
TextField {
-fx-border-color: white;
-fx-border-width: 1px;
-fx-background-color: rgba(0,0,0,0.5);
-fx-text-fill: white;
-fx-prompt-text-fill: grey;
}
.playerBox {
-fx-font-size: 14px;
-fx-font-family: 'Orbitron';
}
.playerBox Text {
-fx-fill: white;
}
.myname {
-fx-font-weight: 700;
}
.instructions {
-fx-font-size: 10px;
-fx-font-family: 'Orbitron';
-fx-fill: white;
}