de/joshuagleitze/tilinggame

Tile.java

Download file
package de.joshuagleitze.tilinggame;

/**
 * A Tile that can be placed on a {@link Board} in the simple tiling game. A
 * tile has 6 edges, numbered from 0 to 5, each one having a
 * {@linkplain LineType}. If two edges have the same {@linkplain LineType}
 * {@code x} and {@link LineType#isColor() x.isColor()}, there is a line between
 * those edges. There is at most one line for each
 * {@linkplain LineType#isColor() color} on a tile.
 *
 * <p>
 * Tiles can be rotated by multiples of 60°.
 *
 * @author Joshua Gleitze
 * @version 1.0
 * @see Board
 */
public class Tile {
    /**
     * The number of edges on one tile.
     */
    static final int EDGE_COUNT = 6;
    /**
     * An empty tile, used for comparisons and generating copies.
     */
    private static final Tile EMPTY_TILE = new Tile(Tile.createTypeNoneArray(), 0);

    /**
     * Queries the edge opposite of a provided edge.
     *
     * @param edgeIndex
     *            An edge index. Must be ≥ 0.
     * @return The index of the edge opposite of the edge denoted by
     *         {@code edgeIndex}.
     */
    static int getOppositeEdgeIndex(final int edgeIndex) {
        return (edgeIndex + EDGE_COUNT / 2) % EDGE_COUNT;
    }

    /**
     * @return An array of size {@link #EDGE_COUNT}, consisting only of
     *         {@link LineType#NONE}.
     */
    private static LineType[] createTypeNoneArray() {
        final LineType[] typeNones = new LineType[EDGE_COUNT];
        for (int edgeIndex = 0; edgeIndex < typeNones.length; edgeIndex++) {
            typeNones[edgeIndex] = LineType.NONE;
        }
        return typeNones;
    }

    /**
     * The line type of each edge. No line type is {@code null}. Each
     * {@linkplain LineType line type} {@code x} with {@link LineType#isColor()
     * x.isColor()} occurs zero or two times in this array.
     */
    // This array is immutable and can be shared between instances.
    private final LineType[] edges;

    /**
     * The top edge’s index. Can be modified to rotate the tile. Satisfies 0 0 ≤
     * {@link #top} ≤ 5
     */
    private int top;

    /**
     * Creates a new tile out of the provided arguments.
     *
     * @param edges
     *            The edges array for the new instance. It is not copied. Must
     *            satisfy {@code lineTypes != null}, {@code edges.length == 6}
     *            and for every 0 ≤ {@code i} ≤ 5: {@code edges[i] != null}.
     * @param top
     *            The top edge’s index. Must satisfy 0 ≤ {@code top} ≤ 5.
     */
    private Tile(final LineType[] edges, final int top) {
        this.edges = edges;
        this.top = top;
    }

    /**
     * Creates a new, empty tile. An empty tile is a tile with no lines, e.g.
     * all edges have {@link LineType#NONE}.
     */
    public Tile() {
        this(EMPTY_TILE.edges, 0);
    }

    /**
     * Creates a new tile that has the provided {@linkplain LineType line types}
     * at its edges.
     *
     * @param lineTypes
     *            The line types for the new tile. Must satisfy
     *            {@code lineTypes != null}, {@code lineTypes.length == 6} and
     *            for every 0 ≤ {@code i} ≤ 5: {@code lineTypes[i] != null}.
     */
    public Tile(final LineType[] lineTypes) {
        this(lineTypes.clone(), 0);
    }

    /**
     * Queries the {@linkplain LineType line type} at a given edge.
     *
     * @param index
     *            The index of the queried edge. Must be ≥ 0.
     * @return The line type at the edge at index {@code index % 6}.
     */
    public LineType getLineTypeAtIndex(final int index) {
        return this.edges[(index + this.top) % EDGE_COUNT];
    }

    /**
     * Queries the number of different {@linkplain LineType#isColor() colors} on
     * this tile.
     *
     * @return The number of lines on this tile.
     */
    public int getNumberOfColors() {
        int numberOfColors = 0;
        for (final LineType edgeType : this.edges) {
            if (edgeType.isColor()) {
                numberOfColors++;
            }
        }
        return numberOfColors / 2;
    }

    /**
     * Rotates this tile clockwise by 60°.
     */
    public void rotateClockwise() {
        this.top = (this.top + EDGE_COUNT - 1) % EDGE_COUNT;
    }

    /**
     * Rotates this tile counterclockwise by 60°.
     */
    public void rotateCounterClockwise() {
        this.top = (this.top + 1) % EDGE_COUNT;
    }

    /**
     * Queries whether there are any lines on this tile.
     *
     * @return {@code true} iff every edge’s line type is {@link LineType#NONE}.
     */
    public boolean isEmpty() {
        return this.isExactlyEqualTo(EMPTY_TILE);
    }

    /**
     * Queries whether this tile is exactly equal to another tile.
     *
     * @param other
     *            A tile to compare with. Must not be {@code null}.
     * @return {@code true} iff this tile and {@code other} have the same
     *         {@linkplain LineType line types} for edges with the same index.
     */
    public boolean isExactlyEqualTo(final Tile other) {
        return this.isEqualIfRotatedBy(0, other);
    }

    /**
     * Queries whether this tile would be equal to another tile if it was
     * rotated appropriately.
     *
     * @param other
     *            A tile to compare with. Must not be {@code null}.
     * @return {@code true} iff there is a natural number {@code i}, such that
     *         if this tile was first {@linkplain #rotateClockwise() rotated}
     *         {@code i} times, it would be {@linkplain #isExactlyEqualTo(Tile)
     *         exactly equal} to {@code other}.
     */
    public boolean isRotationEqualTo(final Tile other) {
        for (int rotation = 0; rotation < this.edges.length; rotation++) {
            if (this.isEqualIfRotatedBy(rotation, other)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Queries whether this tile would be exactly equal to another tile, if it
     * was first rotated by the provided multiple of 60°.
     *
     * @param rotation
     *            How many multiples of 60° this tile is to be (virtually)
     *            rotated by before comparing it with the other tile.
     * @param other
     *            A tile to compare with. Must not be {@code null}.
     * @return {@code true} iff this tile would be
     *         {@linkplain #isExactlyEqualTo(Tile) exactly equal} to
     *         {@code other}, assuming it was first
     *         {@linkplain #rotateClockwise() rotated} {@code rotation} times.
     */
    private boolean isEqualIfRotatedBy(final int rotation, final Tile other) {
        for (int edgeIndex = 0; edgeIndex < this.edges.length; edgeIndex++) {
            // getLineTypeAtIndex support indeces >= EDGE_COUNT
            if (this.getLineTypeAtIndex(edgeIndex + rotation) != other.getLineTypeAtIndex(edgeIndex)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Creates a new tile with the same properties as this tile.
     *
     * @return A new tile, having the same edges as this tile.
     */
    public Tile copy() {
        // share the edges array.
        return new Tile(this.edges, this.top);
    }

    /**
     * Queries whether this tile has the same colors as another tile.
     *
     * @param other
     *            A tile to compare with. Must not be {@code null}.
     * @return {@code true} iff the same colors can be found on both tiles.
     */
    public boolean hasSameColorsAs(final Tile other) {
        for (final LineType color : LineType.colors()) {
            if (this.hasType(color) != other.hasType(color)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Queries whether the provided {@linkplain LineType line type} can be found
     * on any edge on this tile.
     *
     * @param type
     *            The line type to look for. Must not be {@code null}.
     * @return {@code true} iff there is an index {@code i}, such that
     *         {@linkplain #getLineTypeAtIndex(int) the line type at edge}
     *         {@code i} is {@code type}.
     */
    boolean hasType(final LineType type) {
        for (final LineType edgeType : this.edges) {
            if (edgeType == type) {
                return true;
            }
        }
        return false;
    }

    /**
     * Queries whether it’s possible to create another tile by recoloring lines
     * on this tile.
     *
     * @param other
     *            A tile to check against. Must not be {@code null}.
     * @return {@code true} iff this tile would be
     *         {@linkplain #isExactlyEqualTo(Tile) exactly equal} to
     *         {@code other} if no, one or more lines on this tiles would get
     *         another {@linkplain LineType#isColor() color}.
     */
    public boolean canBeRecoloredTo(final Tile other) {
        final LineType[] recolorings = new LineType[LineType.values().length];
        for (int edgeIndex = 0; edgeIndex < this.edges.length; edgeIndex++) {
            final LineType thisType = this.getLineTypeAtIndex(edgeIndex);
            final LineType otherType = other.getLineTypeAtIndex(edgeIndex);
            if (recolorings[thisType.ordinal()] == null) {
                recolorings[thisType.ordinal()] = otherType;
            }
            final LineType recoloredThisType = recolorings[thisType.ordinal()];
            if (thisType.isColor() != otherType.isColor() || recoloredThisType != otherType) {
                return false;
            }
        }
        return true;
    }

    /**
     * Queries whether it’s possible to create this tile by adding lines to
     * another tile.
     *
     * @param other
     *            A tile to check against. Must not be {@code null}.
     * @return {@code true} iff adding at least one line to {@code other} would
     *         result in {@code other} being {@linkplain #isExactlyEqualTo(Tile)
     *         exactly equal} to this tile.
     */
    public boolean dominates(final Tile other) {
        boolean foundLineToAdd = false;
        for (int edgeIndex = 0; edgeIndex < this.edges.length; edgeIndex++) {
            final LineType thisType = this.getLineTypeAtIndex(edgeIndex);
            final LineType otherType = other.getLineTypeAtIndex(edgeIndex);
            if (thisType != otherType) {
                if (otherType.isColor()) {
                    return false;
                }
                foundLineToAdd = true;
            }
        }
        return foundLineToAdd;
    }

    /**
     * Queries if another tile could be validly placed next to this tile at the
     * given edge. A valid placement means that the connecting edges have the
     * same {@linkplain LineType#isColor() colors}, or no line ends on at least
     * one of the connecting edges.
     *
     * @param other
     *            A tile to check against. Must not be {@code null}.
     * @param edgeIndex
     *            The index of the edge {@code other} is to be placed at.
     * @return {@code true} iff placing {@code other} at this tile at the edge
     *         denoted by {@code edgeIndex} would be a correct placement, as
     *         defined above.
     */
    public boolean fitsTo(final Tile other, final int edgeIndex) {
        final LineType thisType = this.getLineTypeAtIndex(edgeIndex);
        final LineType otherType = other.getLineTypeAtIndex(Tile.getOppositeEdgeIndex(edgeIndex));
        return thisType == otherType || !thisType.isColor() || !otherType.isColor();
    }

    @Override
    public String toString() {
        final StringBuilder result = new StringBuilder(this.edges.length);
        for (int edgeIndex = 0; edgeIndex < this.edges.length; edgeIndex++) {
            result.append(this.getLineTypeAtIndex(edgeIndex).getAbbreviation());
        }
        return result.toString();
    }
}