diff --git a/racevisionGame/pom.xml b/racevisionGame/pom.xml index f77e5ea3..b0f6d203 100644 --- a/racevisionGame/pom.xml +++ b/racevisionGame/pom.xml @@ -83,89 +83,40 @@ - - - - mock - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.5.1 - - - org.apache.maven.plugins - maven-shade-plugin - 2.4.3 - - - - - visualiser.app.App - ${maven.compiler.source} - ${maven.compiler.target} - - - - - - - package - - shade - - - - - - - - - - - visualiser - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.5.1 - - - org.apache.maven.plugins - maven-shade-plugin - 2.4.3 - - - - - visualiser.app.App - ${maven.compiler.source} - ${maven.compiler.target} - - - - - - - package - - shade - - - - - - - - - - + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + + org.apache.maven.plugins + maven-shade-plugin + 2.4.3 + + + + + visualiser.app.App + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + + package + + shade + + + + + + diff --git a/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java b/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java index 120da194..ba22fef0 100644 --- a/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java +++ b/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java @@ -1,12 +1,11 @@ package mock.app; -import mock.enums.ConnectionStateEnum; +import mock.model.RaceLogic; import mock.model.ClientConnection; import mock.model.SourceIdAllocator; import mock.model.commandFactory.CompositeCommand; import network.Messages.Enums.XMLMessageType; import network.Messages.LatestMessages; -import network.Messages.RaceSnapshot; import network.Messages.XMLMessage; import java.io.IOException; @@ -65,19 +64,23 @@ public class ConnectionAcceptor implements Runnable { private short boatXMLSequenceNumber; //regatta xml sequence number private short regattaXMLSequenceNumber; + // + private RaceLogic raceLogic = null; /** * Connection Acceptor Constructor * @param latestMessages Latest messages to be sent * @param compositeCommand Collection of commands for race to execute. * @param sourceIdAllocator Object used to allocate source IDs for clients. + * @param raceLogic The race the client will connect to. * @throws IOException if a server socket cannot be instantiated. */ - public ConnectionAcceptor(LatestMessages latestMessages, CompositeCommand compositeCommand, SourceIdAllocator sourceIdAllocator) throws IOException { + public ConnectionAcceptor(LatestMessages latestMessages, CompositeCommand compositeCommand, SourceIdAllocator sourceIdAllocator, RaceLogic raceLogic) throws IOException { this.latestMessages = latestMessages; this.compositeCommand = compositeCommand; this.sourceIdAllocator = sourceIdAllocator; + this.raceLogic = raceLogic; this.serverSocket = new ServerSocket(serverPort); CheckClientConnection checkClientConnection = new CheckClientConnection(clientConnections); @@ -93,6 +96,8 @@ public class ConnectionAcceptor implements Runnable { return serverPort; } + + /** * Run the Acceptor */ @@ -103,11 +108,13 @@ public class ConnectionAcceptor implements Runnable { try { - System.out.println("Waiting for a connection...");//TEMP DEBUG REMOVE Socket mockSocket = serverSocket.accept(); - ClientConnection clientConnection = new ClientConnection(mockSocket, sourceIdAllocator, latestMessages, compositeCommand); + Logger.getGlobal().log(Level.INFO, String.format("Client connected. client ip/port = %s. Local ip/port = %s.", mockSocket.getRemoteSocketAddress(), mockSocket.getLocalSocketAddress())); + + + ClientConnection clientConnection = new ClientConnection(mockSocket, sourceIdAllocator, latestMessages, compositeCommand, raceLogic); clientConnections.add(clientConnection); @@ -153,6 +160,7 @@ public class ConnectionAcceptor implements Runnable { for (ClientConnection client : clientConnections) { if (!client.isAlive()) { connections.remove(client); + client.terminate(); Logger.getGlobal().log(Level.WARNING, "CheckClientConnection is removing the dead connection: " + client); } diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index 7d7c940b..5c6dd193 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -2,10 +2,7 @@ package mock.app; import mock.dataInput.PolarParser; import mock.exceptions.EventConstructionException; -import mock.model.MockRace; -import mock.model.Polars; -import mock.model.RaceLogic; -import mock.model.SourceIdAllocator; +import mock.model.*; import mock.model.commandFactory.CompositeCommand; import network.Messages.LatestMessages; import network.Messages.RaceSnapshot; @@ -15,6 +12,7 @@ import shared.exceptions.InvalidBoatDataException; import shared.exceptions.InvalidRaceDataException; import shared.exceptions.InvalidRegattaDataException; import shared.exceptions.XMLReaderException; +import shared.model.Bearing; import shared.model.Constants; import javax.xml.transform.TransformerException; @@ -67,15 +65,27 @@ public class Event { /** * Constructs an event, using various XML files. + * @param singlePlayer Whether or not to create a single player event. * @throws EventConstructionException Thrown if we cannot create an Event for any reason. */ - public Event() throws EventConstructionException { + public Event(boolean singlePlayer) throws EventConstructionException { + + singlePlayer = false; + + String raceXMLFile = "mock/mockXML/raceTest.xml"; + String boatsXMLFile = "mock/mockXML/boatTest.xml"; + String regattaXMLFile = "mock/mockXML/regattaTest.xml"; + + if (singlePlayer) { + raceXMLFile = "mock/mockXML/raceSinglePlayer.xml"; + boatsXMLFile = "mock/mockXML/boatsSinglePlayer.xml"; + } //Read XML files. try { - this.raceXML = getRaceXMLAtCurrentTime(XMLReader.readXMLFileToString("mock/mockXML/raceTest.xml", StandardCharsets.UTF_8)); - this.boatXML = XMLReader.readXMLFileToString("mock/mockXML/boatsSinglePlayer.xml", StandardCharsets.UTF_8); - this.regattaXML = XMLReader.readXMLFileToString("mock/mockXML/regattaTest.xml", StandardCharsets.UTF_8); + this.raceXML = getRaceXMLAtCurrentTime(XMLReader.readXMLFileToString(raceXMLFile, StandardCharsets.UTF_8)); + this.boatXML = XMLReader.readXMLFileToString(boatsXMLFile, StandardCharsets.UTF_8); + this.regattaXML = XMLReader.readXMLFileToString(regattaXMLFile, StandardCharsets.UTF_8); } catch (TransformerException | XMLReaderException e) { throw new EventConstructionException("Could not read XML files.", e); @@ -98,17 +108,48 @@ public class Event { } - //Create connection acceptor. this.sourceIdAllocator = new SourceIdAllocator(raceDataSource.getParticipants()); this.compositeCommand = new CompositeCommand(); this.latestMessages = new LatestMessages(); + //Create and start race. + WindGenerator windGenerator = new RandomWindGenerator( + Bearing.fromDegrees(225), + Bearing.fromDegrees(215), + Bearing.fromDegrees(235), + 12d, + 8d, + 16d ); + RaceLogic newRace = new RaceLogic( + new MockRace( + boatDataSource, + raceDataSource, + regattaDataSource, + this.latestMessages, + this.boatPolars, + Constants.RaceTimeScale, + windGenerator ), + this.latestMessages, + this.compositeCommand); + + new Thread(newRace, "Event.Start()->RaceLogic thread").start(); + + + //Create connection acceptor. + try { - this.connectionAcceptor = new ConnectionAcceptor(latestMessages, compositeCommand, sourceIdAllocator); + this.connectionAcceptor = new ConnectionAcceptor(latestMessages, compositeCommand, sourceIdAllocator, newRace); } catch (IOException e) { throw new EventConstructionException("Could not create ConnectionAcceptor.", e); } + + + new Thread(connectionAcceptor, "Event.Start()->ConnectionAcceptor thread").start(); + + sendXMLs(); + + } @@ -125,16 +166,8 @@ public class Event { * Sends the initial race data and then begins race simulation. */ public void start() { - new Thread(connectionAcceptor, "Event.Start()->ConnectionAcceptor thread").start(); - - sendXMLs(); - - //Create and start race. - RaceLogic newRace = new RaceLogic(new MockRace(boatDataSource, raceDataSource, regattaDataSource, this.latestMessages, this.boatPolars, Constants.RaceTimeScale), this.latestMessages, this.compositeCommand); - - new Thread(newRace, "Event.Start()->RaceLogic thread").start(); } /** diff --git a/racevisionGame/src/main/java/mock/exceptions/CommandConstructionException.java b/racevisionGame/src/main/java/mock/exceptions/CommandConstructionException.java new file mode 100644 index 00000000..f1b286f7 --- /dev/null +++ b/racevisionGame/src/main/java/mock/exceptions/CommandConstructionException.java @@ -0,0 +1,24 @@ +package mock.exceptions; + +/** + * An exception thrown when we cannot create a command for some reasn (e.g., uknown action type). + */ +public class CommandConstructionException extends Exception { + + /** + * Constructs the exception with a given message. + * @param message Message to store. + */ + public CommandConstructionException(String message) { + super(message); + } + + /** + * Constructs the exception with a given message and cause. + * @param message Message to store. + * @param cause Cause to store. + */ + public CommandConstructionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/racevisionGame/src/main/java/mock/model/ClientConnection.java b/racevisionGame/src/main/java/mock/model/ClientConnection.java index 71d85348..23e00819 100644 --- a/racevisionGame/src/main/java/mock/model/ClientConnection.java +++ b/racevisionGame/src/main/java/mock/model/ClientConnection.java @@ -36,6 +36,11 @@ public class ClientConnection implements Runnable { */ private HeartBeatService heartBeatService; + /** + * The thread the {@link HeartBeatService} runs on. + */ + private Thread heartBeatThread; + /** * Used to allocate source ID to client, if they request to participate. @@ -53,16 +58,31 @@ public class ClientConnection implements Runnable { */ private CompositeCommand compositeCommand; + /** + * The race the client is connected to. + */ + private RaceLogic raceLogic; + /** * Used to send the race snapshot to client. */ private MockOutput mockOutput; + /** + * The thread the {@link MockOutput} runs on. + */ + private Thread mockOutputThread; + /** * Used to receive client input, and turn it into commands. */ private ControllerServer controllerServer; + /** + * The thread the {@link ControllerServer} runs on. + */ + private Thread controllerServerThread; + /** * Used to write messages to socket. @@ -100,13 +120,15 @@ public class ClientConnection implements Runnable { * @param sourceIdAllocator Used to allocate a source ID for the client. * @param latestMessages Latest race snapshot to send to client. * @param compositeCommand Collection of commands for race to execute. + * @param raceLogic The race the client is connected to. * @throws IOException Thrown if there is a problem with the client socket. */ - public ClientConnection(Socket socket, SourceIdAllocator sourceIdAllocator, LatestMessages latestMessages, CompositeCommand compositeCommand) throws IOException { + public ClientConnection(Socket socket, SourceIdAllocator sourceIdAllocator, LatestMessages latestMessages, CompositeCommand compositeCommand, RaceLogic raceLogic) throws IOException { this.socket = socket; this.sourceIdAllocator = sourceIdAllocator; this.latestMessages = latestMessages; this.compositeCommand = compositeCommand; + this.raceLogic = raceLogic; this.outputQueue = new LinkedBlockingQueue<>(); this.inputQueue = new LinkedBlockingQueue<>(); @@ -120,7 +142,8 @@ public class ClientConnection implements Runnable { this.heartBeatService = new HeartBeatService(outputQueue); - new Thread(heartBeatService, "ClientConnection()->HeartBeatService thread " + heartBeatService).start(); + this.heartBeatThread = new Thread(heartBeatService, "ClientConnection()->HeartBeatService thread " + heartBeatService); + this.heartBeatThread.start(); } @@ -145,7 +168,7 @@ public class ClientConnection implements Runnable { * @throws HandshakeException Thrown if something goes wrong with the handshake. * @throws SourceIDAllocationException Thrown if we cannot allocate a sourceID. */ - private void handshake() throws SourceIDAllocationException, HandshakeException { + private void handshake() throws SourceIDAllocationException, HandshakeException { //This function is a bit messy, and could probably be refactored a bit. @@ -162,16 +185,20 @@ public class ClientConnection implements Runnable { allocatedSourceID = sourceIdAllocator.allocateSourceID(); - this.controllerServer = new ControllerServer(compositeCommand, inputQueue, allocatedSourceID); - new Thread(controllerServer, "ClientConnection.run()->ControllerServer thread" + controllerServer).start(); + this.controllerServer = new ControllerServer(compositeCommand, inputQueue, allocatedSourceID, raceLogic.getRace()); + this.controllerServerThread = new Thread(controllerServer, "ClientConnection.run()->ControllerServer thread" + controllerServer); + this.controllerServerThread.start(); } - this.mockOutput = new MockOutput(latestMessages, outputQueue); - new Thread(mockOutput, "ClientConnection.run()->MockOutput thread" + mockOutput).start(); sendJoinAcceptanceMessage(allocatedSourceID); + this.mockOutput = new MockOutput(latestMessages, outputQueue); + this.mockOutputThread = new Thread(mockOutput, "ClientConnection.run()->MockOutput thread" + mockOutput); + this.mockOutputThread.start(); + + connectionState = ConnectionStateEnum.CONNECTED; } @@ -240,4 +267,22 @@ public class ClientConnection implements Runnable { } + /** + * Terminates this connection. + */ + public void terminate() { + + if (this.heartBeatThread != null) { + this.heartBeatThread.interrupt(); + } + + if (this.mockOutputThread != null) { + this.mockOutputThread.interrupt(); + } + + if (this.controllerServerThread != null) { + this.controllerServerThread.interrupt(); + } + } + } diff --git a/racevisionGame/src/main/java/mock/model/ConstantWindGenerator.java b/racevisionGame/src/main/java/mock/model/ConstantWindGenerator.java new file mode 100644 index 00000000..ae14daac --- /dev/null +++ b/racevisionGame/src/main/java/mock/model/ConstantWindGenerator.java @@ -0,0 +1,54 @@ +package mock.model; + + +import shared.model.Bearing; +import shared.model.Wind; + +import java.util.Random; + +/** + * This class generates Wind objects for use in a MockRace. + * Initialised with a baseline wind speed and direction, and keeps it constant. + */ +public class ConstantWindGenerator implements WindGenerator { + + /** + * The bearing the wind direction starts at. + */ + private Bearing windBaselineBearing; + + + /** + * The speed the wind starts at, in knots. + */ + private double windBaselineSpeed; + + + + /** + * Creates a constant wind generator, with a baseline wind speed and direction. + * @param windBaselineBearing Baseline wind direction. + * @param windBaselineSpeed Baseline wind speed, in knots. + */ + public ConstantWindGenerator(Bearing windBaselineBearing, double windBaselineSpeed) { + + this.windBaselineBearing = windBaselineBearing; + this.windBaselineSpeed = windBaselineSpeed; + + } + + + @Override + public Wind generateBaselineWind() { + return new Wind(windBaselineBearing, windBaselineSpeed); + } + + + @Override + public Wind generateNextWind(Wind currentWind) { + + return generateBaselineWind(); + + } + +} diff --git a/racevisionGame/src/main/java/mock/model/MockBoat.java b/racevisionGame/src/main/java/mock/model/MockBoat.java index 104fa264..a2f74dd7 100644 --- a/racevisionGame/src/main/java/mock/model/MockBoat.java +++ b/racevisionGame/src/main/java/mock/model/MockBoat.java @@ -25,7 +25,7 @@ public class MockBoat extends Boat { /** * Stores whether the boat is on autoVMG or not */ - private boolean autoVMG = true; + private boolean autoVMG = false; diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index e389f474..94fc8792 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -2,7 +2,6 @@ package mock.model; import network.Messages.Enums.BoatStatusEnum; import network.Messages.LatestMessages; -import org.opengis.geometry.primitive.*; import shared.dataInput.BoatDataSource; import shared.dataInput.RaceDataSource; import network.Messages.Enums.RaceStatusEnum; @@ -13,7 +12,6 @@ import shared.model.Bearing; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; -import java.time.temporal.TemporalUnit; import java.util.*; import static java.lang.Math.cos; @@ -59,7 +57,7 @@ public class MockRace extends Race { * @param polars The polars table to be used for boat simulation. * @param timeScale The timeScale for the race. See {@link Constants#RaceTimeScale}. */ - public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, Polars polars, int timeScale) { + public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, Polars polars, int timeScale, WindGenerator windGenerator) { super(boatDataSource, raceDataSource, regattaDataSource, latestMessages); @@ -69,14 +67,8 @@ public class MockRace extends Race { this.shrinkBoundary = GPSCoordinate.getShrinkBoundary(this.boundary); - //Set up wind generator. It may be tidier to create this outside the race (with the values sourced from a data file maybe?) and pass it in. - this.windGenerator = new WindGenerator( - Bearing.fromDegrees(225), - Bearing.fromDegrees(215), - Bearing.fromDegrees(235), - 12d, - 8d, - 16d ); + + this.windGenerator = windGenerator; //Wind. this.setWind(windGenerator.generateBaselineWind()); diff --git a/racevisionGame/src/main/java/mock/model/RaceLogic.java b/racevisionGame/src/main/java/mock/model/RaceLogic.java index 42bea8ef..7b314d0f 100644 --- a/racevisionGame/src/main/java/mock/model/RaceLogic.java +++ b/racevisionGame/src/main/java/mock/model/RaceLogic.java @@ -2,14 +2,15 @@ package mock.model; import javafx.animation.AnimationTimer; import mock.model.commandFactory.CompositeCommand; +import mock.model.commandFactory.CommandFactory; +import network.Messages.Enums.BoatActionEnum; import network.Messages.Enums.BoatStatusEnum; import network.Messages.Enums.RaceStatusEnum; import network.Messages.LatestMessages; +import shared.model.RunnableWithFramePeriod; - -public class RaceLogic implements Runnable { - +public class RaceLogic implements RunnableWithFramePeriod { /** * State of current race modified by this object */ @@ -66,6 +67,7 @@ public class RaceLogic implements Runnable { //Parse the race snapshot. server.parseSnapshot(); + // Change wind direction race.changeWindDirection(); @@ -113,7 +115,7 @@ public class RaceLogic implements Runnable { currentTime = System.currentTimeMillis(); //Execute commands from clients. - commands.execute(race); + commands.execute(); //Update race time. race.updateRaceTime(currentTime); @@ -172,4 +174,12 @@ public class RaceLogic implements Runnable { } }; + + /** + * Returns the race state that this RaceLogic is simulating. + * @return Race state this RaceLogic is simulating. + */ + public MockRace getRace() { + return race; + } } diff --git a/racevisionGame/src/main/java/mock/model/RandomWindGenerator.java b/racevisionGame/src/main/java/mock/model/RandomWindGenerator.java new file mode 100644 index 00000000..4f981b8d --- /dev/null +++ b/racevisionGame/src/main/java/mock/model/RandomWindGenerator.java @@ -0,0 +1,242 @@ +package mock.model; + + +import shared.model.Bearing; +import shared.model.Wind; + +import java.util.Random; + +/** + * This class generates Wind objects for use in a MockRace. + * Bounds on bearing and speed can be specified. + * Wind can be completely random, or random incremental change. + */ +public class RandomWindGenerator implements WindGenerator { + + /** + * The bearing the wind direction starts at. + */ + private Bearing windBaselineBearing; + + /** + * The lower bearing angle that the wind may have. + */ + private Bearing windBearingLowerBound; + + /** + * The upper bearing angle that the wind may have. + */ + private Bearing windBearingUpperBound; + + + + /** + * The speed the wind starts at, in knots. + */ + private double windBaselineSpeed; + + /** + * The lower speed that the wind may have, in knots. + */ + private double windSpeedLowerBound; + + /** + * The upper speed that the wind may have, in knots. + */ + private double windSpeedUpperBound; + + + /** + * Creates a wind generator, with a baseline, lower bound, and upper bound, for the wind speed and direction. + * @param windBaselineBearing Baseline wind direction. + * @param windBearingLowerBound Lower bound for wind direction. + * @param windBearingUpperBound Upper bound for wind direction. + * @param windBaselineSpeed Baseline wind speed, in knots. + * @param windSpeedLowerBound Lower bound for wind speed, in knots. + * @param windSpeedUpperBound Upper bound for wind speed, in knots. + */ + public RandomWindGenerator(Bearing windBaselineBearing, Bearing windBearingLowerBound, Bearing windBearingUpperBound, double windBaselineSpeed, double windSpeedLowerBound, double windSpeedUpperBound) { + + this.windBaselineBearing = windBaselineBearing; + this.windBearingLowerBound = windBearingLowerBound; + this.windBearingUpperBound = windBearingUpperBound; + this.windBaselineSpeed = windBaselineSpeed; + this.windSpeedLowerBound = windSpeedLowerBound; + this.windSpeedUpperBound = windSpeedUpperBound; + + } + + + @Override + public Wind generateBaselineWind() { + return new Wind(windBaselineBearing, windBaselineSpeed); + } + + /** + * Generates a random Wind object, that is within the provided bounds. + * @return Generated wind object. + */ + public Wind generateRandomWind() { + + double windSpeed = generateRandomWindSpeed(); + Bearing windBearing = generateRandomWindBearing(); + + return new Wind(windBearing, windSpeed); + + } + + /** + * Generates a random wind speed within the specified bounds. In knots. + * @return Wind speed, in knots. + */ + private double generateRandomWindSpeed() { + + double randomSpeedKnots = generateRandomValueInBounds(windSpeedLowerBound, windSpeedUpperBound); + + return randomSpeedKnots; + } + + + /** + * Generates a random wind bearing within the specified bounds. + * @return Wind bearing. + */ + private Bearing generateRandomWindBearing() { + + double randomBearingDegrees = generateRandomValueInBounds(windBearingLowerBound.degrees(), windBearingUpperBound.degrees()); + + return Bearing.fromDegrees(randomBearingDegrees); + } + + + /** + * Generates a random value within a specified interval. + * @param lowerBound The lower bound of the interval. + * @param upperBound The upper bound of the interval. + * @return A random value within the interval. + */ + private static double generateRandomValueInBounds(double lowerBound, double upperBound) { + + float proportion = new Random().nextFloat(); + + double delta = upperBound - lowerBound; + + double amount = delta * proportion; + + double finalAmount = amount + lowerBound; + + return finalAmount; + + } + + + /** + * Generates a new value within an interval, given a start value, chance to change, and change amount. + * @param lowerBound Lower bound of interval. + * @param upperBound Upper bound of interval. + * @param currentValue The current value to change. + * @param changeAmount The amount to change by. + * @param chanceToChange The change to actually change the value. + * @return The new value. + */ + private static double generateNextValueInBounds(double lowerBound, double upperBound, double currentValue, double changeAmount, double chanceToChange) { + + float chance = new Random().nextFloat(); + + + if (chance <= chanceToChange) { + currentValue += changeAmount; + + } else if (chance <= (2 * chanceToChange)) { + currentValue -= changeAmount; + + } + + currentValue = clamp(lowerBound, upperBound, currentValue); + + return currentValue; + + } + + + @Override + public Wind generateNextWind(Wind currentWind) { + + double windSpeed = generateNextWindSpeed(currentWind.getWindSpeed()); + Bearing windBearing = generateNextWindBearing(currentWind.getWindDirection()); + + return new Wind(windBearing, windSpeed); + + } + + + /** + * Generates the next wind speed to use. + * @param windSpeed Current wind speed, in knots. + * @return Next wind speed, in knots. + */ + private double generateNextWindSpeed(double windSpeed) { + + double chanceToChange = 0.2; + double changeAmount = 0.1; + + double nextWindSpeed = generateNextValueInBounds( + windSpeedLowerBound, + windSpeedUpperBound, + windSpeed, + changeAmount, + chanceToChange); + + return nextWindSpeed; + } + + + /** + * Generates the next wind speed to use. + * @param windBearing Current wind bearing. + * @return Next wind speed. + */ + private Bearing generateNextWindBearing(Bearing windBearing) { + + double chanceToChange = 0.2; + double changeAmount = 0.5; + + double nextWindBearingDegrees = generateNextValueInBounds( + windBearingLowerBound.degrees(), + windBearingUpperBound.degrees(), + windBearing.degrees(), + changeAmount, + chanceToChange); + + return Bearing.fromDegrees(nextWindBearingDegrees); + } + + + + + + /** + * Clamps a value to be within an interval. + * @param lower Lower bound of the interval. + * @param upper Upper bound of the interval. + * @param value Value to clamp. + * @return The clamped value. + */ + private static double clamp(double lower, double upper, double value) { + + if (value > upper) { + value = upper; + + } else if (value < lower) { + value = lower; + + } + + return value; + } + + + + + +} diff --git a/racevisionGame/src/main/java/mock/model/WindGenerator.java b/racevisionGame/src/main/java/mock/model/WindGenerator.java index 30fd24b4..8285d5d3 100644 --- a/racevisionGame/src/main/java/mock/model/WindGenerator.java +++ b/racevisionGame/src/main/java/mock/model/WindGenerator.java @@ -1,249 +1,29 @@ package mock.model; - -import shared.model.Bearing; import shared.model.Wind; -import java.util.Random; /** - * This class generates Wind objects for use in a MockRace. - * Bounds on bearing and speed can be specified. - * Wind can be completely random, or random incremental change. + * Interface for wind generators. It allows for generating a baseline wind, and subsequent winds. */ -public class WindGenerator { - - /** - * The bearing the wind direction starts at. - */ - private Bearing windBaselineBearing; - - /** - * The lower bearing angle that the wind may have. - */ - private Bearing windBearingLowerBound; - - /** - * The upper bearing angle that the wind may have. - */ - private Bearing windBearingUpperBound; - - - - /** - * The speed the wind starts at, in knots. - */ - private double windBaselineSpeed; - - /** - * The lower speed that the wind may have, in knots. - */ - private double windSpeedLowerBound; - - /** - * The upper speed that the wind may have, in knots. - */ - private double windSpeedUpperBound; - - - /** - * Creates a wind generator, with a baseline, lower bound, and upper bound, for the wind speed and direction. - * @param windBaselineBearing Baseline wind direction. - * @param windBearingLowerBound Lower bound for wind direction. - * @param windBearingUpperBound Upper bound for wind direction. - * @param windBaselineSpeed Baseline wind speed, in knots. - * @param windSpeedLowerBound Lower bound for wind speed, in knots. - * @param windSpeedUpperBound Upper bound for wind speed, in knots. - */ - public WindGenerator(Bearing windBaselineBearing, Bearing windBearingLowerBound, Bearing windBearingUpperBound, double windBaselineSpeed, double windSpeedLowerBound, double windSpeedUpperBound) { - - this.windBaselineBearing = windBaselineBearing; - this.windBearingLowerBound = windBearingLowerBound; - this.windBearingUpperBound = windBearingUpperBound; - this.windBaselineSpeed = windBaselineSpeed; - this.windSpeedLowerBound = windSpeedLowerBound; - this.windSpeedUpperBound = windSpeedUpperBound; +public interface WindGenerator { - } /** * Generates a wind object using the baseline wind speed and bearing. * @return Baseline wind object. */ - public Wind generateBaselineWind() { - return new Wind(windBaselineBearing, windBaselineSpeed); - } - - /** - * Generates a random Wind object, that is within the provided bounds. - * @return Generated wind object. - */ - public Wind generateRandomWind() { - - double windSpeed = generateRandomWindSpeed(); - Bearing windBearing = generateRandomWindBearing(); - - return new Wind(windBearing, windSpeed); - - } - - /** - * Generates a random wind speed within the specified bounds. In knots. - * @return Wind speed, in knots. - */ - private double generateRandomWindSpeed() { - - double randomSpeedKnots = generateRandomValueInBounds(windSpeedLowerBound, windSpeedUpperBound); - - return randomSpeedKnots; - } - - - /** - * Generates a random wind bearing within the specified bounds. - * @return Wind bearing. - */ - private Bearing generateRandomWindBearing() { - - double randomBearingDegrees = generateRandomValueInBounds(windBearingLowerBound.degrees(), windBearingUpperBound.degrees()); - - return Bearing.fromDegrees(randomBearingDegrees); - } - - - /** - * Generates a random value within a specified interval. - * @param lowerBound The lower bound of the interval. - * @param upperBound The upper bound of the interval. - * @return A random value within the interval. - */ - private static double generateRandomValueInBounds(double lowerBound, double upperBound) { + Wind generateBaselineWind(); - float proportion = new Random().nextFloat(); - - double delta = upperBound - lowerBound; - - double amount = delta * proportion; - - double finalAmount = amount + lowerBound; - - return finalAmount; - - } - - - /** - * Generates a new value within an interval, given a start value, chance to change, and change amount. - * @param lowerBound Lower bound of interval. - * @param upperBound Upper bound of interval. - * @param currentValue The current value to change. - * @param changeAmount The amount to change by. - * @param chanceToChange The change to actually change the value. - * @return The new value. - */ - private static double generateNextValueInBounds(double lowerBound, double upperBound, double currentValue, double changeAmount, double chanceToChange) { - - float chance = new Random().nextFloat(); - - - if (chance <= chanceToChange) { - currentValue += changeAmount; - - } else if (chance <= (2 * chanceToChange)) { - currentValue -= changeAmount; - - } - - currentValue = clamp(lowerBound, upperBound, currentValue); - - return currentValue; - - } /** - * Generates the next Wind object, that is within the provided bounds. This randomly increases or decreases the wind's speed and bearing. + * Generates the next Wind object, according to the implementation of the wind generator. * @param currentWind The current wind to change. This is not modified. * @return Generated wind object. */ - public Wind generateNextWind(Wind currentWind) { - - double windSpeed = generateNextWindSpeed(currentWind.getWindSpeed()); - Bearing windBearing = generateNextWindBearing(currentWind.getWindDirection()); - - return new Wind(windBearing, windSpeed); - - } - - - /** - * Generates the next wind speed to use. - * @param windSpeed Current wind speed, in knots. - * @return Next wind speed, in knots. - */ - private double generateNextWindSpeed(double windSpeed) { - - double chanceToChange = 0.2; - double changeAmount = 0.1; - - double nextWindSpeed = generateNextValueInBounds( - windSpeedLowerBound, - windSpeedUpperBound, - windSpeed, - changeAmount, - chanceToChange); - - return nextWindSpeed; - } - - - /** - * Generates the next wind speed to use. - * @param windBearing Current wind bearing. - * @return Next wind speed. - */ - private Bearing generateNextWindBearing(Bearing windBearing) { - - double chanceToChange = 0.2; - double changeAmount = 0.5; - - double nextWindBearingDegrees = generateNextValueInBounds( - windBearingLowerBound.degrees(), - windBearingUpperBound.degrees(), - windBearing.degrees(), - changeAmount, - chanceToChange); - - return Bearing.fromDegrees(nextWindBearingDegrees); - } - - - - - - /** - * Clamps a value to be within an interval. - * @param lower Lower bound of the interval. - * @param upper Upper bound of the interval. - * @param value Value to clamp. - * @return The clamped value. - */ - private static double clamp(double lower, double upper, double value) { - - if (value > upper) { - value = upper; - - } else if (value < lower) { - value = lower; - - } - - return value; - } - - - + Wind generateNextWind(Wind currentWind); } diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/CommandFactory.java b/racevisionGame/src/main/java/mock/model/commandFactory/CommandFactory.java index 6e87f11b..4edb87c5 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/CommandFactory.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/CommandFactory.java @@ -1,8 +1,10 @@ package mock.model.commandFactory; +import mock.exceptions.CommandConstructionException; import mock.model.MockBoat; import mock.model.MockRace; -import network.Messages.Enums.BoatActionEnum; +import network.Messages.BoatAction; +import shared.exceptions.BoatNotFoundException; /** * Factory class for Command objects @@ -11,15 +13,28 @@ public class CommandFactory { /** * Generates a command on a race and boat corresponding to the protocol action number. * @param race to receive command - * @param boat to receive command in race * @param action number to select command - * @return + * @return The command to execute the given action. + * @throws CommandConstructionException Thrown if the command cannot be constructed (e.g., unknown action type). */ - public static Command createCommand(MockRace race, MockBoat boat, BoatActionEnum action) { - switch(action) { + public static Command createCommand(MockRace race, BoatAction action) throws CommandConstructionException { + + MockBoat boat = null; + try { + boat = race.getBoat(action.getSourceID()); + + } catch (BoatNotFoundException e) { + throw new CommandConstructionException("Could not create command for BoatAction: " + action + ". Boat with sourceID: " + action.getSourceID() + " not found.", e); + + } + + switch(action.getBoatAction()) { case AUTO_PILOT: return new VMGCommand(race, boat); case TACK_GYBE: return new TackGybeCommand(race, boat); - default: return null; // TODO - please please have discussion over what to default to + case UPWIND: return new WindCommand(race, boat, true); + case DOWNWIND: return new WindCommand(race, boat, false); + + default: throw new CommandConstructionException("Could not create command for BoatAction: " + action + ". Unknown BoatAction."); } } } diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/CompositeCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/CompositeCommand.java index ff09103d..74c5e95b 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/CompositeCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/CompositeCommand.java @@ -1,25 +1,24 @@ package mock.model.commandFactory; -import mock.model.MockRace; - -import java.util.Stack; +import java.util.ArrayDeque; +import java.util.Queue; /** * Wraps multiple commands into a composite to execute queued commands during a frame. */ public class CompositeCommand implements Command { - private Stack commands; + private Queue commands; public CompositeCommand() { - this.commands = new Stack<>(); + this.commands = new ArrayDeque<>(); } public void addCommand(Command command) { - commands.push(command); + commands.add(command); } @Override - public void execute(MockRace race) { - while(!commands.isEmpty()) commands.pop().execute(race); + public void execute() { + while(!commands.isEmpty()) commands.remove().execute(); } } diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java index 150a1da8..8dcf1c48 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java @@ -18,6 +18,9 @@ public class TackGybeCommand implements Command { //The refactoring of MockRace will require changes to be made @Override public void execute() { + + boat.setAutoVMG(false); + /*VMG newVMG = boat.getPolars().calculateVMG( race.getWindDirection(), race.getWindSpeed(), diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java index 64cc6a9f..1a1eeda5 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java @@ -18,6 +18,7 @@ public class VMGCommand implements Command { //The refactoring of MockRace will require changes to be made @Override public void execute() { + boat.setAutoVMG(true); /*VMG newVMG = boat.getPolars().calculateVMG( race.getWindDirection(), race.getWindSpeed(), diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/WindCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/WindCommand.java new file mode 100644 index 00000000..530bf5bc --- /dev/null +++ b/racevisionGame/src/main/java/mock/model/commandFactory/WindCommand.java @@ -0,0 +1,37 @@ +package mock.model.commandFactory; + +import mock.model.MockBoat; +import mock.model.MockRace; +import shared.model.Bearing; + +/** + * Created by connortaylorbrown on 4/08/17. + */ +public class WindCommand implements Command { + private MockRace race; + private MockBoat boat; + private int direction; + + public WindCommand(MockRace race, MockBoat boat, boolean upwind) { + this.race = race; + this.boat = boat; + this.direction = upwind? -1 : 1; + } + + @Override + public void execute() { + + boat.setAutoVMG(false); + + double wind = race.getWindDirection().degrees(); + double heading = boat.getBearing().degrees(); + + double offset = 3.0; + + offset *= direction; + double headWindDelta = wind - heading; + if ((headWindDelta < 0) || (headWindDelta > 180)) offset *= -1; + + boat.setBearing(Bearing.fromDegrees(heading + offset)); + } +} diff --git a/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java b/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java index 39cb0024..028e01a9 100644 --- a/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java +++ b/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java @@ -3,15 +3,12 @@ package network.StreamRelated; import network.BinaryMessageDecoder; import network.Exceptions.InvalidMessageException; -import network.MessageEncoders.RaceVisionByteEncoder; import network.Messages.AC35Data; import shared.model.RunnableWithFramePeriod; import java.io.*; import java.nio.ByteBuffer; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; @@ -36,9 +33,11 @@ public class MessageDeserialiser implements RunnableWithFramePeriod { /** - * Ack numbers used in messages. + * Determines whether or not this runnable is currently running. */ - private int ackNumber = 1; + private boolean isRunning; + + /** @@ -52,16 +51,6 @@ public class MessageDeserialiser implements RunnableWithFramePeriod { } - /** - * Increments the ackNumber value, and returns it. - * @return Incremented ackNumber. - */ - private int getNextAckNumber(){ - this.ackNumber++; - - return this.ackNumber; - } - /** @@ -119,34 +108,35 @@ public class MessageDeserialiser implements RunnableWithFramePeriod { } + /** + * Determines whether or not this runnable is running. + * @return True means that it is still running, false means that it has stopped. + */ + public boolean isRunning() { + return isRunning; + } + + @Override public void run() { - long previousFrameTime = System.currentTimeMillis(); + isRunning = true; while (!Thread.interrupted()) { - - long currentFrameTime = System.currentTimeMillis(); - waitForFramePeriod(previousFrameTime, currentFrameTime, 16); - previousFrameTime = currentFrameTime; - - //Reads the next message. try { AC35Data message = this.getNextMessage(); messagesRead.add(message); } - catch (InvalidMessageException | IOException e) { - - Logger.getGlobal().log(Level.WARNING, "Unable to read message.", e); + catch (InvalidMessageException e) { + Logger.getGlobal().log(Level.WARNING, "Unable to read message on thread: " + Thread.currentThread() + ".", e); - try { - inputStream.reset(); - } catch (IOException e1) { - Logger.getGlobal().log(Level.WARNING, "Unable to reset inputStream.", e); - } + } catch (IOException e) { + Logger.getGlobal().log(Level.SEVERE, "Unable to read inputStream: " + inputStream + " on thread: " + Thread.currentThread() + ".", e); + isRunning = false; + return; } diff --git a/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java b/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java index 02e6f7a6..8d2a7038 100644 --- a/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java +++ b/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java @@ -81,7 +81,7 @@ public class MessageSerialiser implements RunnableWithFramePeriod { isRunning = true; - while (isRunning) { + while (!Thread.interrupted()) { long currentFrameTime = System.currentTimeMillis(); @@ -104,8 +104,9 @@ public class MessageSerialiser implements RunnableWithFramePeriod { Logger.getGlobal().log(Level.WARNING, "Could not encode message: " + message, e); } catch (IOException e) { - Logger.getGlobal().log(Level.WARNING, "Could not write message to outputStream: " + outputStream, e); + Logger.getGlobal().log(Level.SEVERE, "Could not write message to outputStream: " + outputStream + " on thread: " + Thread.currentThread(), e); isRunning = false; + return; } } diff --git a/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java b/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java index 5f1e2d8d..a8caad09 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java @@ -19,6 +19,7 @@ import java.net.URL; import java.net.UnknownHostException; import java.util.ResourceBundle; +//TODO it appears this view/controller was replaced by Lobby.fxml. Remove? /** * Controls the connection that the VIsualiser can connect to. */ @@ -82,11 +83,6 @@ public class ConnectionController extends Controller { private ObservableList connections; - /** - * Represents whether the client is currently hosting a game already - this is to ensure they don't launch multiple servers. - */ - private boolean currentlyHostingGame = false; - @Override public void initialize(URL location, ResourceBundle resources) { diff --git a/racevisionGame/src/main/java/visualiser/Controllers/HostController.java b/racevisionGame/src/main/java/visualiser/Controllers/HostController.java index 7873e8e6..3360e514 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/HostController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/HostController.java @@ -47,7 +47,7 @@ public class HostController extends Controller { */ public void hostGamePressed() throws IOException{ try { - Event game = new Event(); + Event game = new Event(true); game.start(); connectSocket("localhost", 4942); } catch (EventConstructionException e) { @@ -78,7 +78,6 @@ public class HostController extends Controller { */ public void hostGame(){ hostWrapper.setVisible(true); - System.out.println("Reacted hostGame"); } } diff --git a/racevisionGame/src/main/java/visualiser/Controllers/LobbyController.java b/racevisionGame/src/main/java/visualiser/Controllers/LobbyController.java index dc4d8e59..5ae51d35 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/LobbyController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/LobbyController.java @@ -58,7 +58,6 @@ public class LobbyController extends Controller { } else { joinGameBtn.setDisable(true); - System.out.println(curr.statusProperty().getValue()); } }); joinGameBtn.setDisable(true); @@ -70,11 +69,6 @@ public class LobbyController extends Controller { public void refreshBtnPressed(){ for(RaceConnection connection: connections) { connection.check(); - try { - Thread.sleep(500); - } catch (InterruptedException e) { - e.printStackTrace(); - } } try { if (lobbyTable.getSelectionModel().getSelectedItem().statusProperty().getValue().equals("Ready")) { diff --git a/racevisionGame/src/main/java/visualiser/Controllers/MainController.java b/racevisionGame/src/main/java/visualiser/Controllers/MainController.java index 80ef2da4..900e43b0 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/MainController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/MainController.java @@ -5,6 +5,7 @@ import javafx.fxml.FXML; import javafx.scene.layout.AnchorPane; import visualiser.app.VisualiserInput; import visualiser.gameController.ControllerClient; +import visualiser.model.ServerConnection; import visualiser.model.VisualiserBoat; import visualiser.model.VisualiserRace; @@ -36,12 +37,12 @@ public class MainController extends Controller { /** * Transitions from the StartController screen (displays pre-race information) to the RaceController (displays the actual race). - * @param visualiserInput The object used to read packets from the race server. * @param visualiserRace The object modelling the race. * @param controllerClient Socket Client that manipulates the controller. + * @param serverConnection The connection to the server. */ - public void beginRace(VisualiserInput visualiserInput, VisualiserRace visualiserRace, ControllerClient controllerClient) { - raceController.startRace(visualiserInput, visualiserRace, controllerClient); + public void beginRace(VisualiserRace visualiserRace, ControllerClient controllerClient, ServerConnection serverConnection) { + raceController.startRace(visualiserRace, controllerClient, serverConnection); } /** @@ -103,10 +104,15 @@ public class MainController extends Controller { AnchorPane.setLeftAnchor(startController.startWrapper(), 0.0); AnchorPane.setRightAnchor(startController.startWrapper(), 0.0); - AnchorPane.setTopAnchor(connectionController.startWrapper(), 0.0); - AnchorPane.setBottomAnchor(connectionController.startWrapper(), 0.0); - AnchorPane.setLeftAnchor(connectionController.startWrapper(), 0.0); - AnchorPane.setRightAnchor(connectionController.startWrapper(), 0.0); + AnchorPane.setTopAnchor(lobbyController.startWrapper(), 0.0); + AnchorPane.setBottomAnchor(lobbyController.startWrapper(), 0.0); + AnchorPane.setLeftAnchor(lobbyController.startWrapper(), 0.0); + AnchorPane.setRightAnchor(lobbyController.startWrapper(), 0.0); + + AnchorPane.setTopAnchor(hostController.startWrapper(), 0.0); + AnchorPane.setBottomAnchor(hostController.startWrapper(), 0.0); + AnchorPane.setLeftAnchor(hostController.startWrapper(), 0.0); + AnchorPane.setRightAnchor(hostController.startWrapper(), 0.0); AnchorPane.setTopAnchor(finishController.finishWrapper, 0.0); AnchorPane.setBottomAnchor(finishController.finishWrapper, 0.0); diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java index 5a1836f6..0a5fb33a 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java @@ -34,10 +34,6 @@ import java.util.logging.Logger; */ public class RaceController extends Controller { - /** - * The object used to read packets from the connected server. - */ - private VisualiserInput visualiserInput; /** * The race object which describes the currently occurring race. @@ -69,6 +65,11 @@ public class RaceController extends Controller { */ private ControllerClient controllerClient; + /** + * The connection to the server. + */ + private ServerConnection serverConnection; + @FXML private GridPane canvasBase; @@ -351,15 +352,15 @@ public class RaceController extends Controller { /** * Displays a specified race. - * @param visualiserInput Object used to read packets from server. * @param visualiserRace Object modelling the race. * @param controllerClient Socket Client that manipulates the controller. + * @param serverConnection The connection to the server. */ - public void startRace(VisualiserInput visualiserInput, VisualiserRace visualiserRace, ControllerClient controllerClient) { + public void startRace(VisualiserRace visualiserRace, ControllerClient controllerClient, ServerConnection serverConnection) { - this.visualiserInput = visualiserInput; this.visualiserRace = visualiserRace; this.controllerClient = controllerClient; + this.serverConnection = serverConnection; initialiseRace(); @@ -420,6 +421,15 @@ public class RaceController extends Controller { } + //Return to main screen if we lose connection. + if (!serverConnection.isAlive()) { + race.setVisible(false); + parent.enterTitle(); + //TODO currently this doesn't work correctly - the title screen remains visible after clicking join game + //TODO we should display an error to the user + //TODO also need to "reset" any state (race, connections, etc...). + } + } }.start(); } diff --git a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java index 2f13aae2..82ac39f6 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java @@ -74,6 +74,11 @@ public class StartController extends Controller implements Observer { */ private ServerConnection serverConnection; + /** + * Writes BoatActions to outgoing message queue. + */ + private ControllerClient controllerClient; + /** * The race object which describes the currently occurring race. @@ -85,7 +90,7 @@ public class StartController extends Controller implements Observer { /** * An array of colors used to assign colors to each boat - passed in to the VisualiserRace constructor. */ - List colors = new ArrayList<>(Arrays.asList( + private List colors = new ArrayList<>(Arrays.asList( Color.BLUEVIOLET, Color.BLACK, Color.RED, @@ -130,6 +135,14 @@ public class StartController extends Controller implements Observer { this.visualiserRace = new VisualiserRace(boatDataSource, raceDataSource, regattaDataSource, latestMessages, this.colors); new Thread(this.visualiserRace).start(); + //Ugly. TODO refactor + ObservableList boats = visualiserRace.getBoats(); + for (VisualiserBoat boat : boats) { + if (boat.getSourceID() == serverConnection.getAllocatedSourceID()) { + boat.setClientBoat(true); + } + } + //Initialise the boat table. initialiseBoatTable(this.visualiserRace); @@ -264,7 +277,7 @@ public class StartController extends Controller implements Observer { startWrapper.setVisible(false); start.setVisible(false); - parent.beginRace(visualiserInput, visualiserRace, controllerClient); + parent.beginRace(visualiserRace, controllerClient, serverConnection); } } }.start(); @@ -315,6 +328,7 @@ public class StartController extends Controller implements Observer { LatestMessages latestMessages = new LatestMessages(); this.serverConnection = new ServerConnection(socket, latestMessages); + this.controllerClient = serverConnection.getControllerClient(); //Store a reference to latestMessages so that we can observe it. diff --git a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java b/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java index 8ad14a1b..0579b623 100644 --- a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java +++ b/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java @@ -1,17 +1,11 @@ package visualiser.app; -import network.BinaryMessageDecoder; -import network.Exceptions.InvalidMessageException; import network.Messages.*; import shared.model.RunnableWithFramePeriod; -import java.io.DataInputStream; -import java.io.IOException; -import java.net.Socket; -import java.nio.ByteBuffer; -import java.util.Arrays; import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; -import static network.Utils.ByteConverter.bytesToShort; /** * TCP client which receives packets/messages from a race data source @@ -29,7 +23,6 @@ public class VisualiserInput implements RunnableWithFramePeriod { private long lastHeartbeatSequenceNum = -1; - /** * Incoming messages from server. */ @@ -45,7 +38,8 @@ public class VisualiserInput implements RunnableWithFramePeriod { /** * Constructs a visualiserInput to convert an incoming stream of messages into LatestMessages. - * @param latestMessages Object to place messages in. + * + * @param latestMessages Object to place messages in. * @param incomingMessages The incoming queue of messages. */ public VisualiserInput(LatestMessages latestMessages, BlockingQueue incomingMessages) { @@ -58,6 +52,7 @@ public class VisualiserInput implements RunnableWithFramePeriod { /** * Returns the LatestMessages object, which can be queried for any received race related messages. + * * @return The LatestMessages object. */ public LatestMessages getLatestMessages() { @@ -66,6 +61,7 @@ public class VisualiserInput implements RunnableWithFramePeriod { /** * Calculates the time since last heartbeat, in milliseconds. + * * @return Time since last heartbeat, in milliseconds.. */ private double timeSinceHeartbeat() { @@ -74,22 +70,176 @@ public class VisualiserInput implements RunnableWithFramePeriod { } - - @Override public void run() { - //Handshake. + while (!Thread.interrupted()) { - //Main loop. - // take message - // create command - // place in command queue + AC35Data message = null; + try { + message = incomingMessages.take(); + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.SEVERE, "VisualiserInput was interrupted on thread: " + Thread.currentThread() + " while waiting for messages."); + Thread.currentThread().interrupt(); + return; + } + //TODO refactor below + //Checks which message is being received and does what is needed for that message. + switch (message.getType()) { + //Heartbeat. + case HEARTBEAT: { + HeartBeat heartBeat = (HeartBeat) message; - } + //Check that the heartbeat number is greater than the previous value, and then set the last heartbeat time. + if (heartBeat.getSequenceNumber() > this.lastHeartbeatSequenceNum) { + lastHeartbeatTime = System.currentTimeMillis(); + lastHeartbeatSequenceNum = heartBeat.getSequenceNumber(); + //System.out.println("HeartBeat Message! " + lastHeartbeatSequenceNum); + } + + break; + } + + //RaceStatus. + case RACESTATUS: { + RaceStatus raceStatus = (RaceStatus) message; + + //System.out.println("Race Status Message"); + this.latestMessages.setRaceStatus(raceStatus); + + for (BoatStatus boatStatus : raceStatus.getBoatStatuses()) { + this.latestMessages.setBoatStatus(boatStatus); + } + + break; + } + + //DisplayTextMessage. + case DISPLAYTEXTMESSAGE: { + //System.out.println("Display Text Message"); + //No decoder for this. + + break; + } + + //XMLMessage. + case XMLMESSAGE: { + XMLMessage xmlMessage = (XMLMessage) message; + + //System.out.println("XML Message!"); + + this.latestMessages.setXMLMessage(xmlMessage); + + break; + } + + //RaceStartStatus. + case RACESTARTSTATUS: { + + //System.out.println("Race Start Status Message"); + + break; + } + + //YachtEventCode. + case YACHTEVENTCODE: { + //YachtEventCode yachtEventCode = (YachtEventCode) message; + + //System.out.println("Yacht Event Code!"); + //No decoder for this. + + break; + } + + //YachtActionCode. + case YACHTACTIONCODE: { + //YachtActionCode yachtActionCode = (YachtActionCode) message; + + //System.out.println("Yacht Action Code!"); + // No decoder for this. + + break; + } + //ChatterText. + case CHATTERTEXT: { + //ChatterText chatterText = (ChatterText) message; + + //System.out.println("Chatter Text Message!"); + //No decoder for this. + + break; + } + + //BoatLocation. + case BOATLOCATION: { + BoatLocation boatLocation = (BoatLocation) message; + + //System.out.println("Boat Location!"); + + BoatLocation existingBoatLocation = this.latestMessages.getBoatLocationMap().get(boatLocation.getSourceID()); + if (existingBoatLocation != null) { + //If our boatlocation map already contains a boat location message for this boat, check that the new message is actually for a later timestamp (i.e., newer). + if (boatLocation.getTime() > existingBoatLocation.getTime()) { + //If it is, replace the old message. + this.latestMessages.setBoatLocation(boatLocation); + } + } else { + //If the map _doesn't_ already contain a message for this boat, insert the message. + this.latestMessages.setBoatLocation(boatLocation); + } + + break; + } + + //MarkRounding. + case MARKROUNDING: { + MarkRounding markRounding = (MarkRounding) message; + + //System.out.println("Mark Rounding Message!"); + + MarkRounding existingMarkRounding = this.latestMessages.getMarkRoundingMap().get(markRounding.getSourceID()); + if (existingMarkRounding != null) { + + //If our markRoundingMap already contains a mark rounding message for this boat, check that the new message is actually for a later timestamp (i.e., newer). + if (markRounding.getTime() > existingMarkRounding.getTime()) { + //If it is, replace the old message. + this.latestMessages.setMarkRounding(markRounding); + } + + } else { + //If the map _doesn't_ already contain a message for this boat, insert the message. + this.latestMessages.setMarkRounding(markRounding); + } + + break; + } + + //CourseWinds. + case COURSEWIND: { + + //System.out.println("Course Wind Message!"); + CourseWinds courseWinds = (CourseWinds) message; + + this.latestMessages.setCourseWinds(courseWinds); + + break; + } + + + } + + //Main loop. + // take message + // create command + // place in command queue + + + } + + } } diff --git a/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java b/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java index 3757dc01..b9dde4af 100644 --- a/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java +++ b/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java @@ -1,18 +1,15 @@ package visualiser.gameController; +import mock.exceptions.CommandConstructionException; +import mock.model.MockRace; import mock.model.commandFactory.Command; import mock.model.commandFactory.CommandFactory; import mock.model.commandFactory.CompositeCommand; -import network.BinaryMessageDecoder; -import network.Exceptions.InvalidMessageException; -import network.MessageDecoders.BoatActionDecoder; import network.Messages.AC35Data; import network.Messages.BoatAction; import network.Messages.Enums.MessageType; +import shared.model.RunnableWithFramePeriod; -import java.io.DataInputStream; -import java.io.IOException; -import java.io.InputStream; import java.util.concurrent.BlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; @@ -20,7 +17,7 @@ import java.util.logging.Logger; /** * Service for dispatching key press data to race from client */ -public class ControllerServer implements Runnable { +public class ControllerServer implements RunnableWithFramePeriod { /** @@ -33,6 +30,11 @@ public class ControllerServer implements Runnable { * Collection of commands from client for race to execute. */ private CompositeCommand compositeCommand; + + /** + * The context for each command. + */ + private MockRace raceState; /** * This is the source ID associated with the client. @@ -46,15 +48,18 @@ public class ControllerServer implements Runnable { * @param compositeCommand Commands for the race to execute. * @param inputQueue The queue of messages to read from. * @param clientSourceID The source ID of the client's boat. + * @param raceState The context for each command. */ - public ControllerServer(CompositeCommand compositeCommand, BlockingQueue inputQueue, int clientSourceID) { + public ControllerServer(CompositeCommand compositeCommand, BlockingQueue inputQueue, int clientSourceID, MockRace raceState) { this.compositeCommand = compositeCommand; this.inputQueue = inputQueue; this.clientSourceID = clientSourceID; + this.raceState = raceState; } + /** * Wait for controller key input from client and loop. */ @@ -70,10 +75,17 @@ public class ControllerServer implements Runnable { BoatAction boatAction = (BoatAction) message; + boatAction.setSourceID(clientSourceID); - Command command = CommandFactory.createCommand(boatAction); - compositeCommand.addCommand(command); + try { + Command command = CommandFactory.createCommand(raceState, boatAction); + compositeCommand.addCommand(command); + + } catch (CommandConstructionException e) { + Logger.getGlobal().log(Level.WARNING, "ControllerServer could not create a Command for BoatAction: " + boatAction + ".", e); + + } } @@ -81,6 +93,7 @@ public class ControllerServer implements Runnable { Logger.getGlobal().log(Level.WARNING, "ControllerServer Interrupted while waiting for message on incoming message queue.", e); Thread.currentThread().interrupt(); return; + } } diff --git a/racevisionGame/src/main/java/visualiser/model/Ping.java b/racevisionGame/src/main/java/visualiser/model/Ping.java index b81a3e0c..b31716b5 100644 --- a/racevisionGame/src/main/java/visualiser/model/Ping.java +++ b/racevisionGame/src/main/java/visualiser/model/Ping.java @@ -22,16 +22,14 @@ public class Ping implements Runnable { this.rc = rc; } - public boolean pingPort(){ - //TODO the connection needs to be moved to its own thread, so it doesn't block fx thread. + public boolean pingPort() { InetSocketAddress i = new InetSocketAddress(hostname, port); try (Socket s = new Socket()){ - s.connect(i, 750);//TODO this should be at least a second or two, once moved to its own thread + s.connect(i, 1500); s.shutdownInput(); s.shutdownOutput(); s.close(); rc.statusProperty().set("Ready"); - //System.out.println(String.valueOf(s.isClosed())); return true; } catch (IOException e) { rc.statusProperty().set("Offline"); diff --git a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java index e9b4c31c..16393228 100644 --- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -314,6 +314,10 @@ public class ResizableRaceCanvas extends ResizableCanvas { //The position may be null if we haven't received any BoatLocation messages yet. if (boat.getCurrentPosition() != null) { + if (boat.isClientBoat()) { + drawClientBoat(boat); + } + //Convert position to graph coordinate. GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition()); @@ -343,6 +347,39 @@ public class ResizableRaceCanvas extends ResizableCanvas { } + /** + * Draws extra decorations to show which boat has been assigned to the client. + * @param boat The client's boat. + */ + private void drawClientBoat(VisualiserBoat boat) { + + //Convert position to graph coordinate. + GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition()); + + //The x coordinates of each vertex of the boat. + double[] x = { + pos.getX() - 12, + pos.getX(), + pos.getX() + 12 }; + + //The y coordinates of each vertex of the boat. + double[] y = { + pos.getY() + 24, + pos.getY() - 24, + pos.getY() + 24 }; + + //The above shape is essentially a triangle 24px wide, and 48 long. + + //Draw the boat. + gc.setFill(Color.BLACK); + + gc.save(); + rotate(boat.getBearing().degrees(), pos.getX(), pos.getY()); + gc.fillPolygon(x, y, 3); + gc.restore(); + + } + /** * Draws the wake for a given boat. diff --git a/racevisionGame/src/main/java/visualiser/model/ServerConnection.java b/racevisionGame/src/main/java/visualiser/model/ServerConnection.java index ececdd4c..742ab968 100644 --- a/racevisionGame/src/main/java/visualiser/model/ServerConnection.java +++ b/racevisionGame/src/main/java/visualiser/model/ServerConnection.java @@ -1,12 +1,7 @@ package visualiser.model; -import mock.app.MockOutput; import mock.enums.ConnectionStateEnum; -import mock.exceptions.SourceIDAllocationException; -import mock.model.HeartBeatService; -import mock.model.SourceIdAllocator; -import mock.model.commandFactory.CompositeCommand; import network.Messages.AC35Data; import network.Messages.Enums.JoinAcceptanceEnum; import network.Messages.Enums.MessageType; @@ -19,7 +14,6 @@ import network.StreamRelated.MessageSerialiser; import shared.exceptions.HandshakeException; import visualiser.app.VisualiserInput; import visualiser.gameController.ControllerClient; -import visualiser.gameController.ControllerServer; import java.io.IOException; import java.net.Socket; @@ -38,6 +32,11 @@ public class ServerConnection implements Runnable { */ private Socket socket; + /** + * The source ID that has been allocated to the client. + */ + private int allocatedSourceID = 0; + /** * Latest snapshot of the race, received from the server. @@ -106,6 +105,9 @@ public class ServerConnection implements Runnable { new Thread(messageSerialiser, "ServerConnection()->MessageSerialiser thread " + messageSerialiser).start(); new Thread(messageDeserialiser, "ServerConnection()->MessageDeserialiser thread " + messageDeserialiser).start(); + + this.controllerClient = new ControllerClient(outputQueue); + } @@ -135,25 +137,23 @@ public class ServerConnection implements Runnable { connectionState = ConnectionStateEnum.WAITING_FOR_HANDSHAKE; - sendJoinAcceptanceMessage(RequestToJoinEnum.PARTICIPANT); + sendRequestToJoinMessage(RequestToJoinEnum.PARTICIPANT); JoinAcceptance joinAcceptance = waitForJoinAcceptance(); - int allocatedSourceID = 0; //If we join successfully... if (joinAcceptance.getAcceptanceType() == JoinAcceptanceEnum.JOIN_SUCCESSFUL) { - allocatedSourceID = joinAcceptance.getSourceID(); - //TODO need to do something with the ID - maybe flag the correct visualiser boat as being the client's boat? + this.allocatedSourceID = joinAcceptance.getSourceID(); + - this.controllerClient = new ControllerClient(inputQueue); //new Thread(controllerClient, "ServerConnection.run()->ControllerClient thread " + controllerClient).start(); } - this.visualiserInput = new VisualiserInput(latestMessages, outputQueue); + this.visualiserInput = new VisualiserInput(latestMessages, inputQueue); new Thread(visualiserInput, "ServerConnection.run()->VisualiserInput thread " + visualiserInput).start(); @@ -200,7 +200,7 @@ public class ServerConnection implements Runnable { * @param requestType The type of request to send * @throws HandshakeException Thrown if the thread is interrupted while placing message on the outgoing message queue. */ - private void sendJoinAcceptanceMessage(RequestToJoinEnum requestType) throws HandshakeException { + private void sendRequestToJoinMessage(RequestToJoinEnum requestType) throws HandshakeException { //Send them the source ID. RequestToJoin requestToJoin = new RequestToJoin(requestType); @@ -217,12 +217,27 @@ public class ServerConnection implements Runnable { /** * Determines whether or not this connection is still alive. - * This is based off whether the {@link MessageSerialiser} is still alive. + * This is based off whether the {@link MessageDeserialiser} is still alive. * @return True if it is alive, false otherwise. */ public boolean isAlive() { - return messageSerialiser.isRunning(); + return messageDeserialiser.isRunning(); } + /** + * Returns the controller client, which writes BoatAction messages to the outgoing queue. + * @return The ControllerClient. + */ + public ControllerClient getControllerClient() { + return controllerClient; + } + + /** + * Returns the source ID that has been allocated to the client. + * @return Source ID allocated to the client. 0 if it hasn't been allocated. + */ + public int getAllocatedSourceID() { + return allocatedSourceID; + } } diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java index 57ba0790..c99c85cb 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java @@ -57,6 +57,11 @@ public class VisualiserBoat extends Boat { */ private static final double wakeScale = 5; + /** + * If true then this boat has been allocated to the client. + */ + private boolean isClientBoat = false; + @@ -214,4 +219,21 @@ public class VisualiserBoat extends Boat { return " -"; } } + + + /** + * Returns whether or not this boat has been assigned to the client. + * @return True if this is the client's boat. + */ + public boolean isClientBoat() { + return isClientBoat; + } + + /** + * Sets whether or not this boat has been assigned to the client. + * @param clientBoat True if this is the client's boat. + */ + public void setClientBoat(boolean clientBoat) { + isClientBoat = clientBoat; + } } diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java index 15067515..0dc172cd 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java @@ -347,13 +347,16 @@ public class VisualiserRace extends Race implements Runnable { @Override public void handle(long arg0) { + //Calculate the frame period. long currentFrameTime = System.currentTimeMillis(); long framePeriod = currentFrameTime - lastFrameTime; //Update race status. - updateRaceStatus(latestMessages.getRaceStatus()); + if (latestMessages.getRaceStatus() != null) { + updateRaceStatus(latestMessages.getRaceStatus()); + } //Update racing boats. diff --git a/racevisionGame/src/main/resources/mock/mockXML/boatTest.xml b/racevisionGame/src/main/resources/mock/mockXML/boatTest.xml index b43abe7f..0b2b6a00 100644 --- a/racevisionGame/src/main/resources/mock/mockXML/boatTest.xml +++ b/racevisionGame/src/main/resources/mock/mockXML/boatTest.xml @@ -33,8 +33,24 @@ + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/racevisionGame/src/main/resources/mock/mockXML/boatsSinglePlayer.xml b/racevisionGame/src/main/resources/mock/mockXML/boatsSinglePlayer.xml index 98058f3d..e783821f 100644 --- a/racevisionGame/src/main/resources/mock/mockXML/boatsSinglePlayer.xml +++ b/racevisionGame/src/main/resources/mock/mockXML/boatsSinglePlayer.xml @@ -36,20 +36,5 @@ - - - - - - - - - - - - - - - - \ No newline at end of file + diff --git a/racevisionGame/src/main/resources/mock/mockXML/raceSinglePlayer.xml b/racevisionGame/src/main/resources/mock/mockXML/raceSinglePlayer.xml new file mode 100644 index 00000000..a5d6761f --- /dev/null +++ b/racevisionGame/src/main/resources/mock/mockXML/raceSinglePlayer.xml @@ -0,0 +1,52 @@ + + + 5326 + FLEET + CREATION_TIME + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/racevisionGame/src/main/resources/mock/mockXML/raceTest.xml b/racevisionGame/src/main/resources/mock/mockXML/raceTest.xml index 83e36f85..51199001 100644 --- a/racevisionGame/src/main/resources/mock/mockXML/raceTest.xml +++ b/racevisionGame/src/main/resources/mock/mockXML/raceTest.xml @@ -5,7 +5,12 @@ CREATION_TIME + + + + + @@ -49,4 +54,4 @@ - \ No newline at end of file + diff --git a/racevisionGame/src/main/resources/visualiser/scenes/connect.fxml b/racevisionGame/src/main/resources/visualiser/scenes/connect.fxml index f620bfeb..4308a47f 100644 --- a/racevisionGame/src/main/resources/visualiser/scenes/connect.fxml +++ b/racevisionGame/src/main/resources/visualiser/scenes/connect.fxml @@ -1,13 +1,18 @@ - - - - - + + + + + + + + + + - + diff --git a/racevisionGame/src/main/resources/visualiser/scenes/lobby.fxml b/racevisionGame/src/main/resources/visualiser/scenes/lobby.fxml index 13159bec..2f84063e 100644 --- a/racevisionGame/src/main/resources/visualiser/scenes/lobby.fxml +++ b/racevisionGame/src/main/resources/visualiser/scenes/lobby.fxml @@ -1,13 +1,18 @@ - - - - - + + + + + + + + + + - + @@ -35,31 +40,52 @@ - + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - + diff --git a/racevisionGame/src/test/java/mock/dataInput/PolarParserTest.java b/racevisionGame/src/test/java/mock/dataInput/PolarParserTest.java index 78d3364f..b051ba79 100644 --- a/racevisionGame/src/test/java/mock/dataInput/PolarParserTest.java +++ b/racevisionGame/src/test/java/mock/dataInput/PolarParserTest.java @@ -14,6 +14,13 @@ import static org.junit.Assert.fail; public class PolarParserTest { + + public static Polars createPolars() { + Polars polars = PolarParser.parse("mock/polars/acc_polars.csv"); + return polars; + } + + /** * Tests if we can parse a valid polar data file (stored in a string), and create a polar table. */ diff --git a/racevisionGame/src/test/java/mock/model/ConstantWindGeneratorTest.java b/racevisionGame/src/test/java/mock/model/ConstantWindGeneratorTest.java new file mode 100644 index 00000000..6f67dc30 --- /dev/null +++ b/racevisionGame/src/test/java/mock/model/ConstantWindGeneratorTest.java @@ -0,0 +1,72 @@ +package mock.model; + +import org.junit.Before; +import org.junit.Test; +import shared.model.Bearing; +import shared.model.Wind; + +import static org.junit.Assert.*; + + +/** + * Tests the {@link ConstantWindGenerator}. + */ +public class ConstantWindGeneratorTest { + + WindGenerator windGenerator; + Bearing windBearing; + double windSpeedKnots; + + @Before + public void setUp() throws Exception { + + windBearing = Bearing.fromDegrees(78.5); + windSpeedKnots = 18.54; + + windGenerator = new ConstantWindGenerator(windBearing, windSpeedKnots); + + } + + + /** + * Tests if the {@link WindGenerator#generateBaselineWind()} function works. + */ + @Test + public void generateBaselineTest() { + + int repetitions = 100; + + for (int i = 0; i < repetitions; i++) { + + Wind wind = windGenerator.generateBaselineWind(); + + assertEquals(windBearing.degrees(), wind.getWindDirection().degrees(), 0.01); + assertEquals(windSpeedKnots, wind.getWindSpeed(), 0.01); + + } + + } + + + /** + * Tests if the {@link WindGenerator#generateNextWind(Wind)} ()} function works. + */ + @Test + public void generateNextWindTest() { + + int repetitions = 100; + + Wind wind = windGenerator.generateBaselineWind(); + + for (int i = 0; i < repetitions; i++) { + + wind = windGenerator.generateNextWind(wind); + + assertEquals(windBearing.degrees(), wind.getWindDirection().degrees(), 0.01); + assertEquals(windSpeedKnots, wind.getWindSpeed(), 0.01); + + } + + } + +} diff --git a/racevisionGame/src/test/java/mock/model/MockRaceTest.java b/racevisionGame/src/test/java/mock/model/MockRaceTest.java index cf530665..3c01b321 100644 --- a/racevisionGame/src/test/java/mock/model/MockRaceTest.java +++ b/racevisionGame/src/test/java/mock/model/MockRaceTest.java @@ -1,7 +1,44 @@ package mock.model; +import mock.dataInput.PolarParserTest; +import network.Messages.LatestMessages; +import shared.dataInput.*; +import shared.exceptions.InvalidBoatDataException; +import shared.exceptions.InvalidRaceDataException; +import shared.exceptions.InvalidRegattaDataException; +import shared.model.Bearing; +import shared.model.Constants; + import static org.junit.Assert.*; public class MockRaceTest { //TODO + + + /** + * Creates a MockRace for use in testing. + * Has a constant wind generator. + * @return MockRace for testing. + * @throws InvalidBoatDataException + * @throws InvalidRaceDataException + * @throws InvalidRegattaDataException + */ + public static MockRace createMockRace() throws InvalidBoatDataException, InvalidRaceDataException, InvalidRegattaDataException { + + BoatDataSource boatDataSource = BoatXMLReaderTest.createBoatDataSource(); + RaceDataSource raceDataSource = RaceXMLReaderTest.createRaceDataSource(); + RegattaDataSource regattaDataSource = RegattaXMLReaderTest.createRegattaDataSource(); + + LatestMessages latestMessages = new LatestMessages(); + + Polars polars = PolarParserTest.createPolars(); + + WindGenerator windGenerator = new ConstantWindGenerator(Bearing.fromDegrees(230), 10); + + MockRace mockRace = new MockRace(boatDataSource, raceDataSource, regattaDataSource, latestMessages, polars, Constants.RaceTimeScale, windGenerator); + + + return mockRace; + + } } diff --git a/racevisionGame/src/test/java/mock/model/WindGeneratorTest.java b/racevisionGame/src/test/java/mock/model/RandomWindGeneratorTest.java similarity index 86% rename from racevisionGame/src/test/java/mock/model/WindGeneratorTest.java rename to racevisionGame/src/test/java/mock/model/RandomWindGeneratorTest.java index 74ad7421..76eed977 100644 --- a/racevisionGame/src/test/java/mock/model/WindGeneratorTest.java +++ b/racevisionGame/src/test/java/mock/model/RandomWindGeneratorTest.java @@ -8,10 +8,10 @@ import shared.model.Wind; import static org.junit.Assert.*; -public class WindGeneratorTest { +public class RandomWindGeneratorTest { - private WindGenerator windGenerator; + private RandomWindGenerator randomWindGenerator; private Bearing windBaselineBearing; private Bearing windBearingLowerBound; @@ -36,7 +36,7 @@ public class WindGeneratorTest { this.windSpeedLowerBound = 7; this.windSpeedUpperBound = 20; - this.windGenerator = new WindGenerator( + this.randomWindGenerator = new RandomWindGenerator( windBaselineBearing, windBearingLowerBound, windBearingUpperBound, @@ -55,7 +55,7 @@ public class WindGeneratorTest { @Test public void generateBaselineWindTest() { - Wind wind = windGenerator.generateBaselineWind(); + Wind wind = randomWindGenerator.generateBaselineWind(); assertEquals(windBaselineSpeed, wind.getWindSpeed(), speedKnotsEpsilon); assertEquals(windBaselineBearing.degrees(), wind.getWindDirection().degrees(), bearingDegreeEpsilon); @@ -72,7 +72,7 @@ public class WindGeneratorTest { for (int i = 0; i < randomWindCount; i++) { - Wind wind = windGenerator.generateRandomWind(); + Wind wind = randomWindGenerator.generateRandomWind(); assertTrue(wind.getWindSpeed() >= windSpeedLowerBound); assertTrue(wind.getWindSpeed() <= windSpeedUpperBound); @@ -91,13 +91,13 @@ public class WindGeneratorTest { @Test public void generateNextWindTest() { - Wind wind = windGenerator.generateBaselineWind(); + Wind wind = randomWindGenerator.generateBaselineWind(); int randomWindCount = 1000; for (int i = 0; i < randomWindCount; i++) { - wind = windGenerator.generateNextWind(wind); + wind = randomWindGenerator.generateNextWind(wind); assertTrue(wind.getWindSpeed() >= windSpeedLowerBound); assertTrue(wind.getWindSpeed() <= windSpeedUpperBound); diff --git a/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java b/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java index c3d0df04..3a48c096 100644 --- a/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java +++ b/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java @@ -1,31 +1,72 @@ package mock.model.commandFactory; +import mock.exceptions.CommandConstructionException; +import mock.model.MockBoat; import mock.model.MockRace; +import mock.model.MockRaceTest; +import network.Messages.BoatAction; import network.Messages.Enums.BoatActionEnum; import org.junit.Before; import org.junit.Test; +import org.mockito.Mock; +import shared.exceptions.InvalidBoatDataException; +import shared.exceptions.InvalidRaceDataException; +import shared.exceptions.InvalidRegattaDataException; +import shared.model.Bearing; import shared.model.Boat; import shared.model.Race; import visualiser.model.VisualiserRace; +import static org.mockito.Mockito.when; import static org.testng.Assert.*; +import static org.mockito.Mockito.mock; /** * Created by connortaylorbrown on 4/08/17. */ public class WindCommandTest { - private Race race; - private Boat boat; + private MockRace race; + private MockBoat boat; private Command upwind; private Command downwind; + private double initial; + + private double offset = 3.0; @Before - public void setUp() { - boat = new Boat(0, "Bob", "NZ"); + public void setUp() throws CommandConstructionException, InvalidBoatDataException, InvalidRegattaDataException, InvalidRaceDataException { + race = MockRaceTest.createMockRace(); + + boat = race.getBoats().get(0); + + + //when(race.getWindDirection()).thenReturn(Bearing.fromDegrees(0.0)); + boat.setBearing(Bearing.fromDegrees(45.0)); + + BoatAction upwindAction = new BoatAction(BoatActionEnum.UPWIND); + upwindAction.setSourceID(boat.getSourceID()); + + BoatAction downwindAction = new BoatAction(BoatActionEnum.DOWNWIND); + downwindAction.setSourceID(boat.getSourceID()); + + upwind = CommandFactory.createCommand(race, upwindAction); + downwind = CommandFactory.createCommand(race, downwindAction); + + initial = boat.getBearing().degrees(); } + /** + * Ensure the difference between initial and final angle is 3 degrees + */ @Test public void upwindCommandDecreasesAngle() { + upwind.execute(); + assertEquals(initial - boat.getBearing().degrees(), -offset, 1e-5); + } + @Test + public void downwindCommandIncreasesAngle() { + downwind.execute(); + assertEquals(initial - boat.getBearing().degrees(), offset, 1e-5); } -} \ No newline at end of file +} diff --git a/racevisionGame/src/test/java/shared/dataInput/BoatXMLReaderTest.java b/racevisionGame/src/test/java/shared/dataInput/BoatXMLReaderTest.java index 19370be0..60ec2be9 100644 --- a/racevisionGame/src/test/java/shared/dataInput/BoatXMLReaderTest.java +++ b/racevisionGame/src/test/java/shared/dataInput/BoatXMLReaderTest.java @@ -31,31 +31,40 @@ public class BoatXMLReaderTest { private List marks; - @Before - public void setUp() { + public static BoatDataSource createBoatDataSource() throws InvalidBoatDataException { String boatXMLString = null; try { boatXMLString = XMLReader.readXMLFileToString("mock/mockXML/boatTest.xml", StandardCharsets.UTF_8); - } catch (Exception e) { - e.printStackTrace(); - fail("could not read boat xml file into a string"); + } catch (TransformerException | XMLReaderException e) { + throw new InvalidBoatDataException("Could not read boat XML file into a string.", e); + } try { - boatData = new BoatXMLReader(boatXMLString, XMLFileType.Contents); + BoatDataSource boatData = new BoatXMLReader(boatXMLString, XMLFileType.Contents); + return boatData; - boats = new ArrayList<>(boatData.getBoats().values()); - marks = new ArrayList<>(boatData.getMarkerBoats().values()); - } catch (XMLReaderException | InvalidBoatDataException e) { - e.printStackTrace(); - assertTrue(false); + } catch (XMLReaderException e) { + throw new InvalidBoatDataException("Could not parse boat XML file.", e); } + + + } + + @Before + public void setUp() throws InvalidBoatDataException { + + boatData = createBoatDataSource(); + + boats = new ArrayList<>(boatData.getBoats().values()); + marks = new ArrayList<>(boatData.getMarkerBoats().values()); + } @Test diff --git a/racevisionGame/src/test/java/shared/dataInput/RaceXMLReaderTest.java b/racevisionGame/src/test/java/shared/dataInput/RaceXMLReaderTest.java index 0539a990..6080919d 100644 --- a/racevisionGame/src/test/java/shared/dataInput/RaceXMLReaderTest.java +++ b/racevisionGame/src/test/java/shared/dataInput/RaceXMLReaderTest.java @@ -1,5 +1,39 @@ package shared.dataInput; +import shared.enums.XMLFileType; +import shared.exceptions.InvalidRaceDataException; +import shared.exceptions.XMLReaderException; + +import javax.xml.transform.TransformerException; +import java.nio.charset.StandardCharsets; + public class RaceXMLReaderTest { -//TODO + + + + public static RaceDataSource createRaceDataSource() throws InvalidRaceDataException { + + String raceXMLString = null; + + try { + raceXMLString = XMLReader.readXMLFileToString("mock/mockXML/raceTest.xml", StandardCharsets.UTF_8); + + } catch (TransformerException | XMLReaderException e) { + throw new InvalidRaceDataException("Could not read race XML file into a string.", e); + + } + + + try { + + RaceXMLReader raceData = new RaceXMLReader(raceXMLString, XMLFileType.Contents); + return raceData; + + + } catch (XMLReaderException e) { + throw new InvalidRaceDataException("Could not parse race XML file.", e); + } + + + } } diff --git a/racevisionGame/src/test/java/shared/dataInput/RegattaXMLReaderTest.java b/racevisionGame/src/test/java/shared/dataInput/RegattaXMLReaderTest.java index ad9bcd95..2f043c0d 100644 --- a/racevisionGame/src/test/java/shared/dataInput/RegattaXMLReaderTest.java +++ b/racevisionGame/src/test/java/shared/dataInput/RegattaXMLReaderTest.java @@ -3,6 +3,11 @@ package shared.dataInput; import org.junit.Before; import org.junit.Test; import shared.enums.XMLFileType; +import shared.exceptions.InvalidRegattaDataException; +import shared.exceptions.XMLReaderException; + +import javax.xml.transform.TransformerException; +import java.nio.charset.StandardCharsets; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -14,6 +19,34 @@ import static org.junit.Assert.fail; public class RegattaXMLReaderTest { private RegattaXMLReader regatta; + + public static RegattaDataSource createRegattaDataSource() throws InvalidRegattaDataException { + + String regattaXMLString = null; + + try { + regattaXMLString = XMLReader.readXMLFileToString("mock/mockXML/regattaTest.xml", StandardCharsets.UTF_8); + + } catch (TransformerException | XMLReaderException e) { + throw new InvalidRegattaDataException("Could not read regatta XML file into a string.", e); + + } + + + try { + + RegattaDataSource raceData = new RegattaXMLReader(regattaXMLString, XMLFileType.Contents); + return raceData; + + + } catch (XMLReaderException e) { + throw new InvalidRegattaDataException("Could not parse regatta XML file.", e); + } + + + } + + @Before public void findFile() { try {