66. [M] As Gemma, I'd like boats and marks to act as barriers to movement so that another player cannot beat me by just ignoring marks and other boats on the course. Note: as boats and marks don't start in overlapped positions, we are only concerned with collisions. Acceptance criteria: - No boat can pass within an agreed distance of the centre of another boat or mark - Boats that hit other objects are repelled twice the agreed distance in the opposite direction to their current heading See merge request !27main
commit
96acb6251e
@ -0,0 +1,52 @@
|
||||
package mock.model.collider;
|
||||
|
||||
import shared.model.Bearing;
|
||||
import shared.model.Boat;
|
||||
import shared.model.GPSCoordinate;
|
||||
import shared.model.Locatable;
|
||||
|
||||
import java.util.Observable;
|
||||
|
||||
/**
|
||||
* Interface for all objects sensitive to collision in a race.
|
||||
*/
|
||||
public abstract class Collider extends Observable implements Locatable {
|
||||
/**
|
||||
* Indicates whether a ray cast from a boat to a target collider is within the specified length.
|
||||
* @param boat potentially colliding with target
|
||||
* @param distance distance for valid collision
|
||||
* @return whether or not a collision has occurred
|
||||
*/
|
||||
public boolean rayCast(Boat boat, double distance) {
|
||||
double actualDistance = GPSCoordinate.calculateDistanceMeters(boat.getPosition(), this.getPosition());
|
||||
// Compass direction of collider
|
||||
Bearing absolute = Bearing.fromAzimuth(GPSCoordinate.calculateAzimuth(boat.getPosition(), this.getPosition()));
|
||||
// Direction of collider from heading
|
||||
Bearing relative = Bearing.fromDegrees(absolute.degrees() - boat.getBearing().degrees());
|
||||
|
||||
if(actualDistance <= distance) {
|
||||
Collision collision = new Collision(relative, distance);
|
||||
// Notify object of collision
|
||||
onCollisionEnter(boat, collision);
|
||||
// Notify observers of collision
|
||||
notifyObservers(collision);
|
||||
this.setChanged();
|
||||
|
||||
return true;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether a ray cast from a boat to a target collider triggers a collision. Distance is set by the object.
|
||||
* @param boat potentially colliding with target
|
||||
* @return whether or not a collision has occurred
|
||||
*/
|
||||
public abstract boolean rayCast(Boat boat);
|
||||
|
||||
/**
|
||||
* Handle a collision event
|
||||
* @param collider Boat that is colliding
|
||||
* @param e details of collision
|
||||
*/
|
||||
public abstract void onCollisionEnter(Boat collider, Collision e);
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
package mock.model.collider;
|
||||
|
||||
import shared.model.Boat;
|
||||
import shared.model.GPSCoordinate;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Registry for all Collider objects in a MockRace. Wraps the Collider interface as part of a Composite Pattern.
|
||||
*/
|
||||
public class ColliderRegistry extends Collider implements Observer {
|
||||
/**
|
||||
* List of all registered Colliders
|
||||
*/
|
||||
private List<Collider> colliders;
|
||||
|
||||
/**
|
||||
* Default constructor for ColliderRegistry
|
||||
*/
|
||||
public ColliderRegistry() {
|
||||
this.colliders = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void addCollider(Collider collider) {
|
||||
collider.addObserver(this);
|
||||
colliders.add(collider);
|
||||
}
|
||||
|
||||
public void addAllColliders(Collection<? extends Collider> colliders) {
|
||||
for(Collider collider: colliders) addCollider(collider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rayCast(Boat boat) {
|
||||
for(Collider collider: colliders) {
|
||||
if(collider.rayCast(boat)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCollisionEnter(Boat collider, Collision e) {}
|
||||
|
||||
@Override
|
||||
public GPSCoordinate getPosition() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPosition(GPSCoordinate position) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire onCollisionEnter when collision bubbles up from registered colliders.
|
||||
* @param o object collided with
|
||||
* @param arg parameters of the collision
|
||||
*/
|
||||
@Override
|
||||
public void update(Observable o, Object arg) {
|
||||
Collision collision = (Collision)arg;
|
||||
|
||||
notifyObservers(collision);
|
||||
this.setChanged();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package mock.model.collider;
|
||||
|
||||
import shared.model.Bearing;
|
||||
|
||||
/**
|
||||
* Data structure for holding collision details for ray casting and event handling.
|
||||
*/
|
||||
public class Collision {
|
||||
/**
|
||||
* Bearing from boat heading to target
|
||||
*/
|
||||
private Bearing bearing;
|
||||
/**
|
||||
* Distance from boat centre to target centre
|
||||
*/
|
||||
private double distance;
|
||||
|
||||
/**
|
||||
* Constructor for Collision structure
|
||||
* @param bearing from boat heading to target
|
||||
* @param distance from boat centre to target centre
|
||||
*/
|
||||
public Collision(Bearing bearing, double distance) {
|
||||
this.bearing = bearing;
|
||||
this.distance = distance;
|
||||
}
|
||||
|
||||
public Bearing getBearing() {
|
||||
return bearing;
|
||||
}
|
||||
|
||||
public double getDistance() {
|
||||
return distance;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package network;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Common source of ack numbers for all messages
|
||||
*/
|
||||
public class AckSequencer {
|
||||
/**
|
||||
* Generator for ack numbers
|
||||
*/
|
||||
private static AtomicInteger ackNum = new AtomicInteger(0);
|
||||
|
||||
/**
|
||||
* Retrieve next ack number
|
||||
* @return next ack number
|
||||
*/
|
||||
public static int getNextAckNum() {
|
||||
return ackNum.getAndIncrement();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package network.MessageDecoders;
|
||||
|
||||
import network.Exceptions.InvalidMessageException;
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.Enums.YachtEventEnum;
|
||||
import network.Messages.YachtEvent;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static network.Utils.ByteConverter.bytesToInt;
|
||||
import static network.Utils.ByteConverter.bytesToLong;
|
||||
import static network.Utils.ByteConverter.bytesToShort;
|
||||
|
||||
/**
|
||||
* Decodes {@link YachtEvent} messages.
|
||||
*/
|
||||
public class YachtEventCodeDecoder implements MessageDecoder {
|
||||
private YachtEvent message;
|
||||
|
||||
@Override
|
||||
public AC35Data decode(byte[] encodedMessage) throws InvalidMessageException {
|
||||
// Deserialise message
|
||||
byte[] timestamp = Arrays.copyOfRange(encodedMessage, 1, 7);
|
||||
byte[] ackNum = Arrays.copyOfRange(encodedMessage, 7, 9);
|
||||
byte[] raceID = Arrays.copyOfRange(encodedMessage, 9, 13);
|
||||
byte[] sourceID = Arrays.copyOfRange(encodedMessage, 13, 17);
|
||||
byte[] incidentID = Arrays.copyOfRange(encodedMessage, 17, 21);
|
||||
byte eventID = encodedMessage[21];
|
||||
|
||||
// Unpack bytes into YachtEvent
|
||||
this.message = new YachtEvent(
|
||||
bytesToLong(timestamp),
|
||||
bytesToShort(ackNum),
|
||||
bytesToInt(raceID),
|
||||
bytesToInt(sourceID),
|
||||
bytesToInt(incidentID),
|
||||
YachtEventEnum.fromByte(eventID)
|
||||
);
|
||||
|
||||
// Return YachtEvent
|
||||
return message;
|
||||
}
|
||||
|
||||
public YachtEvent getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package network.MessageEncoders;
|
||||
|
||||
import network.Exceptions.InvalidMessageException;
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.YachtEvent;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static network.Utils.ByteConverter.intToBytes;
|
||||
import static network.Utils.ByteConverter.longToBytes;
|
||||
|
||||
/**
|
||||
* Encodes a {@link YachtEvent} message.
|
||||
*/
|
||||
public class YachtEventCodeEncoder implements MessageEncoder {
|
||||
@Override
|
||||
public byte[] encode(AC35Data message) throws InvalidMessageException {
|
||||
// Downcast message
|
||||
YachtEvent yachtEvent = (YachtEvent)message;
|
||||
|
||||
// Serialise message
|
||||
byte messageVersion = 0b10;
|
||||
byte[] timestamp = longToBytes(yachtEvent.getCurrentTime(), 6);
|
||||
byte[] ackNum = intToBytes(yachtEvent.getAckNum(), 2);
|
||||
byte[] raceID = intToBytes(yachtEvent.getRaceID());
|
||||
byte[] sourceID = intToBytes(yachtEvent.getSourceID());
|
||||
byte[] incidentID = intToBytes(yachtEvent.getIncidentID());
|
||||
byte eventID = yachtEvent.getYachtEvent().getValue();
|
||||
|
||||
// Pack bytes into string
|
||||
ByteBuffer yachtEventMessage = ByteBuffer.allocate(22);
|
||||
yachtEventMessage.put(messageVersion);
|
||||
yachtEventMessage.put(timestamp);
|
||||
yachtEventMessage.put(ackNum);
|
||||
yachtEventMessage.put(raceID);
|
||||
yachtEventMessage.put(sourceID);
|
||||
yachtEventMessage.put(incidentID);
|
||||
yachtEventMessage.put(eventID);
|
||||
|
||||
// Return byte string
|
||||
return yachtEventMessage.array();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package network.Messages.Enums;
|
||||
|
||||
/**
|
||||
* Yacht event codes
|
||||
*/
|
||||
public enum YachtEventEnum {
|
||||
NOT_AN_EVENT(-1),
|
||||
COLLISION(1);
|
||||
|
||||
private byte value;
|
||||
|
||||
YachtEventEnum(int value) { this.value = (byte)value; }
|
||||
|
||||
public byte getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static YachtEventEnum fromByte(byte value) {
|
||||
switch(value) {
|
||||
case 1: return COLLISION;
|
||||
default: return NOT_AN_EVENT;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package network.Messages;
|
||||
|
||||
import network.Messages.Enums.MessageType;
|
||||
import network.Messages.Enums.YachtEventEnum;
|
||||
|
||||
/**
|
||||
* Represents a Yacht Event Code message defined in the AC35 spec, with Event IDs amended for the purposes of
|
||||
* a game.
|
||||
*/
|
||||
public class YachtEvent extends AC35Data {
|
||||
private long currentTime;
|
||||
private int ackNum;
|
||||
private int raceID;
|
||||
private int sourceID;
|
||||
private int incidentID;
|
||||
private YachtEventEnum yachtEvent;
|
||||
|
||||
public YachtEvent(long currentTime, int ackNum, int raceID, int sourceID, int incidentID, YachtEventEnum yachtEvent) {
|
||||
super(MessageType.YACHTEVENTCODE);
|
||||
this.currentTime = currentTime;
|
||||
this.ackNum = ackNum;
|
||||
this.raceID = raceID;
|
||||
this.sourceID = sourceID;
|
||||
this.incidentID = incidentID;
|
||||
this.yachtEvent = yachtEvent;
|
||||
}
|
||||
|
||||
public YachtEventEnum getYachtEvent() {
|
||||
return yachtEvent;
|
||||
}
|
||||
|
||||
public int getSourceID() {
|
||||
return sourceID;
|
||||
}
|
||||
|
||||
public int getIncidentID() {
|
||||
return incidentID;
|
||||
}
|
||||
|
||||
public long getCurrentTime() {
|
||||
return currentTime;
|
||||
}
|
||||
|
||||
public int getAckNum() {
|
||||
return ackNum;
|
||||
}
|
||||
|
||||
public int getRaceID() {
|
||||
return raceID;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package shared.model;
|
||||
|
||||
/**
|
||||
* Created by cbt24 on 16/08/17.
|
||||
*/
|
||||
public interface Locatable {
|
||||
GPSCoordinate getPosition();
|
||||
void setPosition(GPSCoordinate position);
|
||||
}
|
||||
@ -1,174 +1,35 @@
|
||||
package mock.model;
|
||||
|
||||
import mock.dataInput.PolarParser;
|
||||
import mock.exceptions.InvalidPolarFileException;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import shared.model.Bearing;
|
||||
import shared.model.CompoundMark;
|
||||
import shared.model.GPSCoordinate;
|
||||
import shared.model.Mark;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class MockBoatTest {
|
||||
private MockBoat boat;
|
||||
private Mark near;
|
||||
private Mark far;
|
||||
|
||||
/**
|
||||
* boat made for testing
|
||||
*/
|
||||
private MockBoat firstTestBoat;
|
||||
|
||||
private Mark markToTest;
|
||||
private Mark markToTest2;
|
||||
|
||||
private GPSCoordinate highGPS;
|
||||
private GPSCoordinate middleGPS;
|
||||
private GPSCoordinate lowGPS;
|
||||
|
||||
/**
|
||||
* Creates the Polars object for the tests.
|
||||
*/
|
||||
@Before
|
||||
public void setUp() {
|
||||
//Read in polars.
|
||||
try {
|
||||
//Parse data file.
|
||||
Polars polars = PolarParser.parse("mock/polars/acc_polars.csv");
|
||||
|
||||
firstTestBoat = new MockBoat(1, "test", "NZ", polars);
|
||||
highGPS = new GPSCoordinate(32.296577, -64.854000);
|
||||
middleGPS = new GPSCoordinate(32.292500, -64.854000);
|
||||
lowGPS = new GPSCoordinate(32.290000, -64.854000);
|
||||
markToTest = new Mark(1, "test MARK", middleGPS);
|
||||
markToTest2 = new Mark(2, "test MARK2", middleGPS);
|
||||
}
|
||||
catch (InvalidPolarFileException e) {
|
||||
fail("Couldn't parse polar file.");
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////Mark Higher////////////////////////////////
|
||||
|
||||
/**
|
||||
* Tests if the boat is lower than the mark that the port side method works if
|
||||
* boat is facing east
|
||||
*/
|
||||
@Test
|
||||
public void testIsPortSide() {
|
||||
firstTestBoat.setBearing(Bearing.fromDegrees(90));
|
||||
firstTestBoat.setCurrentPosition(lowGPS);
|
||||
markToTest.setPosition(highGPS);
|
||||
|
||||
assertEquals(firstTestBoat.isPortSide(markToTest), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the boat is lower than the mark that the port side method works if
|
||||
* boat is facing west
|
||||
*/
|
||||
@Test
|
||||
public void testIsPortSideWrong() {
|
||||
firstTestBoat.setBearing(Bearing.fromDegrees(270));
|
||||
firstTestBoat.setCurrentPosition(lowGPS);
|
||||
markToTest.setPosition(highGPS);
|
||||
|
||||
assertEquals(firstTestBoat.isPortSide(markToTest), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the boat is lower than the mark that the starboard side method works if
|
||||
* boat is facing east
|
||||
*/
|
||||
@Test
|
||||
public void testIsStarboardSideWrong() {
|
||||
firstTestBoat.setBearing(Bearing.fromDegrees(90));
|
||||
firstTestBoat.setCurrentPosition(lowGPS);
|
||||
markToTest.setPosition(highGPS);
|
||||
|
||||
assertEquals(firstTestBoat.isStarboardSide(markToTest), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the boat is lower than the mark that the starboard side method works if
|
||||
* boat is facing west
|
||||
*/
|
||||
@Test
|
||||
public void testIsStarboardSide() {
|
||||
firstTestBoat.setBearing(Bearing.fromDegrees(270));
|
||||
firstTestBoat.setCurrentPosition(lowGPS);
|
||||
markToTest.setPosition(highGPS);
|
||||
|
||||
assertEquals(firstTestBoat.isStarboardSide(markToTest), true);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////Mark Lower////////////////////////////////
|
||||
boat = new MockBoat(0, "Bob", "NZ", null);
|
||||
boat.setPosition(new GPSCoordinate(0,0));
|
||||
boat.setBearing(Bearing.fromDegrees(180));
|
||||
|
||||
/**
|
||||
* Tests if the boat is higher than the mark that the port side method works if
|
||||
* boat is facing east
|
||||
*/
|
||||
@Test
|
||||
public void testIsPortSideHigherWrong() {
|
||||
firstTestBoat.setBearing(Bearing.fromDegrees(90));
|
||||
firstTestBoat.setCurrentPosition(highGPS);
|
||||
markToTest.setPosition(lowGPS);
|
||||
|
||||
assertEquals(firstTestBoat.isPortSide(markToTest), false);
|
||||
near = new Mark(0, "Near", new GPSCoordinate(-.0001, 0));
|
||||
far = new Mark(0, "Far", new GPSCoordinate(.001, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the boat is higher than the mark that the port side method works if
|
||||
* boat is facing west
|
||||
*/
|
||||
@Test
|
||||
public void testIsPortSideHigher() {
|
||||
firstTestBoat.setBearing(Bearing.fromDegrees(270));
|
||||
firstTestBoat.setCurrentPosition(highGPS);
|
||||
markToTest.setPosition(lowGPS);
|
||||
|
||||
assertEquals(firstTestBoat.isPortSide(markToTest), true);
|
||||
public void nearMarkWithin100m() {
|
||||
assertTrue(near.rayCast(boat, 100));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the boat is higher than the mark that the starboard side method works if
|
||||
* boat is facing east
|
||||
*/
|
||||
@Test
|
||||
public void testIsStarboardSideHigher() {
|
||||
firstTestBoat.setBearing(Bearing.fromDegrees(90));
|
||||
firstTestBoat.setCurrentPosition(highGPS);
|
||||
markToTest.setPosition(lowGPS);
|
||||
|
||||
assertEquals(firstTestBoat.isStarboardSide(markToTest), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the boat is higher than the mark that the starboard side method works if
|
||||
* boat is facing west
|
||||
*/
|
||||
@Test
|
||||
public void testIsStarboardSideHigherWrong() {
|
||||
firstTestBoat.setBearing(Bearing.fromDegrees(270));
|
||||
firstTestBoat.setCurrentPosition(highGPS);
|
||||
markToTest.setPosition(lowGPS);
|
||||
|
||||
assertEquals(firstTestBoat.isStarboardSide(markToTest), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a boat is between a gate
|
||||
*/
|
||||
@Test
|
||||
public void testIsBetweenGate(){
|
||||
markToTest.setPosition(highGPS);
|
||||
markToTest2.setPosition(lowGPS);
|
||||
CompoundMark testGate = new CompoundMark(1, "test GATE", markToTest, markToTest2);
|
||||
|
||||
firstTestBoat.setCurrentPosition(middleGPS);
|
||||
|
||||
assertEquals(firstTestBoat.isBetweenGate(testGate), true);
|
||||
|
||||
public void farMarkBeyond100m() {
|
||||
assertFalse(far.rayCast(boat, 100));
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
package network.MessageDecoders;
|
||||
|
||||
import network.MessageEncoders.RaceVisionByteEncoder;
|
||||
import network.Messages.Enums.YachtEventEnum;
|
||||
import network.Messages.YachtEvent;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.testng.Assert.*;
|
||||
|
||||
/**
|
||||
* Tests for the YachtEvent decoder and encoder
|
||||
*/
|
||||
public class YachtEventCodeDecoderTest {
|
||||
private YachtEvent decodedMessage;
|
||||
private YachtEvent originalMessage;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
|
||||
originalMessage = new YachtEvent(
|
||||
timestamp,
|
||||
55,
|
||||
35,
|
||||
0,
|
||||
1,
|
||||
YachtEventEnum.COLLISION
|
||||
);
|
||||
|
||||
byte[] encodedMessage = RaceVisionByteEncoder.encode(originalMessage);
|
||||
|
||||
YachtEventCodeDecoder testDecoder = new YachtEventCodeDecoder();
|
||||
testDecoder.decode(encodedMessage);
|
||||
decodedMessage = testDecoder.getMessage();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodingEqualsOriginal() {
|
||||
assertEquals(originalMessage.getCurrentTime(), decodedMessage.getCurrentTime());
|
||||
assertEquals(originalMessage.getAckNum(), decodedMessage.getAckNum());
|
||||
assertEquals(originalMessage.getRaceID(), decodedMessage.getRaceID());
|
||||
assertEquals(originalMessage.getSourceID(), decodedMessage.getSourceID());
|
||||
assertEquals(originalMessage.getIncidentID(), decodedMessage.getIncidentID());
|
||||
assertEquals(originalMessage.getYachtEvent(), decodedMessage.getYachtEvent());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue