package ch.zhaw.pm2.racetrack; import ch.zhaw.pm2.racetrack.given.ConfigSpecification; import ch.zhaw.pm2.racetrack.given.TrackSpecification; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Scanner; /** * This class represents the racetrack board. * *

The racetrack board consists of a rectangular grid of 'width' columns and 'height' rows. * The zero point of he grid is at the top left. The x-axis points to the right and the y-axis points downwards.

*

Positions on the track grid are specified using {@link PositionVector} objects. These are vectors containing an * x/y coordinate pair, pointing from the zero-point (top-left) to the addressed space in the grid.

* *

Each position in the grid represents a space which can hold an enum object of type {@link Config.SpaceType}.
* Possible Space types are: *

*

Beside the board the track contains the list of cars, with their current state (position, velocity, crashed,...)

* *

At initialization the track grid data is read from the given track file. The track data must be a * rectangular block of text. Empty lines at the start are ignored. Processing stops at the first empty line * following a non-empty line, or at the end of the file.

*

Characters in the line represent SpaceTypes. The mapping of the Characters is as follows:

* * *

All lines must have the same length, used to initialize the grid width). * Beginning empty lines are skipped. * The tracks ends with the first empty line or the file end.
* An {@link InvalidTrackFormatException} is thrown, if *

* *

The Track can return a String representing the current state of the race (including car positons)

*/ public class Track implements TrackSpecification { public static final char CRASH_INDICATOR = 'X'; private final List track; private final List cars; private final List finishLine; private ConfigSpecification.SpaceType finishTyp; /** * Initializes the Track from the given track File including the cars. * Throws a corresponding error if one of the conditions are not met to build a track. * * @param trackFile Reference to a file containing the track data * @throws FileNotFoundException if the given track file could not be found * @throws InvalidTrackFormatException if the track file contains invalid data (no tracklines, ...) */ public Track(File trackFile) throws FileNotFoundException, InvalidTrackFormatException { track = new ArrayList<>(); cars = new ArrayList<>(); finishLine = new ArrayList<>(); readFile(trackFile); findFinish(); addCars(); } /** * This method reads the File and saves it to the track ArrayList Line by Line * * @param trackFile the File where the track has been documented * @throws FileNotFoundException if the FilePath is invalid. */ private void readFile(File trackFile) throws FileNotFoundException { Scanner scanner = new Scanner(new FileInputStream(trackFile), StandardCharsets.UTF_8); while (scanner.hasNextLine()) { track.add(scanner.nextLine()); } } /** * Goes through the track ArrayList and determines the locations of each car and initializes them at the location. * * @throws InvalidTrackFormatException is thrown if a car is found more than once inside the track. */ private void addCars() throws InvalidTrackFormatException { ConfigSpecification.SpaceType[] spaceTypes = ConfigSpecification.SpaceType.values(); List allSpaceTypesAsChar = new ArrayList<>(); List usedSymbolForCar = new ArrayList<>(); for (ConfigSpecification.SpaceType spaceType : spaceTypes) { allSpaceTypesAsChar.add(spaceType.getValue()); } for (int yPosition = 0; yPosition < track.size(); yPosition++) { String line = track.get(yPosition); for (int xPosition = 0; xPosition < line.length(); xPosition++) { char possibleCarChar = line.charAt(xPosition); if (!allSpaceTypesAsChar.contains(possibleCarChar)) { if (usedSymbolForCar.contains(possibleCarChar)) { throw new InvalidTrackFormatException(); } usedSymbolForCar.add(possibleCarChar); cars.add(new Car(possibleCarChar, new PositionVector(xPosition, yPosition))); } } } } /** * Determines the finish line and saves it in a list, throws an Exception if none is found. * * @throws InvalidTrackFormatException thrown if no finish line is found */ private void findFinish() throws InvalidTrackFormatException { for (int i = 0; i < track.size(); i++) { String line = track.get(i); for (int j = 0; j < line.length(); j++) { if (line.charAt(j) == ConfigSpecification.SpaceType.FINISH_LEFT.getValue() || line.charAt(j) == ConfigSpecification.SpaceType.FINISH_RIGHT.getValue() || line.charAt(j) == ConfigSpecification.SpaceType.FINISH_DOWN.getValue() || line.charAt(j) == ConfigSpecification.SpaceType.FINISH_UP.getValue()) { finishLine.add(new PositionVector(j, i)); } } } if (finishLine.size() == 0) { throw new InvalidTrackFormatException(); } finishTyp = getSpaceType(finishLine.get(0)); for (PositionVector positionVector : finishLine) { if (getSpaceType(positionVector) != finishTyp) { throw new InvalidTrackFormatException(); } } } /** * Method to find the PositionVector of a chosen character * * @param symbol char that we are looking for on the track * @return the PositionVector of the desired char */ private PositionVector findChar(char symbol) { PositionVector vector = null; for (int i = 0; i < track.size(); i++) { String line = track.get(i); for (int j = 0; j < line.length(); j++) { if (line.charAt(j) == symbol) { vector = new PositionVector(j, i); } } } return vector; } /** * Method that places a character at a chosen position * * @param positionVector position where char will be placed * @param symbol char that should be placed at desired position */ private void drawCharOnTrackIndicator(PositionVector positionVector, char symbol) { String line = track.get(positionVector.getY()); line = line.substring(0, positionVector.getX()) + symbol + line.substring(positionVector.getX() + 1); track.remove(positionVector.getY()); track.add(positionVector.getY(), line); } /** * Method that returns the finishline as a List * * @return finishLine List */ public List getFinishLine() { return finishLine; } /** * Returns the whole Track as List of Strings * * @return track as List of Strings */ public List getTrack() { return track; } /** * This Method will update the Car on the track * and will make the Car move to the next position * * @param carIndex representing the current Car */ public void moveCar(int carIndex) { makeCarMoveInTrack(carIndex); //Change position of car getCar(carIndex).move(); } /** * This Method does change the Position of the car inside the track object. * * @param carIndex of the current car */ private void makeCarMoveInTrack(int carIndex) { PositionVector carPositionVector = findChar(getCarId(carIndex)); //Removes the Car at Current Pos drawCharOnTrackIndicator(carPositionVector, ConfigSpecification.SpaceType.TRACK.getValue()); //Redraw finishline if Car was on finish-line Position for (PositionVector finishLinePositionVector : finishLine) { if (finishLinePositionVector.equals(carPositionVector)) { drawCharOnTrackIndicator(carPositionVector, finishTyp.getValue()); } } //Adds Car at new Position carPositionVector = cars.get(carIndex).nextPosition(); drawCharOnTrackIndicator(carPositionVector, cars.get(carIndex).getID()); } /** * This Method will check if the Car would crash at the specific position * * @param positionVector the position to check if the car would crash * @return true if crash otherwise false */ public boolean willCrashAtPosition(int carIndex, PositionVector positionVector) { char charAtPosition = track.get(positionVector.getY()).charAt(positionVector.getX()); if (getCarId(carIndex) == charAtPosition) return false; return !(charAtPosition == ConfigSpecification.SpaceType.TRACK.value || charAtPosition == ConfigSpecification.SpaceType.FINISH_RIGHT.value || charAtPosition == ConfigSpecification.SpaceType.FINISH_LEFT.value || charAtPosition == ConfigSpecification.SpaceType.FINISH_UP.value || charAtPosition == ConfigSpecification.SpaceType.FINISH_DOWN.value); } /** * This Method will mark the Car as crashed inside the track and the car Object. * * @param carIndex of car that will be marked as crashed * @param crashPositionVector of the location of the crash */ public void carDoesCrash(int carIndex, PositionVector crashPositionVector) { PositionVector currentCarPosition = getCarPos(carIndex); drawCharOnTrackIndicator(new PositionVector(currentCarPosition.getX(), currentCarPosition.getY()), ConfigSpecification.SpaceType.TRACK.getValue()); Car car = cars.get(carIndex); car.crash(); car.setPosition(crashPositionVector); drawCharOnTrackIndicator(new PositionVector(crashPositionVector.getX(), crashPositionVector.getY()), CRASH_INDICATOR); } /** * Return the type of space at the given position. * If the location is outside the track bounds, it is considered a wall. * * @param position The coordinates of the position to examine * @return The type of space at the desired position */ @Override public Config.SpaceType getSpaceType(PositionVector position) { char charAtPosition = track.get(position.getY()).charAt(position.getX()); ConfigSpecification.SpaceType[] spaceTypes = ConfigSpecification.SpaceType.values(); for (ConfigSpecification.SpaceType spaceType : spaceTypes) { if (spaceType.getValue() == charAtPosition) { return spaceType; } } return ConfigSpecification.SpaceType.WALL; } /** * Return the number of cars that are located in a track * * @return number of cars as int */ @Override public int getCarCount() { return cars.size(); } /** * Get instance of specified car. * * @param carIndex The zero-based carIndex number * @return The car instance at the given index */ @Override public Car getCar(int carIndex) { return cars.get(carIndex); } /** * Get the id of the specified car. * * @param carIndex The zero-based carIndex number * @return A char containing the id of the car */ @Override public char getCarId(int carIndex) { return cars.get(carIndex).getID(); } /** * Get the position of the specified car. * Returns Null if carIndex not valid * * @param carIndex The zero-based carIndex number * @return A PositionVector containing the car's current position */ @Override public PositionVector getCarPos(int carIndex) { return findChar(cars.get(carIndex).getID()); } /** * Get the velocity of the specified car. * * @param carIndex The zero-based carIndex number * @return A PositionVector containing the car's current velocity */ @Override public PositionVector getCarVelocity(int carIndex) { return cars.get(carIndex).getVelocity(); } /** * Gets character at the given position. * If there is a crashed car at the position, {@link #CRASH_INDICATOR} is returned. * * @param y position Y-value * @param x position X-vlaue * @param currentSpace char to return if no car is at position (x,y) * @return character representing position (x,y) on the track */ @Override public char getCharAtPosition(int y, int x, Config.SpaceType currentSpace) { char charAtPos = track.get(y).charAt(x); PositionVector positionVector = new PositionVector(x, y); for (Car car : cars) { if (charAtPos == car.getID()) { return charAtPos; } } if (positionVector.equals(findChar(CRASH_INDICATOR))) return CRASH_INDICATOR; return currentSpace.getValue(); } /** * Determines all points that lie between the two position vectors including the endpoint VectorPosition using the Bresenham algorithm. * * @param startPosition PositionVector of the finish coordinate * @param endPosition PositionVector of the start coordinate * @return ArrayList containing PositionVectors of all position that are between the start and finish including the finish position. */ public ArrayList calculatePointsOnPath(PositionVector startPosition, PositionVector endPosition) { ArrayList pathList = new ArrayList<>(); // Use Bresenham's algorithm to determine positions. int x = startPosition.getX(); int y = startPosition.getY(); // Relative Distance (x & y-axis) between end- and starting position int diffX = endPosition.getX() - startPosition.getX(); int diffY = endPosition.getY() - startPosition.getY(); // Absolute distance (x & y-axis) between end- and starting position int distX = Math.abs(diffX); int distY = Math.abs(diffY); // Direction of vector on x & y axis (-1: to left/down, 0: none, +1 : to right/up) int dirX = Integer.signum(diffX); int dirY = Integer.signum(diffY); // Determine which axis is the fast direction and set parallel/diagonal step values int parallelStepX, parallelStepY; int diagonalStepX, diagonalStepY; int distanceSlowAxis, distanceFastAxis; if (distX > distY) { // x-axis is the 'fast' direction parallelStepX = dirX; parallelStepY = 0; // parallel step only moves in x direction diagonalStepX = dirX; diagonalStepY = dirY; // diagonal step moves in both directions distanceSlowAxis = distY; distanceFastAxis = distX; } else { // y-axis is the 'fast' direction parallelStepX = 0; parallelStepY = dirY; // parallel step only moves in y direction diagonalStepX = dirX; diagonalStepY = dirY; // diagonal step moves in both directions distanceSlowAxis = distX; distanceFastAxis = distY; } int error = distanceFastAxis / 2; for (int step = 0; step < distanceFastAxis; step++) { error -= distanceSlowAxis; if (error < 0) { error += distanceFastAxis; // correct error value to be positive again // step into slow direction; diagonal step x += diagonalStepX; y += diagonalStepY; } else { // step into fast direction; parallel step x += parallelStepX; y += parallelStepY; } pathList.add(new PositionVector(x, y)); } return pathList; } /** * This method will check if a car is passing the finish line. * If the car is passing the finish line in the wrong direction, the car will lose a winpoint. * If the car is passing the finish line in the correct direction, the car will gain a winpoint. * * @param start the start position of the car * @param finish the expected finish position of the car after the move * @return Number of new winpoints for the current player. */ public int calculateNewWinPoints(PositionVector start, PositionVector finish) { List path = calculatePointsOnPath(start, finish); for (PositionVector point : path) { switch (getSpaceType(point)) { case FINISH_UP: if (start.getY() > finish.getY()) { return 1; } else if (start.getY() < finish.getY()) { return -1; } break; case FINISH_DOWN: if (start.getY() < finish.getY()) { return 1; } else if (start.getY() > finish.getY()) { return -1; } break; case FINISH_RIGHT: if (start.getX() < finish.getX()) { return 1; } else if (start.getX() > finish.getX()) { return -1; } break; case FINISH_LEFT: if (start.getX() > finish.getX()) { return 1; } else if (start.getX() < finish.getX()) { return -1; } break; } } return 0; } /** * Return a String representation of the track, including the car locations. * * @return A String representation of the track */ @Override public String toString() { StringBuilder str = new StringBuilder(); for (String line : track) str.append(line).append("\n"); return str.toString(); } }