Tile.java
Download filepackage 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();
}
}