Tetris Game
/**
* @(#)Main.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.
*/
import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.TextArea;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.Hashtable;
import javax.swing.JComponent;
/**
* The main class of the Tetris game. This class contains the
* necessary methods to run the game either as a stand-alone
* application or as an applet inside a web page.
*
* @version 1.2
* @author Per Cederberg, per@percederberg.net
*/
public class Tetris
{
// /**
// * The applet parameter information structure.
// */
// private static final String PARAMETER[][] = {
// { "tetris.color.background", "color",
// "The overall background color." },
// { "tetris.color.label", "color",
// "The text color of the labels." },
// { "tetris.color.button", "color",
// "The start and pause button bolor." },
// { "tetris.color.board.background", "color",
// "The background game board color." },
// { "tetris.color.board.message", "color",
// "The game board message color." },
// { "tetris.color.figure.square", "color",
// "The color of the square figure." },
// { "tetris.color.figure.line", "color",
// "The color of the line figure." },
// { "tetris.color.figure.s", "color",
// "The color of the 's' curved figure." },
// { "tetris.color.figure.z", "color",
// "The color of the 'z' curved figure." },
// { "tetris.color.figure.right", "color",
// "The color of the right angle figure." },
// { "tetris.color.figure.left", "color",
// "The color of the left angle figure." },
// { "tetris.color.figure.triangle", "color",
// "The color of the triangle figure." }
// };
/**
* The Tetris game being played (in applet mode).
*/
private Game game = null;
/**
* The stand-alone main routine.
*
* @param args the command-line arguments
*/
public static void main(String[] args) {
System.out.println("starting");
Frame frame = new Frame("Tetris");
final Game game = new Game();
game.addPropertyChangeListener(new PropertyChangeListener()
{
public void propertyChange(PropertyChangeEvent evt)
{
System.out.println("PCE "+evt.getPropertyName()+" "+evt.getNewValue());
}
});
final TextArea taHiScores = new TextArea("",10,10,TextArea.SCROLLBARS_NONE);
taHiScores.setBackground(Color.black);
taHiScores.setForeground(Color.white);
taHiScores.setFont(new Font("monospaced",0,11));
taHiScores.setText(" High Scores \n"+
" -----------------------------\n\n"+
" PLAYER LEVEL SCORE \n\n"+
" Lorenzo 12 1 50280 \n"+
" Lorenzo 12 1 50280 \n"
);
taHiScores.setEditable(false);
final TextField txt = new TextField();
txt.setEnabled(false);
game.addPropertyChangeListener(new PropertyChangeListener()
{
public void propertyChange(PropertyChangeEvent evt)
{
if (evt.getPropertyName().equals("state"))
{
int state = ((Integer) evt.getNewValue()).intValue();
if (state == Game.STATE_GAMEOVER)
{
txt.setEnabled(true);
txt.requestFocus();
txt.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
txt.setEnabled(false);
game.init();
}
});
// show score...
}
}
}
});
Button btnStart = new Button("Start");
btnStart.setFocusable(false);
btnStart.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
game.start();
}
});
final Container c = new Container();
c.setLayout(new BorderLayout());
c.add(txt, BorderLayout.NORTH);
c.add(game.getSquareBoardComponent(), BorderLayout.CENTER);
c.add(btnStart,BorderLayout.SOUTH);
final Container c2 = new Container();
c2.setLayout(new GridLayout(1,2));
c2.add(c);
c2.add(taHiScores);
frame.add(c2);
System.out.println("packing");
frame.pack();
// Add frame window listener
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
// Show frame (and start game)
frame.show();
}
// /**
// * Returns information about the parameters that are understood by
// * this applet.
// *
// * @return an array describing the parameters to this applet
// */
// public String[][] getParameterInfo() {
// return PARAMETER;
// }
// /**
// * Initializes the game in applet mode.
// */
// public void init() {
// String value;
//
// // Set all configuration parameters
// for (int i = 0; i < PARAMETER.length; i++) {
// value = getParameter(PARAMETER[i][0]);
// if (value != null) {
// Configuration.setValue(PARAMETER[i][0], value);
// }
// }
//
// // Create game object
// game = new Game();
//
// // Initialize applet component
// setLayout(new BorderLayout());
// add(game.getComponent(), "Center");
// }
//
// /**
// * Stops the game in applet mode.
// */
// public void stop() {
// game.quit();
// }
}
/*
* @(#)SquareBoard.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.
*/
/**
* A Tetris square board. The board is rectangular and contains a grid
* of colored squares. The board is considered to be constrained to
* both sides (left and right), and to the bottom. There is no
* constraint to the top of the board, although colors assigned to
* positions above the board are not saved.
*
* @version 1.2
* @author Per Cederberg, per@percederberg.net
*/
class SquareBoard extends Object {
/**
* The board width (in squares)
*/
private final int width;
/**
* The board height (in squares).
*/
private final int height;
/**
* The square board color matrix. This matrix (or grid) contains
* a color entry for each square in the board. The matrix is
* indexed by the vertical, and then the horizontal coordinate.
*/
private Color[][] matrix = null;
/**
* An optional board message. The board message can be set at any
* time, printing it on top of the board.
*/
private String message = null;
/**
* The number of lines removed. This counter is increased each
* time a line is removed from the board.
*/
private int removedLines = 0;
/**
* The graphical sqare board component. This graphical
* representation is created upon the first call to
* getComponent().
*/
private final SquareBoardComponent component;
/**
* Creates a new square board with the specified size. The square
* board will initially be empty.
*
* @param width the width of the board (in squares)
* @param height the height of the board (in squares)
*/
public SquareBoard(int width, int height) {
this.width = width;
this.height = height;
this.matrix = new Color[height][width];
this.component = new SquareBoardComponent();
clear();
}
/**
* Checks if a specified square is empty, i.e. if it is not
* marked with a color. If the square is outside the board,
* false will be returned in all cases except when the square is
* directly above the board.
*
* @param x the horizontal position (0 <= x < width)
* @param y the vertical position (0 <= y < height)
*
* @return true if the square is emtpy, or
* false otherwise
*/
public boolean isSquareEmpty(int x, int y) {
if (x < 0 || x >= width || y < 0 || y >= height) {
return x >= 0 && x < width && y < 0;
} else {
return matrix[y][x] == null;
}
}
/**
* Checks if a specified line is empty, i.e. only contains
* empty squares. If the line is outside the board, false will
* always be returned.
*
* @param y the vertical position (0 <= y < height)
*
* @return true if the whole line is empty, or
* false otherwise
*/
public boolean isLineEmpty(int y) {
if (y < 0 || y >= height) {
return false;
}
for (int x = 0; x < width; x++) {
if (matrix[y][x] != null) {
return false;
}
}
return true;
}
/**
* Checks if a specified line is full, i.e. only contains no empty
* squares. If the line is outside the board, true will always be
* returned.
*
* @param y the vertical position (0 <= y < height)
*
* @return true if the whole line is full, or
* false otherwise
*/
public boolean isLineFull(int y) {
if (y < 0 || y >= height) {
return true;
}
for (int x = 0; x < width; x++) {
if (matrix[y][x] == null) {
return false;
}
}
return true;
}
/**
* Checks if the board contains any full lines.
*
* @return true if there are full lines on the board, or
* false otherwise
*/
public boolean hasFullLines() {
for (int y = height - 1; y >= 0; y--) {
if (isLineFull(y)) {
return true;
}
}
return false;
}
/**
* Returns a graphical component to draw the board. The component
* returned will automatically be updated when changes are made to
* this board. Multiple calls to this method will return the same
* component, as a square board can only have a single graphical
* representation.
*
* @return a graphical component that draws this board
*/
public Component getComponent() {
return component;
}
/**
* Returns the board height (in squares). This method returns,
* i.e, the number of vertical squares that fit on the board.
*
* @return the board height in squares
*/
public int getBoardHeight() {
return height;
}
/**
* Returns the board width (in squares). This method returns, i.e,
* the number of horizontal squares that fit on the board.
*
* @return the board width in squares
*/
public int getBoardWidth() {
return width;
}
/**
* Returns the number of lines removed since the last clear().
*
* @return the number of lines removed since the last clear call
*/
public int getRemovedLines() {
return removedLines;
}
/**
* Returns the color of an individual square on the board. If the
* square is empty or outside the board, null will be returned.
*
* @param x the horizontal position (0 <= x < width)
* @param y the vertical position (0 <= y < height)
*
* @return the square color, or null for none
*/
public Color getSquareColor(int x, int y) {
if (x < 0 || x >= width || y < 0 || y >= height) {
return null;
} else {
return matrix[y][x];
}
}
/**
* Changes the color of an individual square on the board. The
* square will be marked as in need of a repaint, but the
* graphical component will NOT be repainted until the update()
* method is called.
*
* @param x the horizontal position (0 <= x < width)
* @param y the vertical position (0 <= y < height)
* @param color the new square color, or null for empty
*/
public void setSquareColor(int x, int y, Color color) {
if (x < 0 || x >= width || y < 0 || y >= height) {
return;
}
matrix[y][x] = color;
if (component != null) {
component.invalidateSquare(x, y);
}
}
/**
* Sets a message to display on the square board. This is supposed
* to be used when the board is not being used for active drawing,
* as it slows down the drawing considerably.
*
* @param message a message to display, or null to remove a
* previous message
*/
public void setMessage(String message) {
this.message = message;
if (component != null) {
component.redrawAll();
}
}
/**
* Clears the board, i.e. removes all the colored squares. As
* side-effects, the number of removed lines will be reset to
* zero, and the component will be repainted immediately.
*/
public void clear() {
removedLines = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
this.matrix[y][x] = null;
}
}
if (component != null) {
component.redrawAll();
}
}
/**
* Removes all full lines. All lines above a removed line will be
* moved downward one step, and a new empty line will be added at
* the top. After removing all full lines, the component will be
* repainted.
*
* @see #hasFullLines
*/
public void removeFullLines() {
boolean repaint = false;
// Remove full lines
for (int y = height - 1; y >= 0; y--) {
if (isLineFull(y)) {
removeLine(y);
removedLines++;
repaint = true;
y++;
}
}
// Repaint if necessary
if (repaint && component != null) {
component.redrawAll();
}
}
/**
* Removes a single line. All lines above are moved down one step,
* and a new empty line is added at the top. No repainting will be
* done after removing the line.
*
* @param y the vertical position (0 <= y < height)
*/
private void removeLine(int y) {
if (y < 0 || y >= height) {
return;
}
for (; y > 0; y--) {
for (int x = 0; x < width; x++) {
matrix[y][x] = matrix[y - 1][x];
}
}
for (int x = 0; x < width; x++) {
matrix[0][x] = null;
}
}
/**
* Updates the graphical component. Any squares previously changed
* will be repainted by this method.
*/
public void update() {
component.redraw();
}
/**
* The graphical component that paints the square board. This is
* implemented as an inner class in order to better abstract the
* detailed information that must be sent between the square board
* and its graphical representation.
*/
private class SquareBoardComponent extends JComponent {
/**
* The component size. If the component has been resized, that
* will be detected when the paint method executes. If this
* value is set to null, the component dimensions are unknown.
*/
private Dimension size = null;
/**
* The component insets. The inset values are used to create a
* border around the board to compensate for a skewed aspect
* ratio. If the component has been resized, the insets values
* will be recalculated when the paint method executes.
*/
private Insets insets = new Insets(0, 0, 0, 0);
/**
* The square size in pixels. This value is updated when the
* component size is changed, i.e. when the <code>size</code>
* variable is modified.
*/
private Dimension squareSize = new Dimension(0, 0);
/**
* An image used for double buffering. The board is first
* painted onto this image, and that image is then painted
* onto the real surface in order to avoid making the drawing
* process visible to the user. This image is recreated each
* time the component size changes.
*/
private Image bufferImage = null;
/**
* A clip boundary buffer rectangle. This rectangle is used
* when calculating the clip boundaries, in order to avoid
* allocating a new clip rectangle for each board square.
*/
private Rectangle bufferRect = new Rectangle();
/**
* The board message color.
*/
private Color messageColor = Color.white;
/**
* A lookup table containing lighter versions of the colors.
* This table is used to avoid calculating the lighter
* versions of the colors for each and every square drawn.
*/
private Hashtable lighterColors = new Hashtable();
/**
* A lookup table containing darker versions of the colors.
* This table is used to avoid calculating the darker
* versions of the colors for each and every square drawn.
*/
private Hashtable darkerColors = new Hashtable();
/**
* A flag set when the component has been updated.
*/
private boolean updated = true;
/**
* A bounding box of the squares to update. The coordinates
* used in the rectangle refers to the square matrix.
*/
private Rectangle updateRect = new Rectangle();
/**
* Creates a new square board component.
*/
public SquareBoardComponent() {
setBackground(Configuration.getColor("board.background",
"#000000"));
messageColor = Configuration.getColor("board.message",
"#ffffff");
}
/**
* Adds a square to the set of squares in need of redrawing.
*
* @param x the horizontal position (0 <= x < width)
* @param y the vertical position (0 <= y < height)
*/
public void invalidateSquare(int x, int y) {
if (updated) {
updated = false;
updateRect.x = x;
updateRect.y = y;
updateRect.width = 0;
updateRect.height = 0;
} else {
if (x < updateRect.x) {
updateRect.width += updateRect.x - x;
updateRect.x = x;
} else if (x > updateRect.x + updateRect.width) {
updateRect.width = x - updateRect.x;
}
if (y < updateRect.y) {
updateRect.height += updateRect.y - y;
updateRect.y = y;
} else if (y > updateRect.y + updateRect.height) {
updateRect.height = y - updateRect.y;
}
}
}
/**
* Redraws all the invalidated squares. If no squares have
* been marked as in need of redrawing, no redrawing will
* occur.
*/
public void redraw() {
Graphics g;
if (!updated) {
updated = true;
g = getGraphics();
if (g==null) return;
g.setClip(insets.left + updateRect.x * squareSize.width,
insets.top + updateRect.y * squareSize.height,
(updateRect.width + 1) * squareSize.width,
(updateRect.height + 1) * squareSize.height);
paint(g);
}
}
/**
* Redraws the whole component.
*/
public void redrawAll() {
Graphics g;
updated = true;
g = getGraphics();
if (g==null) return;
g.setClip(insets.left,
insets.top,
width * squareSize.width,
height * squareSize.height);
paint(g);
}
/**
* Returns true as this component is double buffered.
*
* @return true as this component is double buffered
*/
public boolean isDoubleBuffered() {
return true;
}
/**
* Returns the preferred size of this component.
*
* @return the preferred component size
*/
public Dimension getPreferredSize() {
return new Dimension(width * 20, height * 20);
}
/**
* Returns the minimum size of this component.
*
* @return the minimum component size
*/
public Dimension getMinimumSize() {
return getPreferredSize();
}
/**
* Returns the maximum size of this component.
*
* @return the maximum component size
*/
public Dimension getMaximumSize() {
return getPreferredSize();
}
/**
* Returns a lighter version of the specified color. The
* lighter color will looked up in a hashtable, making this
* method fast. If the color is not found, the ligher color
* will be calculated and added to the lookup table for later
* reference.
*
* @param c the base color
*
* @return the lighter version of the color
*/
private Color getLighterColor(Color c) {
Color lighter;
lighter = (Color) lighterColors.get(c);
if (lighter == null) {
lighter = c.brighter().brighter();
lighterColors.put(c, lighter);
}
return lighter;
}
/**
* Returns a darker version of the specified color. The
* darker color will looked up in a hashtable, making this
* method fast. If the color is not found, the darker color
* will be calculated and added to the lookup table for later
* reference.
*
* @param c the base color
*
* @return the darker version of the color
*/
private Color getDarkerColor(Color c) {
Color darker;
darker = (Color) darkerColors.get(c);
if (darker == null) {
darker = c.darker().darker();
darkerColors.put(c, darker);
}
return darker;
}
/**
* Paints this component indirectly. The painting is first
* done to a buffer image, that is then painted directly to
* the specified graphics context.
*
* @param g the graphics context to use
*/
public synchronized void paint(Graphics g) {
Graphics bufferGraphics;
Rectangle rect;
// Handle component size change
if (size == null || !size.equals(getSize())) {
size = getSize();
squareSize.width = size.width / width;
squareSize.height = size.height / height;
//if (squareSize.width <= squareSize.height) {
// squareSize.height = squareSize.width;
//} else {
// squareSize.width = squareSize.height;
//}
insets.left = (size.width - width * squareSize.width) / 2;
insets.right = insets.left;
insets.top = 0;
insets.bottom = size.height - height * squareSize.height;
bufferImage = createImage(width * squareSize.width,
height * squareSize.height);
}
// Paint component in buffer image
rect = g.getClipBounds();
bufferGraphics = bufferImage.getGraphics();
bufferGraphics.setClip(rect.x - insets.left,
rect.y - insets.top,
rect.width,
rect.height);
doPaintComponent(bufferGraphics);
// Paint image buffer
g.drawImage(bufferImage,
insets.left,
insets.top,
getBackground(),
null);
}
/**
* Paints this component directly. All the squares on the
* board will be painted directly to the specified graphics
* context.
*
* @param g the graphics context to use
*/
private void doPaintComponent(Graphics g) {
// Paint background
g.setColor(getBackground());
g.fillRect(0,
0,
width * squareSize.width,
height * squareSize.height);
// Paint squares
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if (matrix[y][x] != null) {
paintSquare(g, x, y);
}
}
}
// Paint message
if (message != null) {
paintMessage(g, message);
}
}
/**
* Paints a single board square. The specified position must
* contain a color object.
*
* @param g the graphics context to use
* @param x the horizontal position (0 <= x < width)
* @param y the vertical position (0 <= y < height)
*/
private void paintSquare(Graphics g, int x, int y) {
Color color = matrix[y][x];
int xMin = x * squareSize.width;
int yMin = y * squareSize.height;
int xMax = xMin + squareSize.width - 1;
int yMax = yMin + squareSize.height - 1;
int i;
// Skip drawing if not visible
bufferRect.x = xMin;
bufferRect.y = yMin;
bufferRect.width = squareSize.width;
bufferRect.height = squareSize.height;
if (!bufferRect.intersects(g.getClipBounds())) {
return;
}
// Fill with base color
g.setColor(color);
g.fillRect(xMin, yMin, squareSize.width, squareSize.height);
// Draw brighter lines
g.setColor(getLighterColor(color));
for (i = 0; i < squareSize.width / 10; i++) {
g.drawLine(xMin + i, yMin + i, xMax - i, yMin + i);
g.drawLine(xMin + i, yMin + i, xMin + i, yMax - i);
}
// Draw darker lines
g.setColor(getDarkerColor(color));
for (i = 0; i < squareSize.width / 10; i++) {
g.drawLine(xMax - i, yMin + i, xMax - i, yMax - i);
g.drawLine(xMin + i, yMax - i, xMax - i, yMax - i);
}
}
/**
* Paints a board message. The message will be drawn at the
* center of the component.
*
* @param g the graphics context to use
* @param msg the string message
*/
private void paintMessage(Graphics g, String msg) {
int fontWidth;
int offset;
int x;
int y;
// Find string font width
g.setFont(new Font("SansSerif", Font.BOLD, squareSize.width + 4));
fontWidth = g.getFontMetrics().stringWidth(msg);
// Find centered position
x = (width * squareSize.width - fontWidth) / 2;
y = height * squareSize.height / 2;
// Draw black version of the string
offset = squareSize.width / 10;
g.setColor(Color.black);
g.drawString(msg, x - offset, y - offset);
g.drawString(msg, x - offset, y);
g.drawString(msg, x - offset, y - offset);
g.drawString(msg, x, y - offset);
g.drawString(msg, x, y + offset);
g.drawString(msg, x + offset, y - offset);
g.drawString(msg, x + offset, y);
g.drawString(msg, x + offset, y + offset);
// Draw white version of the string
g.setColor(messageColor);
g.drawString(msg, x, y);
}
}
}
/*
* @(#)Game.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.
*/
/**
* The Tetris game. This class controls all events in the game and
* handles all the game logics. The game is started through user
* interaction with the graphical game component provided by this
* class.
*
* @version 1.2
* @author Per Cederberg, per@percederberg.net
*/
class Game extends Object
{
public static final int STATE_GETREADY =1;
public static final int STATE_PLAYING = 2;
public static final int STATE_PAUSED = 3;
public static final int STATE_GAMEOVER =4;
/**
* The PropertyChangeSupport Object able to register listener and dispatch events to them.
*/
private final PropertyChangeSupport PCS = new PropertyChangeSupport(this);
/**
* The main square board. This board is used for the game itself.
*/
private final SquareBoard board;
/**
* The preview square board. This board is used to display a
* preview of the figures.
*/
private final SquareBoard previewBoard = new SquareBoard(5, 5);
/**
* The figures used on both boards. All figures are reutilized in
* order to avoid creating new objects while the game is running.
* Special care has to be taken when the preview figure and the
* current figure refers to the same object.
*/
private Figure[] figures = {
new Figure(Figure.SQUARE_FIGURE),
new Figure(Figure.LINE_FIGURE),
new Figure(Figure.S_FIGURE),
new Figure(Figure.Z_FIGURE),
new Figure(Figure.RIGHT_ANGLE_FIGURE),
new Figure(Figure.LEFT_ANGLE_FIGURE),
new Figure(Figure.TRIANGLE_FIGURE)
};
/**
* The thread that runs the game. When this variable is set to
* null, the game thread will terminate.
*/
private final GameThread thread;
/**
* The game level. The level will be increased for every 20 lines
* removed from the square board.
*/
private int level = 1;
/**
* The current score. The score is increased for every figure that
* is possible to place on the main board.
*/
private int score = 0;
/**
* The current figure. The figure will be updated when
*/
private Figure figure = null;
/**
* The next figure.
*/
private Figure nextFigure = null;
/**
* The rotation of the next figure.
*/
private int nextRotation = 0;
/**
* The figure preview flag. If this flag is set, the figure
* will be shown in the figure preview board.
*/
private boolean preview = true;
/**
* The move lock flag. If this flag is set, the current figure
* cannot be moved. This flag is set when a figure is moved all
* the way down, and reset when a new figure is displayed.
*/
private boolean moveLock = false;
/**
*
*/
private int state;
/**
* Creates a new Tetris game. The square board will be given
* the default size of 10x20.
*/
public Game() {
this(10, 20);
}
/**
* Creates a new Tetris game. The square board will be given
* the specified size.
*
* @param width the width of the square board (in positions)
* @param height the height of the square board (in positions)
*/
public Game(int width, int height) {
board = new SquareBoard(width, height);
thread = new GameThread();
handleGetReady();
board.getComponent().setFocusable(true);
board.getComponent().addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
handleKeyEvent(e);
}
});
}
/**
* Adds a PropertyChangeListener to this Game.
*
* This is the list the Events that can be fired:
*
* name: "state"
* value: new current state (int) one of those: STATE_OVER,STATE_PLAYING,STATE_PAUSED
* when: fired when the state changes.
*
* name: "level"
* value: current level (int)
* when: fired when the player moves to the next level.
*
* name: "score"
* value: current score (int)
* when: fired when the player increases his/her score.
*
* name: "lines"
* value: number of 'removed' lines (int)
* when: fired when the player removes one or more lines.
*
* @param l the property change listener which is going to be notified.
*/
public void addPropertyChangeListener(PropertyChangeListener l)
{
PCS.addPropertyChangeListener(l);
}
/**
* Removes this propertyChangeListener
* @param l the PropertyChangeListener object to remove.
*/
public void removePropertyChangeListener(PropertyChangeListener l)
{
PCS.removePropertyChangeListener(l);
}
/**
* Gets the current 'state'.
* One of the following:
* STATE_GETREADY,STATE_PLAYING,STATE_PAUSED,STATE_GAMEOVER.
* @return the current state.
*/
public int getState()
{
return state;
}
/**
* Gets the current level.
* @return the current level.
*/
public int getLevel()
{
return level;
}
/**
* Gets the current score.
* @return the current score.
**/
public int getScore()
{
return score;
}
/**
* Gets the number of lines that have been removed since the game started.
* @return the number of removed lines.
*/
public int getRemovedLines()
{
return board.getRemovedLines();
}
/**
* Gets the java.awt.Component for the board.
* @return the gui component for the board.
*/
public Component getSquareBoardComponent()
{
return board.getComponent();
}
/**
* Gets the java.awt.Component for the preview board (5x5)
* @return the gui component for the board.
*/
public Component getPreviewBoardComponent()
{
return previewBoard.getComponent();
}
/**
* Initializes the game ready if the state is on STATE_GAMEOVER
* otherwise it does nothing.
**/
public void init()
{
if (state == STATE_GAMEOVER)
{
handleGetReady();
}
}
/**
* Starts the game. (No matter what the current state is)
**/
public void start()
{
handleStart();
}
/**
* Pauses the game if the state is on STATE_PLAYING
* otherwise it does nothing.
**/
public void pause()
{
if (state == STATE_PLAYING)
{
handlePause();
}
}
/**
* Resumes the game if the state is on STATE_PAUSED
* otherwise it does nothing.
**/
public void resume()
{
if (state == STATE_PAUSED)
{
handleResume();
}
}
/**
* Terminates the game. (No matter what the current state is)
**/
public void terminate()
{
handleGameOver();
}
/**
* Handles a game start event. Both the main and preview square
* boards will be reset, and all other game parameters will be
* reset. Finally the game thread will be launched.
*/
private void handleStart() {
// Reset score and figures
level = 1;
score = 0;
figure = null;
nextFigure = randomFigure();
nextFigure.rotateRandom();
nextRotation = nextFigure.getRotation();
// Reset components
state = STATE_PLAYING;
board.setMessage(null);
board.clear();
previewBoard.clear();
handleLevelModification();
handleScoreModification();
PCS.firePropertyChange("state",-1, STATE_PLAYING );
// Start game thread
thread.reset();
}
/**
* Handles a game over event. This will stop the game thread,
* reset all figures and print a game over message.
*/
private void handleGameOver() {
// Stop game thred
thread.setPaused(true);
// Reset figures
if (figure != null) {
figure.detach();
}
figure = null;
if (nextFigure != null) {
nextFigure.detach();
}
nextFigure = null;
// Handle components
state = STATE_GAMEOVER;
board.setMessage("Game Over");
PCS.firePropertyChange("state",-1, STATE_GAMEOVER );
}
/**
* Handles a getReady event.
* This will print a 'get ready' message on the game board.
*/
private void handleGetReady()
{
board.setMessage("Get Ready");
board.clear();
previewBoard.clear();
state = STATE_GETREADY;
PCS.firePropertyChange("state",-1, STATE_GETREADY );
}
/**
* Handles a game pause event. This will pause the game thread and
* print a pause message on the game board.
*/
private void handlePause() {
thread.setPaused(true);
state = STATE_PAUSED;
board.setMessage("Paused");
PCS.firePropertyChange("state",-1, STATE_PAUSED );
}
/**
* Handles a game resume event. This will resume the game thread
* and remove any messages on the game board.
*/
private void handleResume() {
state = STATE_PLAYING;
board.setMessage(null);
thread.setPaused(false);
PCS.firePropertyChange("state",-1,STATE_PLAYING);
}
/**
* Handles a level modification event. This will modify the level
* label and adjust the thread speed.
*/
private void handleLevelModification() {
PCS.firePropertyChange("level",-1,level);
thread.adjustSpeed();
}
/**
* Handle a score modification event. This will modify the score
* label.
*/
private void handleScoreModification() {
PCS.firePropertyChange("score",-1,score);
}
/**
* Handles a figure start event. This will move the next figure
* to the current figure position, while also creating a new
* preview figure. If the figure cannot be introduced onto the
* game board, a game over event will be launched.
*/
private void handleFigureStart() {
int rotation;
// Move next figure to current
figure = nextFigure;
moveLock = false;
rotation = nextRotation;
nextFigure = randomFigure();
nextFigure.rotateRandom();
nextRotation = nextFigure.getRotation();
// Handle figure preview
if (preview) {
previewBoard.clear();
nextFigure.attach(previewBoard, true);
nextFigure.detach();
}
// Attach figure to game board
figure.setRotation(rotation);
if (!figure.attach(board, false)) {
previewBoard.clear();
figure.attach(previewBoard, true);
figure.detach();
handleGameOver();
}
}
/**
* Handles a figure landed event. This will check that the figure
* is completely visible, or a game over event will be launched.
* After this control, any full lines will be removed. If no full
* lines could be removed, a figure start event is launched
* directly.
*/
private void handleFigureLanded() {
// Check and detach figure
if (figure.isAllVisible()) {
score += 10;
handleScoreModification();
} else {
handleGameOver();
return;
}
figure.detach();
figure = null;
// Check for full lines or create new figure
if (board.hasFullLines()) {
board.removeFullLines();
PCS.firePropertyChange("lines", -1, board.getRemovedLines());
if (level < 9 && board.getRemovedLines() / 20 > level) {
level = board.getRemovedLines() / 20;
handleLevelModification();
}
} else {
handleFigureStart();
}
}
/**
* Handles a timer event. This will normally move the figure down
* one step, but when a figure has landed or isn't ready other
* events will be launched. This method is synchronized to avoid
* race conditions with other asynchronous events (keyboard and
* mouse).
*/
private synchronized void handleTimer() {
if (figure == null) {
handleFigureStart();
} else if (figure.hasLanded()) {
handleFigureLanded();
} else {
figure.moveDown();
}
}
/**
* Handles a button press event. This will launch different events
* depending on the state of the game, as the button semantics
* change as the game changes. This method is synchronized to
* avoid race conditions with other asynchronous events (timer and
* keyboard).
*/
private synchronized void handlePauseOnOff() {
if (nextFigure == null) {
handleStart();
} else if (thread.isPaused()) {
handleResume();
} else {
handlePause();
}
}
/**
* Handles a keyboard event. This will result in different actions
* being taken, depending on the key pressed. In some cases, other
* events will be launched. This method is synchronized to avoid
* race conditions with other asynchronous events (timer and
* mouse).
*
* @param e the key event
*/
private synchronized void handleKeyEvent(KeyEvent e)
{
// Handle start (any key to start !!!)
if (state == STATE_GETREADY)
{
handleStart();
return;
}
// pause and resume
if (e.getKeyCode() == KeyEvent.VK_P) {
handlePauseOnOff();
return;
}
// Don't proceed if stopped or paused
if (figure == null || moveLock || thread.isPaused()) {
return;
}
// Handle remaining key events
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT:
figure.moveLeft();
break;
case KeyEvent.VK_RIGHT:
figure.moveRight();
break;
case KeyEvent.VK_DOWN:
figure.moveAllWayDown();
moveLock = true;
break;
case KeyEvent.VK_UP:
case KeyEvent.VK_SPACE:
if (e.isControlDown()) {
figure.rotateRandom();
} else if (e.isShiftDown()) {
figure.rotateClockwise();
} else {
figure.rotateCounterClockwise();
}
break;
case KeyEvent.VK_S:
if (level < 9) {
level++;
handleLevelModification();
}
break;
case KeyEvent.VK_N:
preview = !preview;
if (preview && figure != nextFigure) {
nextFigure.attach(previewBoard, true);
nextFigure.detach();
} else {
previewBoard.clear();
}
break;
}
}
/**
* Returns a random figure. The figures come from the figures
* array, and will not be initialized.
*
* @return a random figure
*/
private Figure randomFigure() {
return figures[(int) (Math.random() * figures.length)];
}
/**
* The game time thread. This thread makes sure that the timer
* events are launched appropriately, making the current figure
* fall. This thread can be reused across games, but should be set
* to paused state when no game is running.
*/
private class GameThread extends Thread {
/**
* The game pause flag. This flag is set to true while the
* game should pause.
*/
private boolean paused = true;
/**
* The number of milliseconds to sleep before each automatic
* move. This number will be lowered as the game progresses.
*/
private int sleepTime = 500;
/**
* Creates a new game thread with default values.
*/
public GameThread() {
}
/**
* Resets the game thread. This will adjust the speed and
* start the game thread if not previously started.
*/
public void reset() {
adjustSpeed();
setPaused(false);
if (!isAlive()) {
this.start();
}
}
/**
* Checks if the thread is paused.
*
* @return true if the thread is paused, or
* false otherwise
*/
public boolean isPaused() {
return paused;
}
/**
* Sets the thread pause flag.
*
* @param paused the new paused flag value
*/
public void setPaused(boolean paused) {
this.paused = paused;
}
/**
* Adjusts the game speed according to the current level. The
* sleeping time is calculated with a function making larger
* steps initially an smaller as the level increases. A level
* above ten (10) doesn't have any further effect.
*/
public void adjustSpeed() {
sleepTime = 4500 / (level + 5) - 250;
if (sleepTime < 50) {
sleepTime = 50;
}
}
/**
* Runs the game.
*/
public void run() {
while (thread == this) {
// Make the time step
handleTimer();
// Sleep for some time
try {
Thread.sleep(sleepTime);
} catch (InterruptedException ignore) {
// Do nothing
}
// Sleep if paused
while (paused && thread == this) {
try {
Thread.sleep(1000);
} catch (InterruptedException ignore) {
// Do nothing
}
}
}
}
}
}
/*
* @(#)Configuration.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.
*/
/**
* A program configuration. This class provides static methods for
* simplifying the reading of configuration parameters. It also
* provides some methods for transforming string values into more
* useful objects.
*
* @author Per Cederberg, per@percederberg.net
* @version 1.2
*/
class Configuration extends Object {
/**
* The internal configuration property values. This lookup table
* is used to avoid setting configuration parameters in the system
* properties, as some programs (applets) do not have the security
* permissions to set system properties.
*/
private static Hashtable config = new Hashtable();
/**
* Returns a configuration parameter value.
*
* @param key the configuration parameter key
*
* @return the configuration parameter value, or
* null if not set
*/
public static String getValue(String key) {
if (config.containsKey(key)) {
return config.get(key).toString();
} else {
try {
return System.getProperty(key);
} catch (SecurityException ignore) {
return null;
}
}
}
/**
* Returns a configuration parameter value. If the configuration
* parameter is not set, a default value will be returned instead.
*
* @param key the configuration parameter key
* @param def the default value to use
*
* @return the configuration parameter value, or
* the default value if not set
*/
public static String getValue(String key, String def) {
String value = getValue(key);
return (value == null) ? def : value;
}
/**
* Sets a configuration parameter value.
*
* @param key the configuration parameter key
* @param value the configuration parameter value
*/
public static void setValue(String key, String value) {
config.put(key, value);
}
/**
* Returns the color configured for the specified key. The key
* will be prepended with "tetris.color." and the value will be
* read from the system properties. The color value must be
* specified in hexadecimal web format, i.e. in the "#RRGGBB"
* format. If the default color isn't in a valid format, white
* will be returned.
*
* @param key the configuration parameter key
* @param def the default value
*
* @return the color specified in the configuration, or
* a default color value
*/
public static Color getColor(String key, String def) {
String value = getValue("tetris.color." + key, def);
Color color;
color = parseColor(value);
if (color != null) {
return color;
}
color = parseColor(def);
if (color != null) {
return color;
} else {
return Color.white;
}
}
/**
* Parses a web color string. If the color value couldn't be
* parsed correctly, null will be returned.
*
* @param value the color value to parse
*
* @return the color represented by the string, or
* null if the string was malformed
*/
private static Color parseColor(String value) {
if (!value.startsWith("#")) {
return null;
}
try {
return new Color(Integer.parseInt(value.substring(1), 16));
} catch (NumberFormatException ignore) {
return null;
}
}
}
/*
* @(#)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.
*/
/**
* 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
*/
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);
}
}
}
Related examples in the same category