/*
* @(#)Figure.java
*
* This work is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This work is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* Copyright (c) 2003 Per Cederberg. All rights reserved.
*/
package net.percederberg.tetris;
import java.awt.Color;
/**
* A class representing a Tetris square figure. Each figure consists
* of four connected squares in one of seven possible constellations.
* The figures may be rotated in 90 degree steps and have sideways and
* downwards movability.<p>
*
* Each figure instance can have two states, either attached to a
* square board or not. When attached, all move and rotation
* operations are checked so that collisions do not occur with other
* squares on the board. When not attached, any rotation can be made
* (and will be kept when attached to a new board).
*
* @version 1.2
* @author Per Cederberg, per@percederberg.net
*/
public class Figure extends Object {
/**
* A figure constant used to create a figure forming a square.
*/
public static final int SQUARE_FIGURE = 1;
/**
* A figure constant used to create a figure forming a line.
*/
public static final int LINE_FIGURE = 2;
/**
* A figure constant used to create a figure forming an "S".
*/
public static final int S_FIGURE = 3;
/**
* A figure constant used to create a figure forming a "Z".
*/
public static final int Z_FIGURE = 4;
/**
* A figure constant used to create a figure forming a right angle.
*/
public static final int RIGHT_ANGLE_FIGURE = 5;
/**
* A figure constant used to create a figure forming a left angle.
*/
public static final int LEFT_ANGLE_FIGURE = 6;
/**
* A figure constant used to create a figure forming a triangle.
*/
public static final int TRIANGLE_FIGURE = 7;
/**
* The square board to which the figure is attached. If this
* variable is set to null, the figure is not attached.
*/
private SquareBoard board = null;
/**
* The horizontal figure position on the board. This value has no
* meaning when the figure is not attached to a square board.
*/
private int xPos = 0;
/**
* The vertical figure position on the board. This value has no
* meaning when the figure is not attached to a square board.
*/
private int yPos = 0;
/**
* The figure orientation (or rotation). This value is normally
* between 0 and 3, but must also be less than the maxOrientation
* value.
*
* @see #maxOrientation
*/
private int orientation = 0;
/**
* The maximum allowed orientation number. This is used to reduce
* the number of possible rotations for some figures, such as the
* square figure. If this value is not used, the square figure
* will be possible to rotate around one of its squares, which
* gives an erroneous effect.
*
* @see #orientation
*/
private int maxOrientation = 4;
/**
* The horizontal coordinates of the figure shape. The coordinates
* are relative to the current figure position and orientation.
*/
private int[] shapeX = new int[4];
/**
* The vertical coordinates of the figure shape. The coordinates
* are relative to the current figure position and orientation.
*/
private int[] shapeY = new int[4];
/**
* The figure color.
*/
private Color color = Color.white;
/**
* Creates a new figure of one of the seven predefined types. The
* figure will not be attached to any square board and default
* colors and orientations will be assigned.
*
* @param type the figure type (one of the figure constants)
*
* @see #SQUARE_FIGURE
* @see #LINE_FIGURE
* @see #S_FIGURE
* @see #Z_FIGURE
* @see #RIGHT_ANGLE_FIGURE
* @see #LEFT_ANGLE_FIGURE
* @see #TRIANGLE_FIGURE
*
* @throws IllegalArgumentException if the figure type specified
* is not recognized
*/
public Figure(int type) throws IllegalArgumentException {
initialize(type);
}
/**
* Initializes the instance variables for a specified figure type.
*
* @param type the figure type (one of the figure constants)
*
* @see #SQUARE_FIGURE
* @see #LINE_FIGURE
* @see #S_FIGURE
* @see #Z_FIGURE
* @see #RIGHT_ANGLE_FIGURE
* @see #LEFT_ANGLE_FIGURE
* @see #TRIANGLE_FIGURE
*
* @throws IllegalArgumentException if the figure type specified
* is not recognized
*/
private void initialize(int type) throws IllegalArgumentException {
// Initialize default variables
board = null;
xPos = 0;
yPos = 0;
orientation = 0;
// Initialize figure type variables
switch (type) {
case SQUARE_FIGURE :
maxOrientation = 1;
color = Configuration.getColor("figure.square", "#ffd8b1");
shapeX[0] = -1;
shapeY[0] = 0;
shapeX[1] = 0;
shapeY[1] = 0;
shapeX[2] = -1;
shapeY[2] = 1;
shapeX[3] = 0;
shapeY[3] = 1;
break;
case LINE_FIGURE :
maxOrientation = 2;
color = Configuration.getColor("figure.line", "#ffb4b4");
shapeX[0] = -2;
shapeY[0] = 0;
shapeX[1] = -1;
shapeY[1] = 0;
shapeX[2] = 0;
shapeY[2] = 0;
shapeX[3] = 1;
shapeY[3] = 0;
break;
case S_FIGURE :
maxOrientation = 2;
color = Configuration.getColor("figure.s", "#a3d5ee");
shapeX[0] = 0;
shapeY[0] = 0;
shapeX[1] = 1;
shapeY[1] = 0;
shapeX[2] = -1;
shapeY[2] = 1;
shapeX[3] = 0;
shapeY[3] = 1;
break;
case Z_FIGURE :
maxOrientation = 2;
color = Configuration.getColor("figure.z", "#f4adff");
shapeX[0] = -1;
shapeY[0] = 0;
shapeX[1] = 0;
shapeY[1] = 0;
shapeX[2] = 0;
shapeY[2] = 1;
shapeX[3] = 1;
shapeY[3] = 1;
break;
case RIGHT_ANGLE_FIGURE :
maxOrientation = 4;
color = Configuration.getColor("figure.right", "#c0b6fa");
shapeX[0] = -1;
shapeY[0] = 0;
shapeX[1] = 0;
shapeY[1] = 0;
shapeX[2] = 1;
shapeY[2] = 0;
shapeX[3] = 1;
shapeY[3] = 1;
break;
case LEFT_ANGLE_FIGURE :
maxOrientation = 4;
color = Configuration.getColor("figure.left", "#f5f4a7");
shapeX[0] = -1;
shapeY[0] = 0;
shapeX[1] = 0;
shapeY[1] = 0;
shapeX[2] = 1;
shapeY[2] = 0;
shapeX[3] = -1;
shapeY[3] = 1;
break;
case TRIANGLE_FIGURE :
maxOrientation = 4;
color = Configuration.getColor("figure.triangle", "#a4d9b6");
shapeX[0] = -1;
shapeY[0] = 0;
shapeX[1] = 0;
shapeY[1] = 0;
shapeX[2] = 1;
shapeY[2] = 0;
shapeX[3] = 0;
shapeY[3] = 1;
break;
default :
throw new IllegalArgumentException("No figure constant: " +
type);
}
}
/**
* Checks if this figure is attached to a square board.
*
* @return true if the figure is already attached, or
* false otherwise
*/
public boolean isAttached() {
return board != null;
}
/**
* Attaches the figure to a specified square board. The figure
* will be drawn either at the absolute top of the board, with
* only the bottom line visible, or centered onto the board. In
* both cases, the squares on the new board are checked for
* collisions. If the squares are already occupied, this method
* returns false and no attachment is made.<p>
*
* The horizontal and vertical coordinates will be reset for the
* figure, when centering the figure on the new board. The figure
* orientation (rotation) will be kept, however. If the figure was
* previously attached to another board, it will be detached from
* that board before attaching to the new board.
*
* @param board the square board to attach to
* @param center the centered position flag
*
* @return true if the figure could be attached, or
* false otherwise
*/
public boolean attach(SquareBoard board, boolean center) {
int newX;
int newY;
int i;
// Check for previous attachment
if (isAttached()) {
detach();
}
// Reset position (for correct controls)
xPos = 0;
yPos = 0;
// Calculate position
newX = board.getBoardWidth() / 2;
if (center) {
newY = board.getBoardHeight() / 2;
} else {
newY = 0;
for (i = 0; i < shapeX.length; i++) {
if (getRelativeY(i, orientation) - newY > 0) {
newY = -getRelativeY(i, orientation);
}
}
}
// Check position
this.board = board;
if (!canMoveTo(newX, newY, orientation)) {
this.board = null;
return false;
}
// Draw figure
xPos = newX;
yPos = newY;
paint(color);
board.update();
return true;
}
/**
* Detaches this figure from its square board. The figure will not
* be removed from the board by this operation, resulting in the
* figure being left intact.
*/
public void detach() {
board = null;
}
/**
* Checks if the figure is fully visible on the square board. If
* the figure isn't attached to a board, false will be returned.
*
* @return true if the figure is fully visible, or
* false otherwise
*/
public boolean isAllVisible() {
if (!isAttached()) {
return false;
}
for (int i = 0; i < shapeX.length; i++) {
if (yPos + getRelativeY(i, orientation) < 0) {
return false;
}
}
return true;
}
/**
* Checks if the figure has landed. If this method returns true,
* the moveDown() or the moveAllWayDown() methods should have no
* effect. If no square board is attached, this method will return
* true.
*
* @return true if the figure has landed, or false otherwise
*/
public boolean hasLanded() {
return !isAttached() || !canMoveTo(xPos, yPos + 1, orientation);
}
/**
* Moves the figure one step to the left. If such a move is not
* possible with respect to the square board, nothing is done. The
* square board will be changed as the figure moves, clearing the
* previous cells. If no square board is attached, nothing is
* done.
*/
public void moveLeft() {
if (isAttached() && canMoveTo(xPos - 1, yPos, orientation)) {
paint(null);
xPos--;
paint(color);
board.update();
}
}
/**
* Moves the figure one step to the right. If such a move is not
* possible with respect to the square board, nothing is done. The
* square board will be changed as the figure moves, clearing the
* previous cells. If no square board is attached, nothing is
* done.
*/
public void moveRight() {
if (isAttached() && canMoveTo(xPos + 1, yPos, orientation)) {
paint(null);
xPos++;
paint(color);
board.update();
}
}
/**
* Moves the figure one step down. If such a move is not possible
* with respect to the square board, nothing is done. The square
* board will be changed as the figure moves, clearing the
* previous cells. If no square board is attached, nothing is
* done.
*/
public void moveDown() {
if (isAttached() && canMoveTo(xPos, yPos + 1, orientation)) {
paint(null);
yPos++;
paint(color);
board.update();
}
}
/**
* Moves the figure all the way down. The limits of the move are
* either the square board bottom, or squares not being empty. If
* no move is possible with respect to the square board, nothing
* is done. The square board will be changed as the figure moves,
* clearing the previous cells. If no square board is attached,
* nothing is done.
*/
public void moveAllWayDown() {
int y = yPos;
// Check for board
if (!isAttached()) {
return;
}
// Find lowest position
while (canMoveTo(xPos, y + 1, orientation)) {
y++;
}
// Update
if (y != yPos) {
paint(null);
yPos = y;
paint(color);
board.update();
}
}
/**
* Returns the current figure rotation (orientation).
*
* @return the current figure rotation
*/
public int getRotation() {
return orientation;
}
/**
* Sets the figure rotation (orientation). If the desired rotation
* is not possible with respect to the square board, nothing is
* done. The square board will be changed as the figure moves,
* clearing the previous cells. If no square board is attached,
* the rotation is performed directly.
*
* @param rotation the new figure orientation
*/
public void setRotation(int rotation) {
int newOrientation;
// Set new orientation
newOrientation = rotation % maxOrientation;
// Check new position
if (!isAttached()) {
orientation = newOrientation;
} else if (canMoveTo(xPos, yPos, newOrientation)) {
paint(null);
orientation = newOrientation;
paint(color);
board.update();
}
}
/**
* Rotates the figure randomly. If such a rotation is not
* possible with respect to the square board, nothing is done.
* The square board will be changed as the figure moves,
* clearing the previous cells. If no square board is attached,
* the rotation is performed directly.
*/
public void rotateRandom() {
setRotation((int) (Math.random() * 4.0) % maxOrientation);
}
/**
* Rotates the figure clockwise. If such a rotation is not
* possible with respect to the square board, nothing is done.
* The square board will be changed as the figure moves,
* clearing the previous cells. If no square board is attached,
* the rotation is performed directly.
*/
public void rotateClockwise() {
if (maxOrientation == 1) {
return;
} else {
setRotation((orientation + 1) % maxOrientation);
}
}
/**
* Rotates the figure counter-clockwise. If such a rotation
* is not possible with respect to the square board, nothing
* is done. The square board will be changed as the figure
* moves, clearing the previous cells. If no square board is
* attached, the rotation is performed directly.
*/
public void rotateCounterClockwise() {
if (maxOrientation == 1) {
return;
} else {
setRotation((orientation + 3) % 4);
}
}
/**
* Checks if a specified pair of (square) coordinates are inside
* the figure, or not.
*
* @param x the horizontal position
* @param y the vertical position
*
* @return true if the coordinates are inside the figure, or
* false otherwise
*/
private boolean isInside(int x, int y) {
for (int i = 0; i < shapeX.length; i++) {
if (x == xPos + getRelativeX(i, orientation)
&& y == yPos + getRelativeY(i, orientation)) {
return true;
}
}
return false;
}
/**
* Checks if the figure can move to a new position. The current
* figure position is taken into account when checking for
* collisions. If a collision is detected, this method will return
* false.
*
* @param newX the new horizontal position
* @param newY the new vertical position
* @param newOrientation the new orientation (rotation)
*
* @return true if the figure can be moved, or
* false otherwise
*/
private boolean canMoveTo(int newX, int newY, int newOrientation) {
int x;
int y;
for (int i = 0; i < 4; i++) {
x = newX + getRelativeX(i, newOrientation);
y = newY + getRelativeY(i, newOrientation);
if (!isInside(x, y) && !board.isSquareEmpty(x, y)) {
return false;
}
}
return true;
}
/**
* Returns the relative horizontal position of a specified square.
* The square will be rotated according to the specified
* orientation.
*
* @param square the square to rotate (0-3)
* @param orientation the orientation to use (0-3)
*
* @return the rotated relative horizontal position
*/
private int getRelativeX(int square, int orientation) {
switch (orientation % 4) {
case 0 :
return shapeX[square];
case 1 :
return -shapeY[square];
case 2 :
return -shapeX[square];
case 3 :
return shapeY[square];
default:
return 0; // Should never occur
}
}
/**
* Rotates the relative vertical position of a specified square.
* The square will be rotated according to the specified
* orientation.
*
* @param square the square to rotate (0-3)
* @param orientation the orientation to use (0-3)
*
* @return the rotated relative vertical position
*/
private int getRelativeY(int square, int orientation) {
switch (orientation % 4) {
case 0 :
return shapeY[square];
case 1 :
return shapeX[square];
case 2 :
return -shapeY[square];
case 3 :
return -shapeX[square];
default:
return 0; // Should never occur
}
}
/**
* Paints the figure on the board with the specified color.
*
* @param color the color to paint with, or null for clearing
*/
private void paint(Color color) {
int x, y;
for (int i = 0; i < shapeX.length; i++) {
x = xPos + getRelativeX(i, orientation);
y = yPos + getRelativeY(i, orientation);
board.setSquareColor(x, y, color);
}
}
}