package ch.zhaw.pm2.racetrack; import ch.zhaw.pm2.racetrack.given.GameSpecification; import ch.zhaw.pm2.racetrack.strategy.*; import java.io.File; import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.List; import static ch.zhaw.pm2.racetrack.PositionVector.Direction; /** * Game controller class, performing all actions to modify the game state. * It contains the logic to move the cars, detect if they are crashed * and if we have a winner. */ public class Game implements GameSpecification { public static final int NO_WINNER = -1; private Track track; private int currentCarIndex; UserInterface userInterface; public Game(UserInterface userInterface) { this.userInterface = userInterface; } public boolean initPhase() throws InvalidTrackFormatException { File folder = new File("tracks"); File[] listOfFiles = folder.listFiles(); if (listOfFiles.length > 0) { List tracks = new ArrayList<>(); for (File file : listOfFiles) { tracks.add(file.getName()); } File selectedTrack = listOfFiles[userInterface.selectOption("Select Track file", tracks)]; selectTrack(selectedTrack); List moveStrategies = new ArrayList<>(); moveStrategies.add("Do not move Strategy"); moveStrategies.add("User Move Strategy"); moveStrategies.add("Move List Strategy"); moveStrategies.add("Path Follow Move Strategy"); for (int i = 0; i < track.getCarCount(); i++) { Car car = track.getCar(i); while (car.getMoveStrategy() == null) { int moveStrategie = userInterface.selectOption( "Select Strategy for Car " + i + " (" + track.getCarId(i) + ")", moveStrategies); switch (moveStrategie + 1) { case 1: selectMoveStrategy(car, new DoNotMoveStrategy()); break; case 2: selectMoveStrategy(car, new UserMoveStrategy(userInterface, i, track.getCarId(i))); break; case 3: String path = ".\\moves\\" + selectedTrack.getName().split("\\.")[0] + "-car-" + track.getCar(i).getID() + ".txt"; try { MoveStrategy moveStrategy = new MoveListStrategy(path); selectMoveStrategy(car, moveStrategy); } catch (FileNotFoundException e) { userInterface.printInformation("There is no MoveList implemented. Choose another Strategy!"); } //TODO: Backslash kompatibel für Linux break; case 4: //TODO: add Arguments selectMoveStrategy(car, new PathFollowerMoveStrategy()); break; } } } return true; } else { userInterface.printInformation("No Trackfile found!"); return false; } } /** * The functionality was taken out of init to automate testing * * @param selectedTrack */ Track selectTrack(File selectedTrack) { try { track = new Track(selectedTrack); return track; } catch (FileNotFoundException | PositionVectorNotValid | InvalidTrackFormatException e) { e.printStackTrace(); } return null; } /** * The functionality was taken out of init to automate testing * * @param car to set the MoveStrategy * @param strategy The movestrategy to set */ void selectMoveStrategy(Car car, MoveStrategy strategy) { car.setMoveStrategy(strategy); } /** * Return the index of the current active car. * Car indexes are zero-based, so the first car is 0, and the last car is getCarCount() - 1. * * @return The zero-based number of the current car */ @Override public int getCurrentCarIndex() { return currentCarIndex; } /** * 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 track.getCarId(carIndex); } /** * Get the position of the specified car. * * @param carIndex The zero-based carIndex number * @return A PositionVector containing the car's current position */ @Override public PositionVector getCarPosition(int carIndex) { return track.getCarPos(carIndex); } /** * 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 track.getCarVelocity(carIndex); } /** * Return the winner of the game. If the game is still in progress, returns NO_WINNER. * * @return The winning car's index (zero-based, see getCurrentCar()), or NO_WINNER if the game is still in progress */ @Override public int getWinner() { if (onlyOneCarLeft()) { return currentCarIndex; } for (int i = 0; i < track.getCarCount(); i++) { if (track.getCar(i).getWinPoints() == 1) { return i; } } return NO_WINNER; } /** * Execute the next turn for the current active car. *

This method changes the current car's velocity and checks on the path to the next position, * if it crashes (car state to crashed) or passes the finish line in the right direction (set winner state).

*

The steps are as follows

*
    *
  1. Accelerate the current car
  2. *
  3. Calculate the path from current (start) to next (end) position * (see {@link Game#calculatePath(PositionVector, PositionVector)})
  4. *
  5. Verify for each step what space type it hits: *
      *
    • TRACK: check for collision with other car (crashed & don't continue), otherwise do nothing
    • *
    • WALL: car did collide with the wall - crashed & don't continue
    • *
    • FINISH_*: car hits the finish line - wins only if it crosses the line in the correct direction
    • *
    *
  6. *
  7. If the car crashed or wins, set its position to the crash/win coordinates
  8. *
  9. If the car crashed, also detect if there is only one car remaining, remaining car is the winner
  10. *
  11. Otherwise move the car to the end position
  12. *
*

The calling method must check the winner state and decide how to go on. If the winner is different * than {@link Game#NO_WINNER}, or the current car is already marked as crashed the method returns immediately.

* * @param acceleration A Direction containing the current cars acceleration vector (-1,0,1) in x and y direction * for this turn */ @Override public void doCarTurn(Direction acceleration) throws PositionVectorNotValid { track.getCar(currentCarIndex).accelerate(acceleration); PositionVector crashPosition = null; List positionList = calculatePath(track.getCarPos(currentCarIndex), track.getCar(currentCarIndex).nextPosition()); for (int i = 0; i < positionList.size(); i++) { if (willCarCrash(currentCarIndex, positionList.get(i))) { if (i == 0) { crashPosition = track.getCarPos(currentCarIndex); } else { crashPosition = positionList.get(i - 1); } break; } } if (crashPosition != null) { track.carDoesCrash(currentCarIndex, crashPosition); } else { calculateWinner(track.getCarPos(currentCarIndex), track.getCar(currentCarIndex).nextPosition(), currentCarIndex); track.moveCar(currentCarIndex); } } public String gamePhase() throws PositionVectorNotValid { while (carsMoving() && getWinner() == NO_WINNER) { userInterface.printTrack(track); Direction direction; direction = track.getCar(currentCarIndex).getMoveStrategy().nextMove(); if (direction == null) { track.getCar(currentCarIndex).setMoveStrategy(new DoNotMoveStrategy()); direction = track.getCar(currentCarIndex).getMoveStrategy().nextMove(); //TODO: Entfernen? }else { doCarTurn(direction); } switchToNextActiveCar(); } userInterface.printTrack(track); int indexWinner = getWinner(); if (indexWinner == NO_WINNER) { return null; } return String.valueOf(track.getCar(indexWinner).getID()); } /** * Switches to the next car who is still in the game. Skips crashed cars. */ @Override public void switchToNextActiveCar() { do { if ((currentCarIndex + 1) == track.getCarCount()) { currentCarIndex = 0; } else { currentCarIndex++; } } while (track.getCar(currentCarIndex).isCrashed()); // TODO: evtl andere Kapselung } /** * Returns all of the grid positions in the path between two positions, for use in determining line of sight. * Determine the 'pixels/positions' on a raster/grid using Bresenham's line algorithm. * (https://de.wikipedia.org/wiki/Bresenham-Algorithmus) * Basic steps are * - Detect which axis of the distance vector is longer (faster movement) * - for each pixel on the 'faster' axis calculate the position on the 'slower' axis. * Direction of the movement has to correctly considered * * @param startPosition Starting position as a PositionVector * @param endPosition Ending position as a PositionVector * @return Intervening grid positions as a List of PositionVector's, including the starting and ending positions. */ @Override public List calculatePath(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; } private void calculateWinner(PositionVector start, PositionVector finish, int carIndex) { List path = calculatePath(start, finish); for (PositionVector point : path) { if (track.getSpaceType(point) != null) { switch (track.getSpaceType(point)) { case FINISH_UP: if (start.getY() < finish.getY()) { track.getCar(carIndex).increaseWinPoints(); } else if (start.getY() > finish.getY()) { track.getCar(carIndex).deductWinPoints(); } break; case FINISH_DOWN: if (start.getY() > finish.getY()) { track.getCar(carIndex).increaseWinPoints(); } else if (start.getY() < finish.getY()) { track.getCar(carIndex).deductWinPoints(); } break; case FINISH_RIGHT: if (start.getX() < finish.getX()) { track.getCar(carIndex).increaseWinPoints(); } else if (start.getX() > finish.getX()) { track.getCar(carIndex).deductWinPoints(); } break; case FINISH_LEFT: if (start.getX() > finish.getX()) { track.getCar(carIndex).increaseWinPoints(); } else if (start.getX() < finish.getX()) { track.getCar(carIndex).increaseWinPoints(); } break; } } } } /** * Does indicate if a car would have a crash with a WALL space or another car at the given position. * * @param carIndex The zero-based carIndex number * @param position A PositionVector of the possible crash position * @return A boolean indicator if the car would crash with a WALL or another car. */ @Override public boolean willCarCrash(int carIndex, PositionVector position) throws PositionVectorNotValid { return track.willCrashAtPosition(carIndex, position); } public boolean onlyOneCarLeft() { int carsLeft = 0; for (int carIndex = 0; carIndex < track.getCarCount(); carIndex++) { if (!track.getCar(carIndex).isCrashed()) { carsLeft++; } } return !(carsLeft > 1); } public boolean carsMoving() { for (int carIndex = 0; carIndex < track.getCarCount(); carIndex++) { if (!(track.getCar(carIndex).isCrashed() || track.getCar(carIndex).getMoveStrategy().getClass() == DoNotMoveStrategy.class)) { return true; } } return false; } }