12
\$\begingroup\$

Racetrack:

The August community challenge is to implement a program that plays the Racetrack game. Each player starts with an integer position on a square grid. On each turn, the current player can accelerate by -1, 0, or 1 unit in each direction. The first player to reach the finish line wins.

This is a Java implementation of the challenge. Any feedback on any aspect of the program would be appreciated.

Due to the length of this post, the PathFinder and PathFollower classes will be posted separately: Racetrack pathfinding and path following.

I'm sorry about the long lines. I was following a margin line, but I just now realized how far out it was set (120 columns).


Racetrack-Nogui.java

This file has the main function. It loads a track file, feeds the file into a new Track instance, initializes any computer controled players, and drives the gameplay turn by turn. Ideally, I should be able to change the user interface just by replacing this file.

package com.erichamion.racetrack;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.util.*;

public class RacetrackNoGui {

    private static final Map<Character, GridPoint> KEYMAP = new HashMap<>();
    private static final Scanner STDIN = new Scanner(System.in);
    private static final Map<Integer, PathFollower> mComputerPlayers = new HashMap<>();

    static {
        KEYMAP.put('1', new GridPoint(1, -1));
        KEYMAP.put('2', new GridPoint(1, 0));
        KEYMAP.put('3', new GridPoint(1, 1));
        KEYMAP.put('4', new GridPoint(0, -1));
        KEYMAP.put('5', new GridPoint(0, 0));
        KEYMAP.put('6', new GridPoint(0, 1));
        KEYMAP.put('7', new GridPoint(-1, -1));
        KEYMAP.put('8', new GridPoint(-1, 0));
        KEYMAP.put('9', new GridPoint(-1, 1));
    }



    public static void main(String[] args) {
        String filename = null;
        List<Integer> playerIndices = new ArrayList<>();

        for (String arg : args) {
            if (arg.length() == 1 && arg.charAt(0) >= '1' && arg.charAt(0) <= '9') {
                playerIndices.add(Integer.parseInt(arg) - 1);
            } else {
                filename = arg;
            }
        }

        if (filename == null) {
            System.err.println("No filename given\n");
            printUsage(System.err);
            return;
        }

        Track track;
        try {
            track = new Track(new Scanner(new File(filename)));
        } catch (InvalidTrackFormatException e) {
            System.err.println(e.getMessage());
            return;
        } catch (FileNotFoundException e) {
            System.err.println("Could not find file '" + filename + "'");
            return;
        }

        for (Integer playerIndex : playerIndices) {
            if (playerIndex >= track.getPlayerCount()) continue;

            PathFinder playerFinder = new PathFinder(track, playerIndex);
            PathFollower playerFollower = new PathFollower(track, playerFinder, playerIndex);
            mComputerPlayers.put(playerIndex, playerFollower);
        }


        runTextGame(track);
    }

    private static void printUsage(final PrintStream outStream) {
        outStream.println("Usage:");
        outStream.println("    <command> [n1 [n2...]] <filename>");
        outStream.println("Where n1, n2, etc. are player numbers 1-9 for computer control,");
        outStream.println("and <filename> is the path to a track file to load.");
        outStream.println("");
        outStream.println("Example: <command> 2 4 tracks/mytrack.txt");
        outStream.println("    Loads the track file 'tracks/mytrack.txt', and (as long as the track");
        outStream.println("    is for at least 4 players) designates players 2 and 4 as computer");
        outStream.println("    controlled. All other players are keyboard controlled.");
    }

    private static void printDirections() {
        final String outStr = "Directions are based on the number pad:\n"
                + "7 8 9    7=up-left,   8=up,              9=up-right\n"
                + "4 5 6    4=left,      5=no acceleration, 6=right\n"
                + "1 2 3    1=down-left, 2=down,            3=down-right\n"
                + "\n"
                + "h for help\n"
                + "t to show track\n";
        System.out.println(outStr);
    }

    private static GridPoint getTextInput(final String prompt, final Track track) {
        GridPoint result = null;
        do {
            System.out.print(prompt + ": ");
            String line = STDIN.nextLine();
            if (line.length() > 0) {
                char inputChar = line.charAt(0);
                if (inputChar == 'h') {
                    printDirections();
                } else if (inputChar == 't') {
                    System.out.println(track.toString());
                } else if (KEYMAP.containsKey(inputChar)) {
                    result = new GridPoint(KEYMAP.get(inputChar));
                }
            }
        } while (result == null);

        return result;
    }

    private static void runTextGame(final Track track) {
        while (track.getWinner() == Track.NO_WINNER) {
            System.out.println(track.toString());
            int currentPlayer = track.getCurrentPlayer();
            System.out.println("\nPLAYER " + (currentPlayer + 1) + ":");
            PathFollower follower = mComputerPlayers.get(currentPlayer);
            GridPoint acceleration;
            if (follower == null) {
                // Get human input
                acceleration = getTextInput("Acceleration direction (h for help)", track);
            } else {
                // Computer player
                System.out.print("Press Enter to continue.");
                STDIN.nextLine();
                acceleration = follower.getMove();
            }
            track.doPlayerTurn(acceleration);
        }
        System.out.println(track.toString());
        System.out.println();
        System.out.println("Player " + (track.getWinner() + 1) + " WINS!!");
    }

}

Track.java

The Track contains the game logic. It holds the grid that determines which spaces are open for movement, which ones are walls into which players can crash, and which ones are finish lines. The Track also holds all the Players and coordinates their movements.

package com.erichamion.racetrack;

import java.util.*;

/**
 * Created by me on 8/14/15.
 */
public class Track {
    public static final int MAX_PLAYERS = 9;
    public static final int NO_WINNER = -1;

    private static final char CRASH_INDICATOR = 'X';

    private List<Player> mPlayers = new ArrayList<>();
    private int mWidth = 0;
    private int mHeight = 0;
    private List<List<SpaceType>> mGrid = new ArrayList<>();
    private int mCurrentPlayer = 0;
    private int mWinner = NO_WINNER;


    public enum SpaceType {
        WALL('#'),
        TRACK(' '),
        FINISH_UP('^'),
        FINISH_DOWN('v'),
        FINISH_LEFT('<'),
        FINISH_RIGHT('>');

        private final char value;

        SpaceType(final char c) {
            value = c;
        }


    }


    /**
     * Initialize a Track from an input source.
     * @param scanner A java.util.Scanner connected to an input source
     *                that holds the track data. Track data must be a
     *                rectangular grid 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 stream. The first character in the first
     *                non-empty line is considered a wall. A space
     *                character (' ') is open track. Any of '<', '>', '^',
     *                or 'v' represent a finish line and indicate the
     *                direction the car needs to be moving in order to
     *                successfully cross. Any other character indicates
     *                the starting position for a car, and there must be
     *                between 1 and MAX_PLAYERS of these (one for each
     *                player - either the same or different characters).
     * @throws InvalidTrackFormatException
     */
    public Track(final Scanner scanner) throws InvalidTrackFormatException {
        char borderChar = '\0';
        while (scanner.hasNextLine()) {
            String currentLine = scanner.nextLine();
            int lineLength = currentLine.length();
            if (lineLength == 0) {
                if (mHeight == 0) {
                    continue;
                } else {
                    break;
                }
            }

            if (mWidth == 0) {
                mWidth = lineLength;
            } else {
                if (lineLength != mWidth) {
                    throw new InvalidTrackFormatException("Track does not have a consistent width");
                }
            }

            if (borderChar == '\0') {
                borderChar = currentLine.charAt(0);
            }

            addGridRow(currentLine, borderChar);
        }

        // Final sanity checks
        if (mHeight == 0) throw new InvalidTrackFormatException("No track data supplied");
        if (mPlayers.size() == 0) throw new InvalidTrackFormatException("No player positions");

        mCurrentPlayer = 0;
    }

    /**
     * Return a String representation of the track, including the
     * player locations.
     * @return A String representation of the track
     */
    public String toString() {
        StringBuilder result = new StringBuilder();
        for (int rowIndex = 0; rowIndex < mGrid.size(); rowIndex++) {
            List<SpaceType> currentRow = mGrid.get(rowIndex);
            for (int colIndex = 0; colIndex < currentRow.size(); colIndex++) {
                SpaceType currentSpace = currentRow.get(colIndex);
                boolean hasPlayer = false;
                for (int playerNum = 0; playerNum < mPlayers.size(); playerNum++) {
                    Player player = mPlayers.get(playerNum);
                    if (player.getPos().getCol() == colIndex && player.getPos().getRow() == rowIndex) {
                        hasPlayer = true;
                        result.append(player.isCrashed() ? CRASH_INDICATOR : Integer.toString(playerNum + 1));

                        // Only put one player indicator in a given space
                        break;
                    }
                }
                if (!hasPlayer) {
                    result.append(currentSpace.value);
                }
            }
            result.append('\n');
        }

        return result.toString();
    }

    /**
     * Return the width (number of columns) of the track grid.
     * @return Width of the track grid
     */
    public int getWidth() {
        return mWidth;
    }

    /**
     * Return the height (number of rows) of the track grid.
     * @return Height of the track grid
     */
    public int getHeight() {
        return mHeight;
    }

    /**
     * Return the number of players.
     * @return Number of players
     */
    public int getPlayerCount() {
        return mPlayers.size();
    }

    /**
     * Return the current player. Player numbers are zero-based, so the
     * first player is 0, and the last player is getPlayerCount() - 1.
     * @return The zero-based number of the current player
     */
    public int getCurrentPlayer() {
        return mCurrentPlayer;
    }

    /**
     * Find the position of the specified player.
     * @param player The zero-based player number
     * @return A GridPoint containing the player's current position
     */
    public GridPoint getPlayerPos(final int player) {
        return mPlayers.get(player).getPos();
    }

    /**
     * Find the velocity of the specified player.
     * @param player The zero-based player number
     * @return A GridPoint containing the player's current velocity
     */
    public GridPoint getPlayerVelocity(final int player) {
        return mPlayers.get(player).getVelocity();
    }

    /**
     * Return the winner of the game. If the game is still in progress,
     * returns NO_WINNER.
     * @return The winning player (zero-based, see getCurrentPlayer()),
     * or NO_WINNER if the game is still in progress
     */
    public int getWinner() {
        return mWinner;
    }

    /**
     * Accelerate the current player, and update the track state.
     * @param acceleration The current player's acceleration in each
     *                     direction
     */
    public void doPlayerTurn(final GridPoint acceleration) {
        Player player = mPlayers.get(mCurrentPlayer);
        if (player.isCrashed() || mWinner != NO_WINNER) return;

        player.accelerate(acceleration);
        moveCurrentPlayer();

        if (player.isCrashed()) {
            int winCandidate = -1;
            for (int i = 0; i < mPlayers.size(); i++) {
                if (!mPlayers.get(i).isCrashed()) {
                    if (winCandidate == -1) {
                        winCandidate = i;
                    } else {
                        // More than one uncrashed players. Can't declare
                        // a winner.
                        winCandidate = -2;
                        break;
                    }
                }
            }
            if (winCandidate >= 0) {
                mWinner = winCandidate;
            }
        }

        mCurrentPlayer = getNextPlayer();
    }


    /**
     * Gets the next player who is still in the game. Skips crashed
     * players.
     * @return The next active player
     */
    private int getNextPlayer() {
        int result = mCurrentPlayer;
        do {
            result += 1;
            if (result >= getPlayerCount()) {
                result = 0;
            }
        } while (mPlayers.get(result).isCrashed());
        return result;
    }

    /**
     * Returns all of the grid spaces in the path between two spaces, for
     * use in determining line of sight.
     * @param startPoint Starting point as a GridPoint
     * @param endPoint Ending point as a GridPoint
     * @return Intervening grid spaces, as a List of GridPoints. Also
     * includes the starting and ending grid spaces. Each space is given
     * by a an array of length 2, with row first, followed by column.
     */
    public Set<GridPoint> getPath(final GridPoint startPoint, final GridPoint endPoint) {
        // First, pick the axis that has the largest movement.
        // For every grid boundary along that axis, test the line of
        // motion at both the center and the edges of the the cell,
        // identifying the position along the other axis.
        // For each of the identified positions, if the position is
        // within a grid cell, add that cell to the result set.
        // Do nothing if the position is on the boundary between two
        // cells. This means we can squeeze through diagonal corners under
        // the right conditions.

        final double EPS = 1e-8;

        Set<GridPoint> result = new HashSet<>();

        // If there's no movement, no need to do anything. Just return the
        // starting position.
        if (startPoint.equals(endPoint)) {
            result.add(new GridPoint(startPoint));
            return result;
        }

        GridPoint difference = new GridPoint(endPoint.getRow() - startPoint.getRow(),
                endPoint.getCol() - startPoint.getCol());
        GridPoint distance = new GridPoint(Math.abs(difference.getRow()), Math.abs(difference.getCol()));

        GridPoint.Axis mainAxis =
                (distance.getValueOnAxis(GridPoint.Axis.ROW) > distance.getValueOnAxis(GridPoint.Axis.COL)) ?
                        GridPoint.Axis.ROW : GridPoint.Axis.COL;
        GridPoint.Axis secondAxis = (mainAxis == GridPoint.Axis.ROW) ? GridPoint.Axis.COL : GridPoint.Axis.ROW;
        double slope = (double) difference.getValueOnAxis(secondAxis) / difference.getValueOnAxis(mainAxis);
        int stepDirection = (difference.getValueOnAxis(mainAxis) > 0) ? 1 : -1;

        int mainCoord = startPoint.getValueOnAxis(mainAxis);
        while (mainCoord != endPoint.getValueOnAxis(mainAxis)) {
            // Integer coordinate - if applicable, add just the single
            // grid space.
            double secondCoord = Util.getHeightOfLine(slope, startPoint.getValueOnAxis(mainAxis),
                    startPoint.getValueOnAxis(secondAxis), mainCoord);
            if (!Util.isHalfInteger(secondCoord, EPS)) {
                GridPoint newPoint = new GridPoint();
                newPoint.setValueOnAxis(mainAxis, mainCoord);
                newPoint.setValueOnAxis(secondAxis, (int) Math.round(secondCoord));
                result.add(newPoint);
            }
            // Half-integer coordinate - if applicable, add the grid
            // spaces to either side
            double mainHalfCoord = mainCoord + (stepDirection * 0.5);
            double secondHalfCoord = Util.getHeightOfLine(slope, startPoint.getValueOnAxis(mainAxis),
                    startPoint.getValueOnAxis(secondAxis), mainHalfCoord);
            if (!Util.isHalfInteger(secondHalfCoord, EPS)) {
                // Probably not the best names here, but I'm not sure what
                // would be better. If the main axis is the column axis,
                // and if the endPoint is to the right of the startPoint,
                // then the names leftPoint and rightPoint are accurate.
                GridPoint leftPoint = new GridPoint();
                GridPoint rightPoint = new GridPoint();
                int secondHalfInt = (int) Math.round(secondHalfCoord);
                leftPoint.setValueOnAxis(secondAxis, secondHalfInt);
                rightPoint.setValueOnAxis(secondAxis, secondHalfInt);
                leftPoint.setValueOnAxis(mainAxis, mainCoord);
                rightPoint.setValueOnAxis(mainAxis, mainCoord + stepDirection);
                result.add(leftPoint);
                result.add(rightPoint);
            }

            mainCoord += stepDirection;
        }

        result.add(new GridPoint(endPoint));

        return result;
    }

    /**
     * Find the type of track space at the given location. If the location
     * is outside the track bounds, it is considered a wall.
     * @param space The coordinates of the space to examine
     * @return The type of track space at the given location
     */
    public SpaceType getSpace(final GridPoint space) {
        // Anything out of bounds acts like a wall
        if (space.getRow() >= mHeight || space.getRow() < 0 || space.getCol() >= mWidth || space.getCol() < 0) {
            return SpaceType.WALL;
        }

        return mGrid.get(space.getRow()).get(space.getCol());
    }

    public boolean willPlayerCrash(int playerIndex, GridPoint position) {
        return (getSpace(position) == SpaceType.WALL || testPlayerCollision(playerIndex, position));
    }


    private void moveCurrentPlayer() {
        Player player = mPlayers.get(mCurrentPlayer);

        // Check for collisions and for winning
        GridPoint startPoint = player.getPos();
        GridPoint endPoint = player.getNextPos();

        Set<GridPoint> pathPoints = getPath(startPoint, endPoint);
        GridPoint winPoint = null;
        GridPoint winDirection = new GridPoint(0, 0);
        for (GridPoint currentPoint : pathPoints) {
            switch(getSpace(currentPoint)) {
                case TRACK:
                    // As long as we don't collide with another car, do
                    // nothing.
                    if (testPlayerCollision(mCurrentPlayer, endPoint)) {
                        player.crash();
                    }
                    break;
                case WALL:
                    // Crash, and move directly to the location that
                    // caused the crash. No need to keep going
                    player.crash();
                    player.setPos(currentPoint);
                    return;
                case FINISH_UP:
                    // For all of the finishes, set up a potential win,
                    // but don't act on it yet. We still might crash.
                    winDirection.setRow(-1);
                    winPoint = currentPoint;
                    break;
                case FINISH_DOWN:
                    winDirection.setRow(1);
                    winPoint = currentPoint;
                    break;
                case FINISH_LEFT:
                    winDirection.setCol(-1);
                    winPoint = currentPoint;
                    break;
                case FINISH_RIGHT:
                    winDirection.setCol(1);
                    winPoint = currentPoint;
                    break;
            }
        }

        // Test for win
        if (winPoint != null) {
            boolean isValidWin = true;
            if ((winDirection.getRow() != 0 && !Util.isSignSame(winDirection.getRow(), player.getVelocity().getRow()))
                    ||
                    (winDirection.getCol() != 0 &&
                            !Util.isSignSame(winDirection.getCol(), player.getVelocity().getCol()))) {
                isValidWin = false;
            }
            if (isValidWin) {
                mWinner = mCurrentPlayer;
                player.setPos(winPoint);
                return;
            }
        }


        player.move();
    }

    private boolean testPlayerCollision(int playerIndex, GridPoint location) {
        for (int i = 0; i < mPlayers.size(); i++) {
            // Don't check the player against itself
            if (i == playerIndex) continue;

            if (mPlayers.get(i).getPos().equals(location)) {
                return true;
            }
        }
        return false;
    }


    /**
     * Convert a string into a single row, adding it to the bottom of the
     * grid. Increments mHeight to account for the added row.
     * @param rowString A string containing a single row to add.
     * @param border The character to be interpreted as a wall/border.
     */
    private void addGridRow(final String rowString, final char border) throws InvalidTrackFormatException {
        int rowLength = rowString.length();
        List<SpaceType> row = new ArrayList<>(rowLength);
        for (int i = 0; i < rowLength; i++) {
            char currentChar = rowString.charAt(i);
            if (currentChar == border) {
                row.add(SpaceType.WALL);
            } else if (currentChar == SpaceType.TRACK.value) {
                row.add(SpaceType.TRACK);
            } else if (currentChar == SpaceType.FINISH_LEFT.value) {
                row.add(SpaceType.FINISH_LEFT);
            } else if (currentChar == SpaceType.FINISH_RIGHT.value) {
                row.add(SpaceType.FINISH_RIGHT);
            } else if (currentChar == SpaceType.FINISH_UP.value) {
                row.add(SpaceType.FINISH_UP);
            } else if (currentChar == SpaceType.FINISH_DOWN.value) {
                row.add(SpaceType.FINISH_DOWN);
            } else {
                // Unexpected character is a player, as long as we don't
                // have too many players. Since mHeight hasn't yet been
                // updated, the row is mHeight (not mHeight - 1).
                mPlayers.add(new Player(mHeight, i));
                row.add(SpaceType.TRACK);
                if (mPlayers.size() > MAX_PLAYERS) {
                    throw new InvalidTrackFormatException("Unexpected character in row " + Integer.toString(mHeight) +
                            " and column " + Integer.toString(i) + ": " + Character.toString(currentChar));
                }
            }
        }

        mGrid.add(row);
        mHeight++;
    }

}

InvalidTrackFormatException.java:

Nothing much to see here.

package com.erichamion.racetrack;

/**
 * Created by me on 8/16/15.
 */
public class InvalidTrackFormatException extends Exception {
    InvalidTrackFormatException(final String message) {
        super(message);
    }
}

GridPoint.java:

GridPoint started as just a basic point, customized for the (row, column) coordinate system I'm using. It then grew to include some vector operations such as add, subtract, and dot product. Now that it is used in multiple places as both a point and a vector, GridPoint is probably a bad name.

package com.erichamion.racetrack;

/**
 * Started as a simple class to hold a point in a (row, column) grid
 * system, but now also used for vectors and a small set of vector
 * operations in the same (row, column) coordinate system. I'm not sure of
 * a better name that captures both the point and vector usage.
 *
 * Created by me on 8/17/15.
 */
public final class GridPoint implements Cloneable {
    private int mRow;
    private int mCol;



    public enum Axis {
        ROW,
        COL
    }

    /**
     * Adds two vectors or a point and a vector together, returning a new
     * GridPoint.
     * @param obj1 A point or a vector
     * @param obj2 A point or a vector
     * @return A new GridPoint holding the result of the addition. If both
     * arguments are points (not vectors), the result is mathematically
     * correct but meaningless.
     */
    public static GridPoint add(final GridPoint obj1, final GridPoint obj2) {
        return new GridPoint(obj1.getRow() + obj2.getRow(), obj1.getCol() + obj2.getCol());
    }

    /**
     * Subtracts obj2 from obj1.
     * @param obj1 The GridPoint from which to subtract obj2
     * @param obj2 The GridPoint to subtract from obj1
     * @return The result of (obj1 - obj2).
     */
    public static GridPoint subtract(final GridPoint obj1, final GridPoint obj2) {
        return new GridPoint(obj1.getRow() - obj2.getRow(), obj1.getCol() - obj2.getCol());
    }

    /**
     * Returns the dot product of two 2D vectors. The dot product
     * multiplies the lengths of the parallel components of the vectors.
     * @param vectorA A GridPoint representing a vector
     * @param vectorB A GridPoint representing a vector
     * @return The dot product (vectorA * vectorB). Since vectorA and
     * vectorB are GridPoints, and GridPoints hold only integer
     * coordinates, the resulting dot product is an integer.
     */
    public static int dotProduct(final GridPoint vectorA, final GridPoint vectorB) {
        return (vectorA.getRow() * vectorB.getRow()) + (vectorA.getCol() * vectorB.getCol());
    }

    /**
     * Returns the dot product of the unit vectors associated with two
     * specified 2D vectors (a unit vector has the same direction as the
     * given vector, but a length of 1). The unit vector dot product is a
     * measure of how parallel the vectors are. Parallel unit vectors have
     * a dot product of 1, perpendicular vectors have a dot product of 0,
     * and parallel but opposite unit vectors have a dot product of -1.
     * @param vectorA A GridPoint representing a vector
     * @param vectorB A GridPoint representing a vector
     * @return The dot product of the two unit vectors associated with
     * vectorA and vectorB. Although vectorA and vectorB have only
     * integer coordinates, their corresponding unit vectors may have
     * non-integer coordinates, and therefore the dot product is not
     * generally an integer.
     */
    public static double unitDotProduct(final GridPoint vectorA, final GridPoint vectorB) {
        int dot = dotProduct(vectorA, vectorB);
        double lengthASquared = Math.pow(vectorA.getRow(), 2) + Math.pow(vectorA.getCol(), 2);
        double lengthBSquared = Math.pow(vectorB.getRow(), 2) + Math.pow(vectorB.getCol(), 2);
        return dot / Math.sqrt(lengthASquared * lengthBSquared);
    }



    public GridPoint(final int row, final int col) {
        mRow = row;
        mCol = col;
    }

    public GridPoint(final GridPoint other) {
        mRow = other.getRow();
        mCol = other.getCol();
    }

    public GridPoint() {
        mRow = 0;
        mCol = 0;
    }

    public int getRow() {
        return mRow;
    }

    public void setRow(final int row) {
        this.mRow = row;
    }

    public int getCol() {
        return mCol;
    }

    public void setCol(final int col) {
        this.mCol = col;
    }

    public int getValueOnAxis(final Axis axis) {
        return (axis == Axis.ROW) ? mRow : mCol;
    }

    public void setValueOnAxis(final Axis axis, int value) {
        if (axis == Axis.ROW) {
            mRow = value;
        } else {
            mCol = value;
        }
    }

    @Override
    public boolean equals(final Object other) {
        if (!(other instanceof GridPoint)) throw new ClassCastException();
        final GridPoint otherGridPoint = (GridPoint) other;
        return mRow == otherGridPoint.getRow() && mCol == otherGridPoint.getCol();
    }

    @Override
    public String toString() {
        return "R " + Integer.toString(mRow) + ", C " + Integer.toString(mCol);
    }
}

Player.java:

This holds basic information about a player, including position, velocity, and whether or not the player has crashed.

package com.erichamion.racetrack;

/**
 * Created by me on 8/16/15.
 */
public class Player {
    private GridPoint mPosition;
    private GridPoint mVelocity = new GridPoint(0, 0);
    private boolean mIsCrashed = false;

    public Player(final GridPoint position) {
        setPos(position);
    }

    public Player(final int row, final int col) {
        mPosition = new GridPoint(row, col);
    }

    public GridPoint getPos() {
        return new GridPoint(mPosition);
    }

    /**
     * Return the position that will apply after the next move at the
     * current velocity. Does not complete the move, so the current
     * position remains unchanged.
     * @return Expected position after the next move
     */
    public GridPoint getNextPos() {
        return GridPoint.add(mPosition, mVelocity);
    }

    /**
     * Set this Player's position directly, regardless of current position
     * and velocity.
     * @param pos The new position
     */
    public void setPos(final GridPoint pos) {
        mPosition = new GridPoint(pos);
    }

    public GridPoint getVelocity() {
        return new GridPoint(mVelocity);
    }

    /**
     * Add the specified amounts to this Player's velocity. Changes only
     * velocity, not position.
     * @param acceleration A GridPoint containing the amounts to add to
     *                     the velocity in each dimension (row and column)
     */
    public void accelerate(final GridPoint acceleration) {
        mVelocity = GridPoint.add(mVelocity, acceleration);
    }

    /**
     * Update this Player's position based on its current velocity.
     */
    public void move() {
        mPosition = GridPoint.add(mPosition, mVelocity);
    }

    /**
     * Mark this Player as having crashed.
     */
    public void crash() {
        mIsCrashed = true;
    }

    /**
     * Determine whether this Player has been marked as crashed.
     * @return Returns true if crash has been called on this Player, false
     * otherwise.
     */
    public boolean isCrashed() {
        return mIsCrashed;
    }

}

Util.java:

Just a few utility functions that don't really belong anywhere.

package com.erichamion.racetrack;

/**
 * Created by me on 8/16/15.
 */
public class Util {
    /**
     * Compares two double values for approximate equality
     * @param value1 The first value to compare
     * @param value2 The second value to compare
     * @param epsilon If the difference between the values is less than
     *                epsilon, they will be considered equal
     * @return -1 if value1 is less than value2, 1 if value1 is greater
     * than value2, or 0 if the values are approximately equal (within
     * epsilon)
     */
    public static int doubleCompare(final double value1, final double value2, final double epsilon) {
        double diff = value1 - value2;
        if (Math.abs(diff) < epsilon) {
            return 0;
        } else if (diff < 0) {
            return -1;
        } else {
            return 1;
        }
    }

    /**
     * Determine whether a value lies halfway between two consecutive
     * integers (that is, whether the value is (n + 0.5) for some integer
     * n. Uses an approximate comparison.
     * @param value The value to check
     * @param epsilon If value is different from (n + 0.5) by an amount
     *                less than epsilon, it will be considered equal
     * @return Returns true if value is within epsilon of (n + 0.5), false
     * otherwise
     */
    public static boolean isHalfInteger(final double value, final double epsilon){
        return doubleCompare(Math.floor(value), value - 0.5, epsilon) == 0;
    }

    /**
     * Get the Y-coordinate of a line at the specified X-coordinate, given
     * a slope and a starting point.
     * @param slope The slope of the line
     * @param startX The X-coordinate (independent axis coordinate of the
     *               starting point
     * @param startY The Y-coordinate (dependent axis coordinate of the
     *               starting point
     * @param x The X-coordinate for which to find the corresponding Y
     * @return The height of the line at the given x
     */
    public static double getHeightOfLine(final double slope, final int startX, final int startY, final double x) {
        return slope * (x - startX) + startY;
    }

    /**
     * Determine whether two integer values have the same sign (positive,
     * negative, or 0)
     * @param value1 An integer
     * @param value2 An integer
     * @return Return true if both values are positive, both are negative,
     * or both are 0. Return false otherwise.
     */
    public static boolean isSignSame(final int value1, final int value2) {
        return Integer.signum(value1) == Integer.signum(value2);
    }

    /**
     * Returns a reference to a specified element within  an Iterable
     * container, if such element exists. Does not remove the element
     * from its container.
     * @param iterable The collection or other Iterable that (potentially)
     *                 contains the specified element
     * @param obj An object used to select the desired element. Each
     *            element e will be tested using e.equals(obj).
     * @param <T> The type of element held by iterable
     * @return If one or more element e exist such that e.equals(obj),
     * returns the first such element. Otherwise, returns null.
     */
    public static <T> T getObjectFromIterable(final Iterable<T> iterable, final Object obj) {
        for (T currentElement : iterable) {
            if (currentElement.equals(obj)) return currentElement;
        }
        return null;
    }


}

Continued:

Due to the length of this post, the pathfinding and path following classes will be in a separate post.

\$\endgroup\$
5
\$\begingroup\$

One point I want to make is that your algorithm for determining a win is flawed:

 // Test for win
        if (winPoint != null) {
            boolean isValidWin = true;
            if ((winDirection.getRow() != 0 && !Util.isSignSame(winDirection.getRow(), player.getVelocity().getRow()))
                    ||
                    (winDirection.getCol() != 0 &&
                            !Util.isSignSame(winDirection.getCol(), player.getVelocity().getCol()))) {
                isValidWin = false;
            }
            if (isValidWin) {
                mWinner = mCurrentPlayer;
                player.setPos(winPoint);
                return;
            }
        }

This only tests if user passes the finish line in the right direction. A player can cheat, as I did, by crossing the finish line in the reverse direction a little and reversing direction. You can prevent this in two ways:

  • either disallowing crossing the finish line in the reverse direction

  • or, if you want to allow crossing the finish line in the reverse direction for some reason, by counting the times it is crossed in each direction, and declare a winner once the player crosses the line in the right direction more than he did in the reverse direction.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.