/*
* @(#)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.
*/
package net.percederberg.tetris;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.util.Hashtable;
import org.jopenray.server.thinclient.ThinClient;
/**
* 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
*/
public class SquareBoard {
/**
* The board width (in squares)
*/
private int width = 0;
/**
* The board height (in squares).
*/
private int height = 0;
/**
* 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 SquareBoardComponent component = null;
public ThinClient client;
/**
* 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(ThinClient client, int width, int height) {
this.client = client;
this.width = width;
this.height = height;
this.matrix = new Color[height][width];
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() {
if (component == null) {
component = new SquareBoardComponent();
}
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() {
if (component != null) {
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 Component {
/**
* 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 BufferedImage 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();
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();
g.setClip(insets.left, insets.top, width * squareSize.width, height
* squareSize.height);
paint(g);
}
public Graphics getGraphics() {
BufferedImage i = new BufferedImage(1, 1,
BufferedImage.TYPE_INT_ARGB);
return i.getGraphics();
}
/**
* 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 = new BufferedImage(width * squareSize.width,
height * squareSize.height, BufferedImage.TYPE_INT_ARGB);
}
// 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);
paintComponent(bufferGraphics);
// Paint image buffer
// g.drawImage(bufferImage, insets.left, insets.top,
// getBackground(),
// null);
client.getWriter().sendImage(bufferImage,
(client.getScreenWidth() - bufferImage.getWidth()) / 2,
client.getScreenHeight() - bufferImage.getHeight() - 2);
}
public Dimension getSize() {
return new Dimension(640, 480);
}
/**
* 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 paintComponent(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);
}
}
}