diff --git a/.mailmap b/.mailmap index cad3a3a8..caf8a624 100644 --- a/.mailmap +++ b/.mailmap @@ -23,5 +23,5 @@ Haoming Yin Peter Galloway Peter Zhi You Tan zyt10 Zhi You Tan Ryan Tan -Alistair McIntyre alistairjmcintyre +Alistair McIntyre Calum cir27 \ No newline at end of file diff --git a/pom.xml b/pom.xml index b329e067..fca06536 100644 --- a/pom.xml +++ b/pom.xml @@ -69,24 +69,9 @@ commons-cli 1.4 - - - org.testfx - testfx-core - 4.0.1-alpha - test - - - - org.testfx - testfx-junit - 4.0.6-alpha - test - - diff --git a/src/main/java/seng302/gameServer/GameState.java b/src/main/java/seng302/gameServer/GameState.java index 52280317..9a54c7d1 100644 --- a/src/main/java/seng302/gameServer/GameState.java +++ b/src/main/java/seng302/gameServer/GameState.java @@ -5,8 +5,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import seng302.gameServer.server.messages.BoatAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import seng302.model.Player; import seng302.model.Yacht; +import seng302.model.mark.MarkOrder; /** * A Static class to hold information about the current state of the game (model) @@ -14,7 +17,10 @@ import seng302.model.Yacht; */ public class GameState implements Runnable { + private Logger logger = LoggerFactory.getLogger(MarkOrder.class); + private static Integer STATE_UPDATES_PER_SECOND = 60; + public static Integer MAX_PLAYERS = 8; private static Long previousUpdateTime; public static Double windDirection; @@ -25,6 +31,7 @@ public class GameState implements Runnable { private static Map yachts; private static Boolean isRaceStarted; private static GameStages currentStage; + private static MarkOrder markOrder; private static long startTime; private static Map playerStringMap = new HashMap<>(); @@ -51,8 +58,10 @@ public class GameState implements Runnable { isRaceStarted = false; //set this when game stage changes to prerace previousUpdateTime = System.currentTimeMillis(); + yachts = new HashMap<>(); + markOrder = new MarkOrder(); //This could be instantiated at some point with a select map? - new Thread(this).start(); + new Thread(this).start(); //Run the auto updates on the game state } public static String getHostIpAddress() { @@ -99,6 +108,10 @@ public class GameState implements Runnable { GameState.currentStage = currentStage; } + public static MarkOrder getMarkOrder() { + return markOrder; + } + public static long getStartTime(){ return startTime; } diff --git a/src/main/java/seng302/gameServer/MainServerThread.java b/src/main/java/seng302/gameServer/MainServerThread.java index 1539730e..8c563258 100644 --- a/src/main/java/seng302/gameServer/MainServerThread.java +++ b/src/main/java/seng302/gameServer/MainServerThread.java @@ -44,6 +44,7 @@ public class MainServerThread extends Observable implements Runnable, ClientConn HeartbeatThread heartbeatThread; serverListenThread = new ServerListenThread(serverSocket, this); + heartbeatThread = new HeartbeatThread(this); heartbeatThread.start(); @@ -102,9 +103,11 @@ public class MainServerThread extends Observable implements Runnable, ClientConn public void clientConnected(ServerToClientThread serverToClientThread) { serverLog("Player Connected From " + serverToClientThread.getThread().getName(), 0); serverToClientThreads.add(serverToClientThread); - this.addObserver(serverToClientThread); - setChanged(); - notifyObservers(); + serverToClientThread.addConnectionListener(() -> { + for (ServerToClientThread thread : serverToClientThreads) { + thread.sendSetupMessages(); + } + }); } /** @@ -121,11 +124,15 @@ public class MainServerThread extends Observable implements Runnable, ClientConn serverLog("Player " + player.getYacht().getSourceId() + "'s socket disconnected", 0); GameState.removeYacht(player.getYacht().getSourceId()); GameState.removePlayer(player); + ServerToClientThread closedConnection = null; for (ServerToClientThread serverToClientThread : serverToClientThreads) { if (serverToClientThread.getSocket() == player.getSocket()) { - this.deleteObserver(serverToClientThread); + closedConnection = serverToClientThread; + } else { + serverToClientThread.sendSetupMessages(); } } + serverToClientThreads.remove(closedConnection); setChanged(); notifyObservers(); } diff --git a/src/main/java/seng302/gameServer/ServerPacketParser.java b/src/main/java/seng302/gameServer/ServerPacketParser.java index b6555f28..84f8fd17 100644 --- a/src/main/java/seng302/gameServer/ServerPacketParser.java +++ b/src/main/java/seng302/gameServer/ServerPacketParser.java @@ -1,37 +1,26 @@ package seng302.gameServer; import java.util.Arrays; + +import seng302.gameServer.server.messages.ClientType; +import seng302.gameServer.server.messages.Message; import seng302.model.stream.packets.StreamPacket; import seng302.gameServer.server.messages.BoatAction; public class ServerPacketParser { - public static BoatAction extractBoatAction(StreamPacket packet) { byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; - long actionTypeValue = bytesToLong(Arrays.copyOfRange(payload, 0, 1)); + long actionTypeValue = Message.bytesToLong(Arrays.copyOfRange(payload, 0, 1)); return BoatAction.getType((int) actionTypeValue); } - /** - * takes an array of up to 7 bytes and returns a positive - * long constructed from the input bytes - * - * @return a positive long if there is less than 7 bytes -1 otherwise - */ - private static long bytesToLong(byte[] bytes) { - long partialLong = 0; - int index = 0; - for (byte b : bytes) { - if (index > 6) { - return -1; - } - partialLong = partialLong | (b & 0xFFL) << (index * 8); - index++; - } - return partialLong; + public static ClientType extractClientType(StreamPacket packet){ + byte[] payload = packet.getPayload(); + long value = Message.bytesToLong(Arrays.copyOfRange(payload, 0, 1)); + return ClientType.getClientType((int) value); } } diff --git a/src/main/java/seng302/gameServer/ServerToClientThread.java b/src/main/java/seng302/gameServer/ServerToClientThread.java index 35302133..a3f87e2b 100644 --- a/src/main/java/seng302/gameServer/ServerToClientThread.java +++ b/src/main/java/seng302/gameServer/ServerToClientThread.java @@ -12,12 +12,23 @@ import java.net.SocketException; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; -import java.util.Observable; -import java.util.Observer; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; import java.util.zip.CRC32; import java.util.zip.Checksum; +import seng302.gameServer.server.messages.BoatAction; +import seng302.gameServer.server.messages.BoatLocationMessage; +import seng302.gameServer.server.messages.BoatStatus; +import seng302.gameServer.server.messages.BoatSubMessage; +import seng302.gameServer.server.messages.ClientType; +import seng302.gameServer.server.messages.Message; +import seng302.gameServer.server.messages.RaceStatus; +import seng302.gameServer.server.messages.RaceStatusMessage; +import seng302.gameServer.server.messages.RaceType; +import seng302.gameServer.server.messages.RegistrationResponseMessage; +import seng302.gameServer.server.messages.RegistrationResponseStatus; +import seng302.gameServer.server.messages.XMLMessage; +import seng302.gameServer.server.messages.XMLMessageSubType; import seng302.model.Player; import seng302.model.Yacht; import seng302.model.stream.packets.PacketType; @@ -25,23 +36,21 @@ import seng302.model.stream.packets.StreamPacket; import seng302.model.stream.xml.generator.Race; import seng302.model.stream.xml.generator.Regatta; import seng302.utilities.XMLGenerator; -import seng302.gameServer.server.messages.BoatAction; -import seng302.gameServer.server.messages.BoatLocationMessage; -import seng302.gameServer.server.messages.BoatStatus; -import seng302.gameServer.server.messages.BoatSubMessage; -import seng302.gameServer.server.messages.Message; -import seng302.gameServer.server.messages.RaceStatus; -import seng302.gameServer.server.messages.RaceStatusMessage; -import seng302.gameServer.server.messages.RaceType; -import seng302.gameServer.server.messages.XMLMessage; -import seng302.gameServer.server.messages.XMLMessageSubType; /** * A class describing a single connection to a Client for the purposes of sending and receiving on * its own thread. All server threads created and owned by the server thread handler which can * trigger client updates on its threads Created by wmu16 on 13/07/17. */ -public class ServerToClientThread implements Runnable, Observer { +public class ServerToClientThread implements Runnable { + + /** + * Called to notify listeners when this thread receives a connection correctly. + */ + @FunctionalInterface + interface ConnectionListener { + void notifyConnection (); + } private static final Integer LOG_LEVEL = 1; private static final Integer MAX_ID_ATTEMPTS = 10; @@ -54,65 +63,64 @@ public class ServerToClientThread implements Runnable, Observer { private ByteArrayOutputStream crcBuffer; - private Boolean userIdentified = false; - private Boolean connected = true; - private Boolean updateClient = true; // private Boolean initialisedRace = true; private Integer seqNo; private Integer sourceId; + private ClientType clientType; + private Boolean isRegistered = false; + private XMLGenerator xml; + private List connectionListeners = new ArrayList<>(); + public ServerToClientThread(Socket socket) { this.socket = socket; + seqNo = 0; + + try{ + is = socket.getInputStream(); + os = socket.getOutputStream(); + } catch (IOException e) { + return; + } + + thread = new Thread(this); + thread.start(); + } + + private void setUpPlayer(){ BufferedReader fn; String fName = ""; BufferedReader ln; String lName = ""; - try { - is = socket.getInputStream(); - os = socket.getOutputStream(); - fn = new BufferedReader( - new InputStreamReader( - ServerToClientThread.class.getResourceAsStream( - "/server_config/CSV_Database_of_First_Names.csv" - ) - ) - ); - List all = fn.lines().collect(Collectors.toList()); - fName = all.get(ThreadLocalRandom.current().nextInt(0, all.size())); - ln = new BufferedReader( - new InputStreamReader( - ServerToClientThread.class.getResourceAsStream( - "/server_config/CSV_Database_of_Last_Names.csv" - ) - ) - ); - all = ln.lines().collect(Collectors.toList()); - lName = all.get(ThreadLocalRandom.current().nextInt(0, all.size())); - } catch (IOException e) { - serverLog("IO error in server thread upon grabbing streams", 1); - } - //Attempt threeway handshake with connection - sourceId = GameState.getUniquePlayerID(); - if (threeWayHandshake(sourceId)) { - serverLog("Successful handshake. Client allocated id: " + sourceId, 0); - Yacht yacht = new Yacht( - "Yacht", sourceId, sourceId.toString(), fName, fName + " " + lName, "NZ" - ); -// Yacht yacht = new Yacht("Kappa", "Kap", new GeoPoint(57.6708220, 11.8321340), 90.0); - GameState.addYacht(sourceId, yacht); - GameState.addPlayer(new Player(socket, yacht)); - } else { - serverLog("Unsuccessful handshake. Connection rejected", 1); - closeSocket(); - return; - } - seqNo = 0; - thread = new Thread(this); - thread.start(); + fn = new BufferedReader( + new InputStreamReader( + ServerToClientThread.class.getResourceAsStream( + "/server_config/CSV_Database_of_First_Names.csv" + ) + ) + ); + List all = fn.lines().collect(Collectors.toList()); + fName = all.get(ThreadLocalRandom.current().nextInt(0, all.size())); + ln = new BufferedReader( + new InputStreamReader( + ServerToClientThread.class.getResourceAsStream( + "/server_config/CSV_Database_of_Last_Names.csv" + ) + ) + ); + all = ln.lines().collect(Collectors.toList()); + lName = all.get(ThreadLocalRandom.current().nextInt(0, all.size())); + + + Yacht yacht = new Yacht( + "Yacht", sourceId, sourceId.toString(), fName, fName + " " + lName, "NZ" + ); + GameState.addYacht(sourceId, yacht); + GameState.addPlayer(new Player(socket, yacht)); } static void serverLog(String message, int logLevel) { @@ -122,9 +130,33 @@ public class ServerToClientThread implements Runnable, Observer { } } - @Override - public void update(Observable o, Object arg) { - sendSetupMessages(); + private void completeRegistration(ClientType clientType) throws IOException { + // Fail if not a player + if (!clientType.equals(ClientType.PLAYER)){ + RegistrationResponseMessage responseMessage = new RegistrationResponseMessage(0, RegistrationResponseStatus.FAILURE_GENERAL); + os.write(responseMessage.getBuffer()); + return; + } + + if (GameState.getPlayers().size() >= GameState.MAX_PLAYERS){ + RegistrationResponseMessage responseMessage = new RegistrationResponseMessage(0, RegistrationResponseStatus.FAILURE_FULL); + os.write(responseMessage.getBuffer()); + return; + } + + Integer sourceId = GameState.getUniquePlayerID(); + RegistrationResponseMessage responseMessage = new RegistrationResponseMessage(sourceId, RegistrationResponseStatus.SUCCESS_PLAYING); + + this.clientType = clientType; + this.sourceId = sourceId; + isRegistered = true; + os.write(responseMessage.getBuffer()); + + setUpPlayer(); + + for (ConnectionListener listener : connectionListeners) { + listener.notifyConnection(); + } } public void run() { @@ -132,23 +164,10 @@ public class ServerToClientThread implements Runnable, Observer { int sync2; // TODO: 14/07/17 wmu16 - Work out how to fix this while loop + while (socket.isConnected()) { try { - //Perform a write if it is time to as delegated by the MainServerThread - if (updateClient) { - // TODO: 13/07/17 wmu16 - Write out game state - some function that would write all appropriate messages to this output stream -// ChatterMessage chatterMessage = new ChatterMessage(4, 14, "Hello, it's me"); -// sendMessage(chatterMessage); -// try { -// GameState.outputState(os); -// } catch (IOException e) { -// System.out.println("IO error in server thread upon writing to output stream"); -// } -// sendBoatLocationPackets(); - updateClient = false; - } - crcBuffer = new ByteArrayOutputStream(); sync1 = readByte(); sync2 = readByte(); @@ -169,10 +188,17 @@ public class ServerToClientThread implements Runnable, Observer { switch (PacketType.assignPacketType(type, payload)) { case BOAT_ACTION: BoatAction actionType = ServerPacketParser - .extractBoatAction( - new StreamPacket(type, payloadLength, timeStamp, payload)); + .extractBoatAction( + new StreamPacket(type, payloadLength, timeStamp, payload)); GameState.updateBoat(sourceId, actionType); break; + + case RACE_REGISTRATION_REQUEST: + ClientType requestedType = ServerPacketParser.extractClientType( + new StreamPacket(type, payloadLength, timeStamp, payload)); + + completeRegistration(requestedType); + break; } } else { serverLog("Packet has been dropped", 1); @@ -187,7 +213,7 @@ public class ServerToClientThread implements Runnable, Observer { } } - private void sendSetupMessages() { + public void sendSetupMessages() { xml = new XMLGenerator(); Race race = new Race(); @@ -215,40 +241,6 @@ public class ServerToClientThread implements Runnable, Observer { public void updateClient() { sendBoatLocationPackets(); - updateClient = true; - } - - - /** - * Tries to confirm the connection just accepted. - * Sends ID, expects that ID echoed for confirmation, - * if so, sends a confirmation packet back to that connection - * Creates a player instance with that ID and this thread and adds it to the GameState - * If not, close the socket and end the threads execution - * - * @param id the id to try and assign to the connection - * @return A boolean indicating if it was a successful handshake - */ - private Boolean threeWayHandshake(Integer id) { - Integer confirmationID = null; - Integer identificationAttempt = 0; - while (!userIdentified) { - try { - os.write(id); //Send out new ID looking for echo - confirmationID = is.read(); - } catch (IOException e) { - serverLog("Three way handshake failed", 1); - } - - if (id.equals(confirmationID)) { //ID is echoed back. Connection is a client - return true; - } else if (identificationAttempt > MAX_ID_ATTEMPTS) { //No response. not a client. tidy up and go home. - return false; - } - identificationAttempt++; - } - - return true; } private void closeSocket() { @@ -259,7 +251,6 @@ public class ServerToClientThread implements Runnable, Observer { } } - private int readByte() throws Exception { int currentByte = -1; try { @@ -310,7 +301,6 @@ public class ServerToClientThread implements Runnable, Observer { private void sendBoatLocationPackets() { ArrayList yachts = new ArrayList<>(GameState.getYachts().values()); for (Yacht yacht : yachts) { -// System.out.println("[SERVER] Lat: " + yacht.getLocation().getLat() + " Lon: " + yacht.getLocation().getLng()); BoatLocationMessage boatLocationMessage = new BoatLocationMessage( yacht.getSourceId(), @@ -331,7 +321,6 @@ public class ServerToClientThread implements Runnable, Observer { public void sendRaceStatusMessage() { // variables taken from GameServerThread - List boatSubMessages = new ArrayList<>(); BoatStatus boatStatus; RaceStatus raceStatus; @@ -366,4 +355,12 @@ public class ServerToClientThread implements Runnable, Observer { public Socket getSocket() { return socket; } + + public void addConnectionListener(ConnectionListener listener) { + connectionListeners.add(listener); + } + + public void removeConnectionListener(ConnectionListener listener) { + connectionListeners.remove(listener); + } } diff --git a/src/main/java/seng302/gameServer/server/messages/ClientType.java b/src/main/java/seng302/gameServer/server/messages/ClientType.java new file mode 100644 index 00000000..b96ca5c7 --- /dev/null +++ b/src/main/java/seng302/gameServer/server/messages/ClientType.java @@ -0,0 +1,33 @@ +package seng302.gameServer.server.messages; + +public enum ClientType { + SPECTATOR(0x00), + PLAYER(0x01), + CONTROL_TUTORIAL(0x02), + GHOST_MODE(0x03); + + private int type; + + ClientType(int type){ + this.type = type; + } + + public int getCode(){ + return type; + } + + public static ClientType getClientType(int typeCode){ + switch (typeCode){ + case 0x00: + return SPECTATOR; + case 0x01: + return PLAYER; + case 0x02: + return CONTROL_TUTORIAL; + case 0x03: + return GHOST_MODE; + default: + return PLAYER; + } + } +} diff --git a/src/main/java/seng302/gameServer/server/messages/MessageType.java b/src/main/java/seng302/gameServer/server/messages/MessageType.java index 1d25e61d..4552d781 100644 --- a/src/main/java/seng302/gameServer/server/messages/MessageType.java +++ b/src/main/java/seng302/gameServer/server/messages/MessageType.java @@ -17,7 +17,9 @@ public enum MessageType { MARK_ROUNDING(38), COURSE_WIND(44), AVERAGE_WIND(47), - BOAT_ACTION(100); + BOAT_ACTION(100), + REGISTRATION_REQUEST(101), + REGISTRATION_RESPONSE(102); private int code; @@ -32,4 +34,6 @@ public enum MessageType { int getCode(){ return this.code; } + + } diff --git a/src/main/java/seng302/gameServer/server/messages/RegistrationRequestMessage.java b/src/main/java/seng302/gameServer/server/messages/RegistrationRequestMessage.java new file mode 100644 index 00000000..59757a18 --- /dev/null +++ b/src/main/java/seng302/gameServer/server/messages/RegistrationRequestMessage.java @@ -0,0 +1,22 @@ +package seng302.gameServer.server.messages; + + +public class RegistrationRequestMessage extends Message { + private static int MESSAGE_LENGTH = 2; + + public RegistrationRequestMessage(ClientType type){ + setHeader(new Header(MessageType.REGISTRATION_REQUEST, 1, (short) getSize())); + + allocateBuffer(); + writeHeaderToBuffer(); + + putInt(type.getCode(), 2); + + writeCRC(); + } + + @Override + public int getSize() { + return MESSAGE_LENGTH; + } +} diff --git a/src/main/java/seng302/gameServer/server/messages/RegistrationResponseMessage.java b/src/main/java/seng302/gameServer/server/messages/RegistrationResponseMessage.java new file mode 100644 index 00000000..e2174da4 --- /dev/null +++ b/src/main/java/seng302/gameServer/server/messages/RegistrationResponseMessage.java @@ -0,0 +1,20 @@ +package seng302.gameServer.server.messages; + +public class RegistrationResponseMessage extends Message{ + + public RegistrationResponseMessage(int clientSourceID, RegistrationResponseStatus status){ + setHeader(new Header(MessageType.REGISTRATION_RESPONSE, 1, (short) getSize())); + allocateBuffer(); + writeHeaderToBuffer(); + + putInt(clientSourceID, 4); + putInt(status.getCode(), 1); + + writeCRC(); + } + + @Override + public int getSize() { + return 5; + } +} diff --git a/src/main/java/seng302/gameServer/server/messages/RegistrationResponseStatus.java b/src/main/java/seng302/gameServer/server/messages/RegistrationResponseStatus.java new file mode 100644 index 00000000..2ae47a92 --- /dev/null +++ b/src/main/java/seng302/gameServer/server/messages/RegistrationResponseStatus.java @@ -0,0 +1,44 @@ +package seng302.gameServer.server.messages; + +public enum RegistrationResponseStatus { + SUCCESS_SPECTATING(0x00), + SUCCESS_PLAYING(0x01), + SUCCESS_TUTORIAL(0x02), + SUCCESS_GHOSTING(0x03), + + FAILURE_GENERAL(0x10), + FAILURE_FULL(0x11); + + private int code; + + RegistrationResponseStatus(int code){ + this.code = code; + } + + /** + * Get the message code (From the API Spec) + * @return the message code + */ + int getCode(){ + return this.code; + } + + public static RegistrationResponseStatus getResponseStatus(int typeCode){ + switch (typeCode){ + case 0x00: + return SUCCESS_SPECTATING; + case 0x01: + return SUCCESS_PLAYING; + case 0x02: + return SUCCESS_TUTORIAL; + case 0x03: + return SUCCESS_GHOSTING; + case 0x10: + return FAILURE_GENERAL; + case 0x11: + return FAILURE_FULL; + default: + return FAILURE_GENERAL; + } + } +} diff --git a/src/main/java/seng302/gameServer/server/simulator/parsers/CourseParser.java b/src/main/java/seng302/gameServer/server/simulator/parsers/CourseParser.java index 66def8a1..36164af2 100644 --- a/src/main/java/seng302/gameServer/server/simulator/parsers/CourseParser.java +++ b/src/main/java/seng302/gameServer/server/simulator/parsers/CourseParser.java @@ -8,10 +8,10 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; -import seng302.model.mark.CompoundMark; import seng302.gameServer.server.simulator.Corner; -import seng302.model.mark.Mark; import seng302.gameServer.server.simulator.RoundingType; +import seng302.model.mark.CompoundMark; +import seng302.model.mark.Mark; /** * Parses the race xml file to get course details @@ -79,21 +79,22 @@ public class CourseParser extends FileParser { if (node.getNodeType() == Node.ELEMENT_NODE) { Element e = (Element) node; Integer markID = Integer.valueOf(e.getAttribute("CompoundMarkID")); - String name = e.getAttribute("Name"); - CompoundMark cMark = new CompoundMark(markID, name); NodeList marks = e.getElementsByTagName("Mark"); - for (int i = 0; i < marks.getLength(); i++) { - Mark mark = getMark(marks.item(i)); - if (mark != null) - cMark.addSubMarks(mark); - } - return cMark; - } - System.out.println("Failed to create compound mark."); - return null; - } + List subMarks = new ArrayList<>(); + for (int i = 0; i < marks.getLength(); i++) { + Mark mark = getMark(marks.item(i)); + if (mark != null) { + subMarks.add(mark); + } + } + + return new CompoundMark(markID, name, subMarks); + } + System.out.println("Failed to create compound mark."); + return null; + } private Mark getMark(Node node) { diff --git a/src/main/java/seng302/model/Yacht.java b/src/main/java/seng302/model/Yacht.java index addb6a40..f71e49da 100644 --- a/src/main/java/seng302/model/Yacht.java +++ b/src/main/java/seng302/model/Yacht.java @@ -10,6 +10,8 @@ import javafx.beans.property.ReadOnlyDoubleWrapper; import javafx.beans.property.ReadOnlyLongProperty; import javafx.beans.property.ReadOnlyLongWrapper; import javafx.scene.paint.Color; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import seng302.gameServer.GameState; import seng302.model.mark.CompoundMark; import seng302.model.mark.Mark; @@ -28,7 +30,10 @@ public class Yacht { void notifyLocation(Yacht yacht, double lat, double lon, double heading, double velocity); } - private static final Double ROUNDING_DISTANCE = 15d; // TODO: 3/08/17 wmu16 - Look into this value further + private Logger logger = LoggerFactory.getLogger(Yacht.class); + + private static final Double ROUNDING_DISTANCE = 50d; // TODO: 3/08/17 wmu16 - Look into this value further + //BOTH AFAIK private String boatType; @@ -39,11 +44,10 @@ public class Yacht { private String country; private Long estimateTimeAtFinish; - private Long lastMark; + private Integer currentMarkSeqID = 0; private Long markRoundTime; - private Double distanceToNextMark; + private Double distanceToCurrentMark; private Long timeTillNext; - private CompoundMark nextMark; private Double heading; private Integer legNumber = 0; @@ -54,11 +58,15 @@ public class Yacht { private GeoPoint location; private Integer boatStatus; private Double velocity; + private Boolean isAuto; + private Double autoHeading; + //MARK ROUNDING INFO private GeoPoint lastLocation; //For purposes of mark rounding calculations private Boolean hasEnteredRoundingZone; //The distance that the boat must be from the mark to round - private Boolean hasPassedFirstLine; //The line extrapolated from the next mark to the current mark - private Boolean hasPassedSecondLine; //The line extrapolated from the last mark to the current mark + private Boolean hasPassedLine; + private Boolean hasPassedThroughGate; + private Boolean finishedRace; //CLIENT SIDE private List locationListeners = new ArrayList<>(); @@ -78,14 +86,16 @@ public class Yacht { this.boatName = boatName; this.country = country; this.sailIn = false; + this.isAuto = false; this.location = new GeoPoint(57.670341, 11.826856); this.lastLocation = location; this.heading = 120.0; //In degrees this.velocity = 0d; //in mms-1 this.hasEnteredRoundingZone = false; - this.hasPassedFirstLine = false; - this.hasPassedSecondLine = false; + this.hasPassedLine = false; + this.hasPassedThroughGate = false; + this.finishedRace = false; } /** @@ -121,13 +131,15 @@ public class Yacht { } } + runAutoPilot(); + //UPDATE BOAT LOCATION + lastLocation = location; location = GeoUtility.getGeoCoordinate(location, heading, velocity * secondsElapsed); //CHECK FOR MARK ROUNDING - distanceToNextMark = calcDistanceToNextMark(); - if (distanceToNextMark < ROUNDING_DISTANCE) { - hasEnteredRoundingZone = true; + if (!finishedRace) { + checkForLegProgression(); } // TODO: 3/08/17 wmu16 - Implement line cross check here @@ -135,14 +147,16 @@ public class Yacht { /** - * Calculates the distance to the next mark (closest of the two if a gate mark). - * + * Calculates the distance to the next mark (closest of the two if a gate mark). For purposes + * of mark rounding * @return A distance in metres. Returns -1 if there is no next mark + * @throws IndexOutOfBoundsException If the next mark is null (ie the last mark in the race) + * Check first using {@link seng302.model.mark.MarkOrder#isLastMark(Integer)} */ - public Double calcDistanceToNextMark() { - if (nextMark == null) { - return -1d; - } else if (nextMark.isGate()) { + public Double calcDistanceToCurrentMark() throws IndexOutOfBoundsException { + CompoundMark nextMark = GameState.getMarkOrder().getCurrentMark(currentMarkSeqID); + + if (nextMark.isGate()) { Mark sub1 = nextMark.getSubMark(1); Mark sub2 = nextMark.getSubMark(2); Double distance1 = GeoUtility.getDistance(location, sub1); @@ -153,15 +167,198 @@ public class Yacht { } } + + /** + * 4 Different cases of progression in the race + * 1 - Passing the start line + * 2 - Passing any in-race Gate + * 3 - Passing any in-race Mark + * 4 - Passing the finish line + */ + private void checkForLegProgression() { + CompoundMark currentMark = GameState.getMarkOrder().getCurrentMark(currentMarkSeqID); + if (currentMarkSeqID == 0) { + checkStartLineCrossing(currentMark); + } else if (GameState.getMarkOrder().isLastMark(currentMarkSeqID)) { + checkFinishLineCrossing(currentMark); + } else if (currentMark.isGate()) { + checkGateRounding(currentMark); + } else { + checkMarkRounding(currentMark); + } + } + + /** + * If we pass the start line gate in the correct direction, progress + * + * @param currentMark The current gate + */ + private void checkStartLineCrossing(CompoundMark currentMark) { + Mark mark1 = currentMark.getSubMark(1); + Mark mark2 = currentMark.getSubMark(2); + CompoundMark nextMark = GameState.getMarkOrder().getNextMark(currentMarkSeqID); + + Integer crossedLine = GeoUtility.checkCrossedLine(mark1, mark2, lastLocation, location); + if (crossedLine > 0) { + Boolean isClockwiseCross = GeoUtility.isClockwise(mark1, mark2, nextMark.getMidPoint()); + if (crossedLine == 2 && isClockwiseCross || crossedLine == 1 && !isClockwiseCross) { + currentMarkSeqID++; + logMarkRounding(currentMark); + } + } + } + + + /** + * This algorithm checks for mark rounding. And increments the currentMarSeqID number attribute + * of the yacht if so. + * A visual representation of this algorithm can be seen on the Wiki under + * 'mark passing algorithm' + */ + private void checkMarkRounding(CompoundMark currentMark) { + distanceToCurrentMark = calcDistanceToCurrentMark(); + GeoPoint nextPoint = GameState.getMarkOrder().getNextMark(currentMarkSeqID).getMidPoint(); + GeoPoint prevPoint = GameState.getMarkOrder().getPreviousMark(currentMarkSeqID) + .getMidPoint(); + GeoPoint midPoint = GeoUtility.getDirtyMidPoint(nextPoint, prevPoint); + + //1 TEST FOR ENTERING THE ROUNDING DISTANCE + if (distanceToCurrentMark < ROUNDING_DISTANCE) { + hasEnteredRoundingZone = true; + } + + //In case current mark is a gate, loop through all marks just in case + for (Mark thisCurrentMark : currentMark.getMarks()) { + if (GeoUtility.isPointInTriangle(lastLocation, location, midPoint, thisCurrentMark)) { + hasPassedLine = true; + } + } + + if (hasPassedLine && hasEnteredRoundingZone) { + currentMarkSeqID++; + hasPassedLine = false; + hasEnteredRoundingZone = false; + hasPassedThroughGate = false; + logMarkRounding(currentMark); + } + } + + + /** + * Checks if a gate line has been crossed and in the correct direction + * + * @param currentMark The current gate + */ + private void checkGateRounding(CompoundMark currentMark) { + Mark mark1 = currentMark.getSubMark(1); + Mark mark2 = currentMark.getSubMark(2); + CompoundMark prevMark = GameState.getMarkOrder().getPreviousMark(currentMarkSeqID); + CompoundMark nextMark = GameState.getMarkOrder().getNextMark(currentMarkSeqID); + + Integer crossedLine = GeoUtility.checkCrossedLine(mark1, mark2, lastLocation, location); + + //We have crossed the line + if (crossedLine > 0) { + Boolean isClockwiseCross = GeoUtility.isClockwise(mark1, mark2, prevMark.getMidPoint()); + + //Check we cross the line in the correct direction + if (crossedLine == 1 && isClockwiseCross || crossedLine == 2 && !isClockwiseCross) { + hasPassedThroughGate = true; + } + } + + Boolean prevMarkSide = GeoUtility.isClockwise(mark1, mark2, prevMark.getMidPoint()); + Boolean nextMarkSide = GeoUtility.isClockwise(mark1, mark2, nextMark.getMidPoint()); + + if (hasPassedThroughGate) { + //Check if we need to round this gate after passing through + if (prevMarkSide == nextMarkSide) { + checkMarkRounding(currentMark); + } else { + currentMarkSeqID++; + logMarkRounding(currentMark); + } + } + } + + /** + * If we pass the finish gate in the correct direction + * + * @param currentMark The current gate + */ + private void checkFinishLineCrossing(CompoundMark currentMark) { + Mark mark1 = currentMark.getSubMark(1); + Mark mark2 = currentMark.getSubMark(2); + CompoundMark prevMark = GameState.getMarkOrder().getPreviousMark(currentMarkSeqID); + + Integer crossedLine = GeoUtility.checkCrossedLine(mark1, mark2, lastLocation, location); + if (crossedLine > 0) { + Boolean isClockwiseCross = GeoUtility.isClockwise(mark1, mark2, prevMark.getMidPoint()); + if (crossedLine == 1 && isClockwiseCross || crossedLine == 2 && !isClockwiseCross) { + currentMarkSeqID++; + finishedRace = true; + logMarkRounding(currentMark); + logger.debug(sourceId + " finished"); + // TODO: 8/08/17 wmu16 - Do something! + } + } + } + + + /** + * Adjusts the heading of the boat by a given amount, while recording the boats + * last heading. + * + * @param amount the amount by which to adjust the boat heading. + */ public void adjustHeading(Double amount) { Double newVal = heading + amount; lastHeading = heading; heading = (double) Math.floorMod(newVal.longValue(), 360L); } + /** + * Swaps the boats direction from one side of the wind to the other. + */ public void tackGybe(Double windDirection) { - Double normalizedHeading = normalizeHeading(); - adjustHeading(-2 * normalizedHeading); + if (isAuto) { + disableAutoPilot(); + } else { + Double normalizedHeading = normalizeHeading(); + Double newVal = (-2 * normalizedHeading) + heading; + Double newHeading = (double) Math.floorMod(newVal.longValue(), 360L); + setAutoPilot(newHeading); + } + } + + /** + * Enables the boats auto pilot feature, which will move the boat towards a given heading. + * @param thisHeading The heading to move the boat towards. + */ + private void setAutoPilot(Double thisHeading) { + isAuto = true; + autoHeading = thisHeading; + } + + /** + * Disables the auto pilot function. + */ + public void disableAutoPilot() { + isAuto = false; + } + + /** + * Moves the boat towards the given heading when the auto pilot was set. Disables the auto pilot + * in the event that the boat is within the range of 1 turn step of its goal. + */ + public void runAutoPilot() { + if (isAuto) { + turnTowardsHeading(autoHeading); + if (Math.abs(heading - autoHeading) + <= TURN_STEP) { //Cancel when within 1 turn step of target. + isAuto = false; + } + } } public void toggleSailIn() { @@ -169,6 +366,7 @@ public class Yacht { } public void turnUpwind() { + disableAutoPilot(); Double normalizedHeading = normalizeHeading(); if (normalizedHeading == 0) { if (lastHeading < 180) { @@ -190,6 +388,7 @@ public class Yacht { } public void turnDownwind() { + disableAutoPilot(); Double normalizedHeading = normalizeHeading(); if (normalizedHeading == 0) { if (lastHeading < 180) { @@ -210,39 +409,59 @@ public class Yacht { } } + /** + * Takes the VMG from the polartable for upwind or downwind depending on the boats direction, + * and uses this to calculate a heading to move the yacht towards. + */ public void turnToVMG() { - Double normalizedHeading = normalizeHeading(); - Double optimalHeading; - HashMap optimalPolarMap; - - if (normalizedHeading >= 90 && normalizedHeading <= 270) { // Downwind - optimalPolarMap = PolarTable.getOptimalDownwindVMG(GameState.getWindSpeedKnots()); - optimalHeading = optimalPolarMap.keySet().iterator().next(); + if (isAuto) { + disableAutoPilot(); } else { - optimalPolarMap = PolarTable.getOptimalUpwindVMG(GameState.getWindSpeedKnots()); - optimalHeading = optimalPolarMap.keySet().iterator().next(); - } - // Take optimal heading and turn into correct - optimalHeading = - optimalHeading + (double) Math.floorMod(GameState.getWindDirection().longValue(), 360L); + Double normalizedHeading = normalizeHeading(); + Double optimalHeading; + HashMap optimalPolarMap; - turnTowardsHeading(optimalHeading); - - } - - private void turnTowardsHeading(Double newHeading) { - System.out.println(newHeading); - if (heading < 90 && newHeading > 270) { - adjustHeading(-TURN_STEP); - } else { - if (heading < newHeading) { - adjustHeading(TURN_STEP); + if (normalizedHeading >= 90 && normalizedHeading <= 270) { // Downwind + optimalPolarMap = PolarTable.getOptimalDownwindVMG(GameState.getWindSpeedKnots()); } else { - adjustHeading(-TURN_STEP); + optimalPolarMap = PolarTable.getOptimalUpwindVMG(GameState.getWindSpeedKnots()); } + optimalHeading = optimalPolarMap.keySet().iterator().next(); + + if (normalizedHeading > 180) { + optimalHeading = 360 - optimalHeading; + } + + // Take optimal heading and turn into a boat heading rather than a wind heading. + optimalHeading = + optimalHeading + GameState.getWindDirection(); + + setAutoPilot(optimalHeading); } } + /** + * Takes a given heading and rotates the boat towards that heading. + * This does not care about being upwind or downwind, just which direction will reach a given + * heading faster. + * + * @param newHeading The heading to turn the yacht towards. + */ + private void turnTowardsHeading(Double newHeading) { + Double newVal = heading - newHeading; + if (Math.floorMod(newVal.longValue(), 360L) > 180) { + adjustHeading(TURN_STEP / 5); + } else { + adjustHeading(-TURN_STEP / 5); + } + } + + /** + * Returns a heading normalized for the wind direction. Heading direction into the wind is 0, + * directly away is 180. + * + * @return The normalized heading accounting for wind direction. + */ private Double normalizeHeading() { Double normalizedHeading = heading - GameState.windDirection; normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360L); @@ -357,14 +576,6 @@ public class Yacht { this.lastMarkRounded = lastMarkRounded; } - public void setNextMark(CompoundMark nextMark) { - this.nextMark = nextMark; - } - - public CompoundMark getNextMark(){ - return nextMark; - } - public GeoPoint getLocation() { return location; } @@ -429,8 +640,8 @@ public class Yacht { this.velocity = velocity; } - public Double getDistanceToNextMark() { - return distanceToNextMark; + public Double getDistanceToCurrentMark() { + return distanceToCurrentMark; } public void updateLocation(double lat, double lng, double heading, double velocity) { @@ -443,6 +654,20 @@ public class Yacht { } } + private void logMarkRounding(CompoundMark currentMark) { + String typeString = "mark"; + if (currentMark.isGate()) { + typeString = "gate"; + } + logger.debug( + String.format("BoatID %d passed %s %s with id %d. Now on leg %d", + sourceId, + typeString, + currentMark.getMarks().get(0).getName(), + currentMark.getId(), + currentMarkSeqID)); + } + public void addLocationListener (YachtLocationListener listener) { locationListeners.add(listener); } diff --git a/src/main/java/seng302/model/mark/CompoundMark.java b/src/main/java/seng302/model/mark/CompoundMark.java index af4cd8a8..fe5147de 100644 --- a/src/main/java/seng302/model/mark/CompoundMark.java +++ b/src/main/java/seng302/model/mark/CompoundMark.java @@ -1,8 +1,9 @@ package seng302.model.mark; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; +import seng302.model.GeoPoint; +import seng302.utilities.GeoUtility; public class CompoundMark { @@ -10,18 +11,17 @@ public class CompoundMark { private String name; private List marks = new ArrayList<>(); + private GeoPoint midPoint; - public CompoundMark(int markID, String name) { - this.compoundMarkId = markID; - this.name = name; - } - - public void addSubMarks(Mark... marks) { - this.marks.addAll(Arrays.asList(marks)); - } - - public void addSubMarks(List marks) { - this.marks.addAll(marks); + public CompoundMark(int markID, String name, List marks) { + this.compoundMarkId = markID; + this.name = name; + this.marks.addAll(marks); + if (marks.size() > 1) { + this.midPoint = GeoUtility.getDirtyMidPoint(marks.get(0), marks.get(1)); + } else { + this.midPoint = marks.get(0); + } } /** @@ -68,6 +68,16 @@ public class CompoundMark { } } + /** + * NOTE: This is a 'dirty' mid point as it is simply calculated as an xy point would be. + * NO CHECKING FOR LAT / LNG WRAPPING IS DONE IN CREATION OF THIS MIDPOINT + * + * @return GeoPoint of the midpoint of the two marks, or the one mark if there is only one + */ + public GeoPoint getMidPoint() { + return midPoint; + } + /** * Returns whether or not this CompoundMark is a Gate. It is generally cleaner to program to a * specific singleMark or the list of marks. @@ -87,38 +97,6 @@ public class CompoundMark { return marks; } - -// @Override -// public boolean equals(Object other) { -// if (other == null) { -// return false; -// } -// -// if (!(other instanceof Mark)){ -// return false; -// } -// -// Mark otherMark = (Mark) other; -// -// if (otherMark.getLat() != getLat() || otherMark.getLongitude() != getLongitude()) { -// return false; -// } -// -// if (otherMark.getCompoundMarkID() != getCompoundMarkID()){ -// return false; -// } -// -// if (otherMark.getId() != getId()){ -// return false; -// } -// -// if (!otherMark.getName().equals(name)){ -// return false; -// } -// -// return true; -// } - @Override public int hashCode() { int hash = 0; diff --git a/src/main/java/seng302/model/mark/MarkOrder.java b/src/main/java/seng302/model/mark/MarkOrder.java index ca2b4356..1b744fc2 100644 --- a/src/main/java/seng302/model/mark/MarkOrder.java +++ b/src/main/java/seng302/model/mark/MarkOrder.java @@ -1,5 +1,14 @@ package seng302.model.mark; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; @@ -10,21 +19,12 @@ import seng302.model.stream.xml.parser.RaceXMLData; import seng302.utilities.XMLGenerator; import seng302.utilities.XMLParser; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import java.io.IOException; -import java.io.StringReader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - /** * Class to hold the order of the marks in the race. */ public class MarkOrder { - private List raceMarkOrder; + + private List raceMarkOrder; private Logger logger = LoggerFactory.getLogger(MarkOrder.class); public MarkOrder(){ @@ -35,7 +35,7 @@ public class MarkOrder { * @return An ordered list of marks in the race * OR null if the mark order could not be loaded */ - public List getMarkOrder(){ + public List getMarkOrder() { if (raceMarkOrder == null){ logger.warn("Race order accessed but not instantiated"); return null; @@ -45,26 +45,34 @@ public class MarkOrder { } /** - * Returns the mark in the race after the previous mark - * @param position The current race position - * @return the next race position - * OR null if there is no position + * @param seqID The seqID of the current mark the boat is heading to + * @return A Boolean indicating if this coming mark is the last one (finish line) */ - public RacePosition getNextPosition(RacePosition position){ - Mark previousMark = position.getNextMark(); - Mark nextMark; + public Boolean isLastMark(Integer seqID) { + return seqID == raceMarkOrder.size() - 1; + } - if (position.getPositionIndex() + 1 >= raceMarkOrder.size() - 1){ - RacePosition nextRacePosition = new RacePosition(raceMarkOrder.size() - 1, null, previousMark); - nextRacePosition.setFinishingLeg(); + /** + * @param currentSeqID The seqID of the current mark the boat is heading to + * @return The mark last passed + * @throws IndexOutOfBoundsException if there is no next mark. Check seqID != 0 first + */ + public CompoundMark getPreviousMark(Integer currentSeqID) throws IndexOutOfBoundsException { + return raceMarkOrder.get(currentSeqID - 1); + } - return nextRacePosition; - } + public CompoundMark getCurrentMark(Integer currentSeqID) { + return raceMarkOrder.get(currentSeqID); + } - Integer nextPositionIndex = position.getPositionIndex() + 1; - RacePosition nextRacePosition = new RacePosition(nextPositionIndex, raceMarkOrder.get(nextPositionIndex), previousMark); - - return nextRacePosition; + /** + * @param currentSeqID The seqID of the current mark the boat is heading to + * @return The mark following the mark that the boat is heading to + * @throws IndexOutOfBoundsException if there is no next mark. Check using {@link + * #isLastMark(Integer)} + */ + public CompoundMark getNextMark(Integer currentSeqID) throws IndexOutOfBoundsException { + return raceMarkOrder.get(currentSeqID + 1); } /** @@ -72,7 +80,7 @@ public class MarkOrder { * @param xml An AC35 RaceXML * @return An ordered list of marks in the race */ - private List loadRaceOrderFromXML(String xml){ + private List loadRaceOrderFromXML(String xml) { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db; @@ -92,11 +100,11 @@ public class MarkOrder { logger.debug("Loaded RaceXML for mark order"); List corners = data.getMarkSequence(); Map marks = data.getCompoundMarks(); - List course = new ArrayList<>(); + List course = new ArrayList<>(); for (Corner corner : corners){ CompoundMark compoundMark = marks.get(corner.getCompoundMarkID()); - course.add(compoundMark.getMarks().get(0)); + course.add(compoundMark); } return course; @@ -105,17 +113,6 @@ public class MarkOrder { return null; } - /** - * @return The first position in the race - */ - public RacePosition getFirstPosition(){ - if (raceMarkOrder.size() > 0){ - return new RacePosition(-1, raceMarkOrder.get(0), null); - } - - return null; - } - /** * Load the raceXML and mark order */ @@ -132,4 +129,4 @@ public class MarkOrder { } raceMarkOrder = loadRaceOrderFromXML(raceXML); } -} +} \ No newline at end of file diff --git a/src/main/java/seng302/model/mark/RacePosition.java b/src/main/java/seng302/model/mark/RacePosition.java deleted file mode 100644 index fc160b10..00000000 --- a/src/main/java/seng302/model/mark/RacePosition.java +++ /dev/null @@ -1,55 +0,0 @@ -package seng302.model.mark; - -/** - * Represents a boats position between two marks - */ -public class RacePosition { - private Integer positionIndex; - private Mark nextMark; - private Mark previousMark; - private Boolean isFinishingLeg; - - public RacePosition(Integer positionIndex, Mark nextMark, Mark previousMark){ - this.positionIndex = positionIndex; - this.nextMark = nextMark; - this.previousMark = previousMark; - isFinishingLeg = false; - } - - /** - * @return The position of the boat (0...number of marks in race - 1) - */ - public Integer getPositionIndex(){ - return positionIndex; - } - - /** - * @return The mark the boat is heading to - * will return NULL if this is the finishing legg - */ - public Mark getNextMark(){ - return nextMark; - } - - /** - * @return The mark the boat is heading away from, - * Will return NULL if this is the starting leg - */ - public Mark getPreviousMark(){ - return previousMark; - } - - /** - * Sets a flag that this is the last leg in the race - */ - public void setFinishingLeg(){ - isFinishingLeg = true; - } - - /** - * @return true if this is the last leg in the race - */ - public boolean getIsFinishingLeg() { - return isFinishingLeg; - } -} diff --git a/src/main/java/seng302/model/stream/packets/PacketType.java b/src/main/java/seng302/model/stream/packets/PacketType.java index e7f55b49..1a38ae14 100644 --- a/src/main/java/seng302/model/stream/packets/PacketType.java +++ b/src/main/java/seng302/model/stream/packets/PacketType.java @@ -19,7 +19,9 @@ public enum PacketType { COURSE_WIND, AVG_WIND, BOAT_ACTION, - OTHER; + OTHER, + RACE_REGISTRATION_REQUEST, + RACE_REGISTRATION_RESPONSE; public static PacketType assignPacketType(int packetType, byte[] payload){ switch(packetType){ @@ -56,6 +58,10 @@ public enum PacketType { return AVG_WIND; case 100: return BOAT_ACTION; + case 101: + return RACE_REGISTRATION_REQUEST; + case 102: + return RACE_REGISTRATION_RESPONSE; default: } return OTHER; diff --git a/src/main/java/seng302/utilities/GeoUtility.java b/src/main/java/seng302/utilities/GeoUtility.java index 9aa61b9d..f6968601 100644 --- a/src/main/java/seng302/utilities/GeoUtility.java +++ b/src/main/java/seng302/utilities/GeoUtility.java @@ -45,6 +45,19 @@ public class GeoUtility { return (Math.toDegrees(getBearingRad(p1, p2)) + 360.0) % 360.0; } + + /** + * WARNING: this function DOES NOT account for wrapping around on lats / longs etc. + * SO BE CAREFUL IN USING THIS FUNCTION + * + * @param p1 GeoPoint 1 + * @param p2 GeoPoint 2 + * @return GeoPoint midPoint + */ + public static GeoPoint getDirtyMidPoint(GeoPoint p1, GeoPoint p2) { + return new GeoPoint((p1.getLat() + p2.getLat()) / 2, (p1.getLng() + p2.getLng()) / 2); + } + /** * Calculates the angle between to angular co-ordinates on a sphere in radians. * @@ -93,7 +106,6 @@ public class GeoUtility { return new GeoPoint(Math.toDegrees(endLat), Math.toDegrees(endLng)); } - /** * Performs the line function on two points of a line and a test point to test which side of the * line that point is on. If the return value is return 1, then the point is on one side of the @@ -125,6 +137,31 @@ public class GeoUtility { } } + /** + * Checks if the line formed by lastLocation and location doesn't intersect the line segment + * formed by mark1 and mark2 See the wiki Mark Rounding algorithm for more info + * + * @param mark1 One mark of the line + * @param mark2 The second mark of the line + * @param lastLocation The last location of the point crossing this line + * @param location The current location of the point crossing this line + * @return 0 if two line segment doesn't intersect, otherwise 1 if they intersect and + * lastLocation is on RHS of the line segment (mark1 to mark2) or 2 if lastLocation on LHS of + * the line segment (mark1 to mark2) + */ + public static Integer checkCrossedLine(GeoPoint mark1, GeoPoint mark2, GeoPoint lastLocation, + GeoPoint location) { + boolean enteredDirection = isClockwise(mark1, mark2, lastLocation); + boolean exitedDirection = isClockwise(mark1, mark2, location); + if (enteredDirection != exitedDirection) { + if (!isPointInTriangle(mark1, lastLocation, location, mark2) + && !isPointInTriangle(mark2, lastLocation, location, mark1)) { + + return enteredDirection ? 1 : 2; + } + } + return 0; + } /** * Given a point and a vector (angle and vector length) Will create a new point, that vector @@ -155,10 +192,24 @@ public class GeoUtility { * @param bearing2 the bearing of v2 * @return the difference of bearing from v1 to v2 */ - private static double getBearingDiff(double bearing1, double bearing2) { + private static Double getBearingDiff(double bearing1, double bearing2) { return ((360 - bearing1) + bearing2) % 360; } + /** + * Check if a geo point ins on the right hand side of the line segment, which + * formed by two geo points v1 to v2. (Algorithm: point is clockwise to the + * line if the bearing difference is less than 180 deg.) + * + * @param v1 one end of the line segment + * @param v2 another end of the line segment + * @param point the point to be tested + * @return true if the point is on the RHS of the line + */ + public static Boolean isClockwise(GeoPoint v1, GeoPoint v2, GeoPoint point) { + return getBearingDiff(getBearing(v1, v2), getBearing(v1, point)) < 180; + } + /** * Given three geo points to form a triangle, the method returns true if the fourth point is * inside the triangle @@ -169,15 +220,15 @@ public class GeoUtility { * @param point the point to be tested * @return true if the fourth point is inside the triangle */ - public static boolean isPointInTriangle(GeoPoint v1, GeoPoint v2, GeoPoint v3, GeoPoint point) { - // true, if diff of bearing from (v1->v2) to (v1->p) is less than 180 deg - boolean sideFlag = getBearingDiff(getBearing(v1, v2), getBearing(v1, point)) < 180; + public static Boolean isPointInTriangle(GeoPoint v1, GeoPoint v2, GeoPoint v3, GeoPoint point) { + // true, if diff of bearing from (v1 to v2) to (v1 to p) is less than 180 deg + boolean isCW = isClockwise(v1, v2, point); - if ((getBearingDiff(getBearing(v2, v3), getBearing(v2, point)) < 180) != sideFlag) { + if (isClockwise(v2, v3, point) != isCW) { return false; } - if ((getBearingDiff(getBearing(v3, v1), getBearing(v3, point)) < 180) != sideFlag) { + if (isClockwise(v3, v1, point) != isCW) { return false; } diff --git a/src/main/java/seng302/utilities/XMLParser.java b/src/main/java/seng302/utilities/XMLParser.java index c231535d..c4c4348b 100644 --- a/src/main/java/seng302/utilities/XMLParser.java +++ b/src/main/java/seng302/utilities/XMLParser.java @@ -256,9 +256,9 @@ public class XMLParser { if (cMarkNode.getNodeName().equals("CompoundMark")) { cMark = new CompoundMark( XMLParser.getNodeAttributeInt(cMarkNode, "CompoundMarkID"), - XMLParser.getNodeAttributeString(cMarkNode, "Name") + XMLParser.getNodeAttributeString(cMarkNode, "Name"), + createMarks(cMarkNode) ); - cMark.addSubMarks(createMarks(cMarkNode)); allMarks.add(cMark); } } diff --git a/src/main/java/seng302/visualiser/ClientToServerThread.java b/src/main/java/seng302/visualiser/ClientToServerThread.java index c387ff46..2313ebb2 100644 --- a/src/main/java/seng302/visualiser/ClientToServerThread.java +++ b/src/main/java/seng302/visualiser/ClientToServerThread.java @@ -7,6 +7,7 @@ import java.io.OutputStream; import java.net.Socket; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Queue; import java.util.Timer; @@ -17,9 +18,16 @@ import java.util.zip.Checksum; import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; -import seng302.gameServer.server.messages.BoatActionMessage; +import javafx.scene.control.ButtonType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import seng302.gameServer.server.messages.BoatAction; +import seng302.gameServer.server.messages.BoatActionMessage; +import seng302.gameServer.server.messages.ClientType; import seng302.gameServer.server.messages.Message; +import seng302.gameServer.server.messages.RegistrationRequestMessage; +import seng302.gameServer.server.messages.RegistrationResponseStatus; +import seng302.model.stream.packets.PacketType; import seng302.model.stream.packets.StreamPacket; /** @@ -51,6 +59,8 @@ public class ClientToServerThread implements Runnable { private Socket socket; private InputStream is; + private Logger logger = LoggerFactory.getLogger(ClientToServerThread.class); + //Output stream private OutputStream os; private Timer upWindPacketTimer = new Timer(); @@ -58,7 +68,9 @@ public class ClientToServerThread implements Runnable { private boolean upwindTimerFlag = false, downwindTimerFlag = false; static public final int PACKET_SENDING_INTERVAL_MS = 100; - private int clientId; + private int clientId = -1; + +// private Boolean updateClient = true; private ByteArrayOutputStream crcBuffer; private boolean socketOpen = true; @@ -78,15 +90,8 @@ public class ClientToServerThread implements Runnable { socket = new Socket(ipAddress, portNumber); is = socket.getInputStream(); os = socket.getOutputStream(); - Integer allocatedID = threeWayHandshake(); - if (allocatedID != null) { - clientId = allocatedID; - clientLog("Successful handshake. Allocated ID: " + clientId, 1); - } else { - clientLog("Unsuccessful handshake", 1); - closeSocket(); - return; - } + + sendRegistrationRequest(); thread = new Thread(this); thread.start(); @@ -135,15 +140,22 @@ public class ClientToServerThread implements Runnable { if (streamPackets.size() > 0) { streamPackets.add(new StreamPacket(type, payloadLength, timeStamp, payload)); } else { - streamPackets.add(new StreamPacket(type, payloadLength, timeStamp, payload)); - for (ClientSocketListener csl : listeners) - csl.newPacket(); + if (PacketType.RACE_REGISTRATION_RESPONSE == PacketType.assignPacketType(type, payload)){ + processRegistrationResponse(new StreamPacket(type, payloadLength, timeStamp, payload)); + } + else { + if (clientId == -1) continue; // Do not continue if not registered + streamPackets.add(new StreamPacket(type, payloadLength, timeStamp, payload)); + for (ClientSocketListener csl : listeners) + csl.newPacket(); + } } } else { clientLog("Packet has been dropped", 1); } } } catch (ByteReadException e) { + e.printStackTrace(); closeSocket(); if (Platform.isFxApplicationThread()) { Platform.runLater(() -> { @@ -163,30 +175,52 @@ public class ClientToServerThread implements Runnable { /** - * Listens for an allocated sourceID and returns it to the server - * - * @return the sourceID allocated to us by the server + * Sends a request to the server asking for a source ID */ - private Integer threeWayHandshake() { - Integer ourSourceID = null; - while (true) { - try { - ourSourceID = is.read(); - } catch (IOException e) { - clientLog("Three way handshake failed", 1); - } - if (ourSourceID != null) { - try { - os.write(ourSourceID); - return ourSourceID; - } catch (IOException e) { - clientLog("Three way handshake failed", 1); - return null; - } - } + private void sendRegistrationRequest() { + RegistrationRequestMessage requestMessage = new RegistrationRequestMessage(ClientType.PLAYER); + + try { + os.write(requestMessage.getBuffer()); + } catch (IOException e) { + logger.error("Could not send registration request. Exiting"); + System.exit(1); } } + /** + * Accepts a response to the registration request message, and updates the client OR quits + * @param packet The registration requests packet + */ + private void processRegistrationResponse(StreamPacket packet){ + int sourceId = (int) Message.bytesToLong(Arrays.copyOfRange(packet.getPayload(), 0, 3)); + int statusCode = (int) Message.bytesToLong(Arrays.copyOfRange(packet.getPayload(), 4,5)); + + RegistrationResponseStatus status = RegistrationResponseStatus.getResponseStatus(statusCode); + + if (status.equals(RegistrationResponseStatus.SUCCESS_PLAYING)){ + clientId = sourceId; + + return; + } + + logger.error("Server Denied Connection, Exiting"); + + final String alertErrorText; + + if (status.equals(RegistrationResponseStatus.FAILURE_FULL)){ + alertErrorText = "Server is full"; + } + else{ + alertErrorText = "Could not connect to server"; + } + + Platform.runLater(() -> { + new Alert(AlertType.ERROR, alertErrorText, ButtonType.OK).showAndWait(); + System.exit(1); + }); + } + /** * Sends packets for the given boat action. Special cases are: \n * - DOWNWIND = Packets are sent every ClientToServerThread.PACKET_SENDING_INTERVAL_MS @@ -254,10 +288,12 @@ public class ClientToServerThread implements Runnable { * @param message The given message type. */ private void sendBoatAction(BoatActionMessage message) { - try { - os.write(message.getBuffer()); - } catch (IOException e) { - clientLog("Could not write to server", 1); + if (clientId != -1) { + try { + os.write(message.getBuffer()); + } catch (IOException e) { + clientLog("Could not write to server", 1); + } } } diff --git a/src/main/java/seng302/visualiser/GameClient.java b/src/main/java/seng302/visualiser/GameClient.java index 04c8a87d..e65cfbd9 100644 --- a/src/main/java/seng302/visualiser/GameClient.java +++ b/src/main/java/seng302/visualiser/GameClient.java @@ -30,7 +30,8 @@ import seng302.visualiser.controllers.LobbyController.CloseStatus; import seng302.visualiser.controllers.RaceViewController; /** - * Created by cir27 on 20/07/17. + * This class is a client side instance of a yacht racing game in JavaFX. The game is instantiated + * with a JavaFX Pane to insert itself into. */ public class GameClient { @@ -47,10 +48,20 @@ public class GameClient { private ObservableList clientLobbyList = FXCollections.observableArrayList(); + /** + * Create an instance of the game client. Does not do anything until run with runAsClient() + * runAsHost(). + * @param holder The JavaFX Pane that the visual elements for the race will be inserted into. + */ public GameClient(Pane holder) { this.holderPane = holder; } + /** + * Connect to a game at the given address and starts the visualiser. + * @param ipAddress IP to connect to. + * @param portNumber Port to connect to. + */ public void runAsClient(String ipAddress, Integer portNumber) { try { socketThread = new ClientToServerThread(ipAddress, portNumber); @@ -58,6 +69,7 @@ public class GameClient { ioe.printStackTrace(); System.out.println("Unable to connect to host..."); } + socketThread.addStreamObserver(this::parsePackets); LobbyController lobbyController = loadLobby(); lobbyController.setPlayerListSource(clientLobbyList); @@ -66,6 +78,11 @@ public class GameClient { lobbyController.addCloseListener((exitCause) -> this.loadStartScreen()); } + /** + * Connect to a game as the host at the given address and starts the visualiser. + * @param ipAddress IP to connect to. + * @param portNumber Port to connect to. + */ public void runAsHost(String ipAddress, Integer portNumber) { server = new MainServerThread(); try { @@ -89,10 +106,8 @@ public class GameClient { private void loadStartScreen() { socketThread.setSocketToClose(); - socketThread = null; if (server != null) { - // TODO: 26/07/17 cir27 - handle disconnecting -// server.shutDown(); + server.terminate(); server = null; } FXMLLoader fxmlLoader = new FXMLLoader( @@ -174,9 +189,9 @@ public class GameClient { StreamParser.extractXmlMessage(packet) ); clientLobbyList.clear(); - allBoatsMap.forEach((id, boat) -> { - clientLobbyList.add(id + " " + boat.getBoatName()); - }); + allBoatsMap.forEach((id, boat) -> + clientLobbyList.add(id + " " + boat.getBoatName()) + ); break; case RACE_START_STATUS: diff --git a/src/main/java/seng302/visualiser/controllers/RaceViewController.java b/src/main/java/seng302/visualiser/controllers/RaceViewController.java index aa9dfd47..4af617fc 100644 --- a/src/main/java/seng302/visualiser/controllers/RaceViewController.java +++ b/src/main/java/seng302/visualiser/controllers/RaceViewController.java @@ -312,7 +312,7 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel updateRaceTime(); updateWindDirection(); updateOrder(); - updateSparkLine(); +// updateSparkLine(); } }, 0, 1000); } diff --git a/src/test/java/seng302/TestRaceTimer.java b/src/test/java/seng302/TestRaceTimer.java deleted file mode 100644 index 8fd5b645..00000000 --- a/src/test/java/seng302/TestRaceTimer.java +++ /dev/null @@ -1,22 +0,0 @@ -package seng302; - -import org.junit.Test; - - -public class TestRaceTimer { - @Test - public void testPositiveTimeString(){ -// RaceViewController controller = new RaceViewController(); -// String result = controller.convertTimeToMinutesSeconds(61); -// -// assertTrue(result.equals("01:01")); - } - - @Test - public void testNegativeTimeString(){ -// RaceViewController controller = new RaceViewController(); -// String result = controller.convertTimeToMinutesSeconds(-61); -// -// assertTrue(result.equals("-01:01")); - } -} diff --git a/src/test/java/seng302/model/YachtTest.java b/src/test/java/seng302/model/YachtTest.java deleted file mode 100644 index 7937b43c..00000000 --- a/src/test/java/seng302/model/YachtTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package seng302.model; - -import static org.junit.Assert.*; - -import org.junit.Before; -import org.junit.Test; -import seng302.model.mark.CompoundMark; -import seng302.model.mark.Mark; - -/** - * Use this link to test geo distances - * http://www.csgnetwork.com/gpsdistcalc.html - * Created by wmu16 on 3/08/17. - */ -public class YachtTest { - - private Yacht yacht; - private CompoundMark compoundMark; - private Double toleranceRatio = 0.01; - private GeoPoint p1 = new GeoPoint(57.670333, 11.827833); - private GeoPoint p2 = new GeoPoint(57.671524, 11.844495); - private GeoPoint p3 = new GeoPoint(57.670822, 11.843392); - private GeoPoint p4 = new GeoPoint(25.694829, 98.392049); - - @Before - public void setup() { - yacht = new Yacht("Yacht", - 0, - "0", - "WillIsCool", - "HaomingIsOk", - "NZL"); - - yacht.setLocation(57.670333, 11.827833); - - compoundMark = new CompoundMark(0, "HaomingsMark"); - Mark subMark1 = new Mark("H", 57.671524, 11.844495, 0); - Mark subMark2 = new Mark("H", 57.670822, 11.843392, 0); - compoundMark.addSubMarks(subMark1, subMark2); - - yacht.setNextMark(compoundMark); - } - - - @Test - public void testDistanceToNextMark() { - Double actual, expected; - actual = yacht.calcDistanceToNextMark(); - expected = 927d; - assertEquals(expected, actual, expected * toleranceRatio); - } - - -} \ No newline at end of file diff --git a/src/test/java/seng302/model/mark/CompoundMarkTest.java b/src/test/java/seng302/model/mark/CompoundMarkTest.java new file mode 100644 index 00000000..83b2b2bb --- /dev/null +++ b/src/test/java/seng302/model/mark/CompoundMarkTest.java @@ -0,0 +1,68 @@ +package seng302.model.mark; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import seng302.model.GeoPoint; + +/** + * A class to test the compound mark calss + * Created by wmu16 on 10/08/17. + */ +public class CompoundMarkTest { + + private Mark mark1; + private Mark mark2; + private CompoundMark gateMark; + private CompoundMark singleMark; + + private static Double TOLERANCE_RATIO = 0.01; + + + @Before + public void setUp() throws Exception { + mark1 = new Mark("Mark1", 57.670333, 11.842833, 0); + mark2 = new Mark("Mark2", 57.671524, 11.844495, 1); + + List gateMarks = new ArrayList(); + gateMarks.add(mark1); + gateMarks.add(mark2); + + List singleMarks = new ArrayList(); + singleMarks.add(mark1); + + gateMark = new CompoundMark(0, "Fun Mark", gateMarks); + singleMark = new CompoundMark(1, "Awesome Mark", singleMarks); + } + + + @Test + public void getSubMark() throws Exception { + assertEquals(mark1, gateMark.getSubMark(1)); + assertEquals(mark2, gateMark.getSubMark(2)); + + assertEquals(mark1, singleMark.getSubMark(1)); + } + + @Test + public void getMidPoint() throws Exception { + GeoPoint result = gateMark.getMidPoint(); + assertEquals(57.6709285, result.getLat(), result.getLat() * TOLERANCE_RATIO); + assertEquals(11.843664, result.getLng(), result.getLng() * TOLERANCE_RATIO); + + result = singleMark.getMidPoint(); + assertEquals(result, mark1); + } + + @Test + public void isGate() throws Exception { + assertTrue(gateMark.isGate()); + assertFalse(singleMark.isGate()); + } + +} \ No newline at end of file diff --git a/src/test/java/seng302/models/MarkOrderTest.java b/src/test/java/seng302/models/MarkOrderTest.java index 8db06ec8..92bbc664 100644 --- a/src/test/java/seng302/models/MarkOrderTest.java +++ b/src/test/java/seng302/models/MarkOrderTest.java @@ -1,21 +1,23 @@ package seng302.models; +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; + import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import seng302.model.mark.Mark; +import seng302.model.mark.CompoundMark; import seng302.model.mark.MarkOrder; -import seng302.model.mark.RacePosition; - -import static junit.framework.TestCase.assertEquals; -import static junit.framework.TestCase.assertTrue; public class MarkOrderTest { private static MarkOrder markOrder; + private static Integer currentSeqID; @BeforeClass public static void setup(){ markOrder = new MarkOrder(); + currentSeqID = 0; } /** @@ -26,54 +28,39 @@ public class MarkOrderTest { assertTrue(markOrder != null); } - /** - * Test if .getNextMark() returns null if it is called with the final mark in the race - */ + @Test - public void testNextMarkAtEnd(){ - // There are no marks in the XML, therefore this can't be tested - if (markOrder.getMarkOrder().size() == 0){ - return; - } + public void testIsLastMark() { + currentSeqID = 0; + assertFalse(markOrder.isLastMark(currentSeqID)); - Mark lastMark = markOrder.getMarkOrder().get(markOrder.getMarkOrder().size() - 1); - Integer lastIndex = markOrder.getMarkOrder().size() - 1; - - RacePosition lastRacePosition = new RacePosition(lastIndex, lastMark, null); - - assertEquals(null, markOrder.getNextPosition(lastRacePosition).getNextMark()); + currentSeqID = markOrder.getMarkOrder().size() - 1; + assertTrue(markOrder.isLastMark(currentSeqID)); } - /** - * Test if .getNextMark() method returns the next mark in the race - */ @Test - public void testNextMark(){ - // There are not enough marks for this to be tested - if (markOrder.getMarkOrder().size() < 2){ - return; - } + public void testGetNextMark() { + currentSeqID = 4; + CompoundMark nextMark = markOrder.getMarkOrder().get(4 + 1); + assertEquals(nextMark, markOrder.getNextMark(currentSeqID)); - RacePosition firstRacePos = new RacePosition(0, markOrder.getMarkOrder().get(0), null); - - assertEquals(markOrder.getMarkOrder().get(1).getName(), markOrder.getNextPosition(firstRacePos).getNextMark().getName()); + currentSeqID = 3; + nextMark = markOrder.getMarkOrder().get(3 + 1); + assertEquals(nextMark, markOrder.getNextMark(currentSeqID)); } - /** - * Test if a whole race can be completed - */ @Test - public void testMarkSequence(){ - RacePosition current = markOrder.getFirstPosition(); + public void testGetCurrentMark() { + currentSeqID = 0; + CompoundMark currentMark = markOrder.getMarkOrder().get(0); + assertEquals(currentMark, markOrder.getCurrentMark(0)); + } - while (!current.getIsFinishingLeg()){ - - current = markOrder.getNextPosition(current); - - if (current.getIsFinishingLeg()){ - assertEquals(null, current.getNextMark()); - } - } + @Test + public void testGetPreviousMark() { + currentSeqID = 1; + CompoundMark prevMark = markOrder.getMarkOrder().get(0); + assertEquals(prevMark, markOrder.getPreviousMark(currentSeqID)); } @AfterClass diff --git a/src/test/java/seng302/models/YachtTest.java b/src/test/java/seng302/models/YachtTest.java new file mode 100644 index 00000000..49b73daa --- /dev/null +++ b/src/test/java/seng302/models/YachtTest.java @@ -0,0 +1,87 @@ +package seng302.models; + +import static org.junit.Assert.assertEquals; + +import java.util.HashMap; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import seng302.gameServer.GameState; +import seng302.model.PolarTable; +import seng302.model.Yacht; + + +public class YachtTest { + + private static Yacht y1; + //Yacht y2; + private static Double windDirection = 180d; + private static Double windSpeed = 20d; + private static GameState gs; + + @BeforeClass + public static void setUp() { + y1 = new Yacht("Yacht", 101, "Y1", "Y1", "Yacht 1", "C1"); + gs = new GameState("localhost"); + } + + @Test + public void tackGybeTest() { + HashMap values = new HashMap<>(); + values.put(280.0, 80.0); + values.put(270.0, 90.0); + values.put(359.0, 1.0); + values.put(180.0, 180.0); + values.put(75.0, 285.0); + + for (Double begin : values.keySet()) { + y1.setHeading(begin); + y1.tackGybe(windDirection); + + for (int i = 0; i < 200; i++) { + y1.runAutoPilot(); + } + assertEquals(values.get(begin), y1.getHeading(), 5.0); + } + } + + @Test + public void vmgTest() { + + PolarTable.parsePolarFile(getClass().getResourceAsStream("/config/acc_polars.csv")); + Double upwind = PolarTable.getOptimalUpwindVMG(windSpeed).keySet().iterator().next(); + Double downwind = PolarTable.getOptimalDownwindVMG(windSpeed).keySet().iterator().next(); + + HashMap values = new HashMap<>(); + + upwind = (double) Math.floorMod(upwind.longValue() + windDirection.longValue(), 360L); + Double upwindRight = upwind; + Double upwindLeft = 360 - upwindRight; + downwind = (double) Math.floorMod(downwind.longValue() + windDirection.longValue(), 360L); + Double downwindRight = downwind; + Double downwindLeft = 360 - downwindRight; + + values.put(190d, upwindRight); + values.put(170d, upwindLeft); + values.put(10d, downwindLeft); + values.put(350d, downwindRight); + + for (Double begin : values.keySet()) { + y1.setHeading(begin); + y1.turnToVMG(); + for (int i = 0; i < 200; i++) { + y1.runAutoPilot(); + } + y1.disableAutoPilot(); + assertEquals(values.get(begin), y1.getHeading(), 5.0); + } + + } + + + @AfterClass + public static void tearDown() { + y1 = null; + } + +} diff --git a/src/test/java/seng302/utilities/GeoUtilityTest.java b/src/test/java/seng302/utilities/GeoUtilityTest.java index 8382bb8c..ca2af0e9 100644 --- a/src/test/java/seng302/utilities/GeoUtilityTest.java +++ b/src/test/java/seng302/utilities/GeoUtilityTest.java @@ -6,12 +6,11 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import javafx.geometry.Point2D; -import org.junit.Before; import org.junit.Test; import seng302.model.GeoPoint; -import seng302.utilities.GeoUtility; /** + * http://www.geoplaner.com/ For plotting geo points for visualisation * To test methods in GeoUtility. * Use this site to calculate distances * https://rechneronline.de/geo-coordinates/#distance @@ -150,4 +149,32 @@ public class GeoUtilityTest { assertFalse(GeoUtility.isPointInTriangle(v1, v2, v3, p2)); } + + + @Test + public void testCheckCrossedGate() { + GeoPoint mark1 = new GeoPoint(37.40937, -122.62233); + GeoPoint mark2 = new GeoPoint(37.40938, -122.62154); + GeoPoint location1 = new GeoPoint(37.40964, -122.62196); + GeoPoint location2 = new GeoPoint(37.40910, -122.62189); + GeoPoint location3 = new GeoPoint(37.40949, -122.62202); + GeoPoint location4 = new GeoPoint(37.40927, -122.62152); + + // M1 -> M3 enters from CCW side + assertTrue(GeoUtility.checkCrossedLine(mark1, mark2, location1, location2) == 2); + // M1 -> M3 doesn't across + assertFalse(GeoUtility.checkCrossedLine(mark1, mark2, location1, location3) > 0); + // M2 -> M3 enters from CW side + assertTrue(GeoUtility.checkCrossedLine(mark1, mark2, location2, location3) == 1); + // order changes intersect direction + assertTrue(GeoUtility.checkCrossedLine(mark2, mark1, location2, location3) == 2); + assertTrue(GeoUtility.checkCrossedLine(mark1, mark2, location3, location2) == 2); + } + + @Test + public void testDirtyMiddlePoint() { + GeoPoint result = GeoUtility.getDirtyMidPoint(p1, p2); + assertEquals(57.6709285, result.getLat(), result.getLat() * toleranceRate); + assertEquals(11.836164, result.getLng(), result.getLng() * toleranceRate); + } } \ No newline at end of file diff --git a/src/test/java/seng302/visualiser/ClientToServerTests/RegularPacketsTest.java b/src/test/java/seng302/visualiser/ClientToServerTests/RegularPacketsTest.java index 41583cfe..bf5fcab1 100644 --- a/src/test/java/seng302/visualiser/ClientToServerTests/RegularPacketsTest.java +++ b/src/test/java/seng302/visualiser/ClientToServerTests/RegularPacketsTest.java @@ -15,7 +15,6 @@ import seng302.visualiser.ClientToServerThread; /** * Test for checking how regularly packets are sent from ClientToServer Thread. */ -//@FixMethodOrder(MethodSorters.NAME_ASCENDING) public class RegularPacketsTest { private MainServerThread serverThread; @@ -30,7 +29,7 @@ public class RegularPacketsTest { } @Test - public void Test1PacketsSentAtRegularIntervals () throws Exception { + public void packetsSentAtRegularIntervals () throws Exception { final double TEST_DISTANCE = 10.0; serverThread.startGame(); SleepThreadMaxDelay(); @@ -51,7 +50,7 @@ public class RegularPacketsTest { } @Test - public void Test2ArbitraryPacketSentOnRelease() throws Exception { + public void testArbitraryPacketSent() throws Exception { serverThread.startGame(); SleepThreadMaxDelay(); Yacht yacht = new ArrayList<>(GameState.getYachts().values()).get(0); @@ -61,21 +60,6 @@ public class RegularPacketsTest { Assert.assertEquals(startState, !yacht.getSailIn()); } - @Test - public void Test3ArbitraryPacketSentOnPress() throws Exception { - serverThread.startGame(); - SleepThreadMaxDelay(); - Yacht yacht = new ArrayList<>(GameState.getYachts().values()).get(0); - double heading = yacht.getHeading(); - double windDirection = GameState.getWindDirection(); - Yacht testYacht = new Yacht("", 0, "", "", "", ""); - testYacht.setHeading(heading); - testYacht.tackGybe(windDirection); - clientThread.sendBoatAction(BoatAction.TACK_GYBE); - SleepThreadMaxDelay(); - Assert.assertEquals(testYacht.getHeading(), yacht.getHeading(), 1); - } - /** * Give time for processing and packet sending. 200ms listed as absolute maximum for an * acceptable delay. @@ -90,7 +74,7 @@ public class RegularPacketsTest { serverThread.terminate(); clientThread.setSocketToClose(); GameState.setCurrentStage(GameStages.LOBBYING); - SleepThreadMaxDelay(); //Make sure socket is closed. - SleepThreadMaxDelay(); + for (int i = 0; i<20; i++) + SleepThreadMaxDelay(); //Make sure socket is closed and toolkit remade. } }