본문 바로가기
Java

[Java] awt 테트리스 - 실전편

by teamnova 2023. 6. 17.

저번 글 : https://stickode.tistory.com/822

 

안녕하세요. 저번 시간에 이어서, 오늘은 테트리스를 게임답게 만들어보겠습니다. 우선 모든 코드를 올려두고, 모듈화한 코드들을 설명하겠습니다. 총 5부에 걸쳐서 업로드 할 예정입니다. 각 모듈들(Configuration, Figure, Game, SquareBoard, ,,,)에 대한 설명은 각 게시물로 따로 올리도록 하겠습니다. 오늘 업로드된 코드들에는 모듈에 주석이 없습니다.

 

 

우선 디렉토리 구조는 다음과 같습니다.

Tetris.java

import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Color;
import java.awt.Container;
import java.awt.Font;
import java.awt.Frame;
import java.awt.GridLayout;
import java.awt.TextArea;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

/**
 * 테트리스 게임의 메인 클래스입니다. 이 클래스에는 독립적으로 실행되는 애플리케이션 또는 웹 페이지 내의 애플릿으로 게임을 실행하기 위한 필요한 메서드가 포함되어 있습니다.
 */
public class Tetris {

    /**
     * 애플릿에서 실행되는 테트리스 게임입니다.
     */
    private Game game = null;

    /**
     * 독립 실행형 메인 루틴입니다.
     *
     * @param args 명령 줄 인수
     */
    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(" 득점자 점수판                  \n" +
                " -----------------------------\n\n" +
                " PLAYER     LEVEL    SCORE    \n\n" +
                " 김씨         12 1  50280     \n" +
                " 양씨         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();
                            }
                        });
                        // 점수 표시...
                    }
                }
            }
        });

        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();

        // 프레임 창 리스너 추가
        frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });

        // 프레임 보이기 (게임 시작)
        frame.show();
    }
}

 

 

 

Configuration.java

import java.awt.*;
import java.util.Hashtable;

public class Configuration extends Object {

    private static Hashtable config = new Hashtable();

    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;
            }
        }
    }

    public static String getValue(String key, String def) {
        String value = getValue(key);

        return (value == null) ? def : value;
    }

    public static void setValue(String key, String value) {
        config.put(key, 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;
        }
    }

    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

import java.awt.*;

public class Figure extends Object {

    public static final int SQUARE_FIGURE = 1;

    public static final int LINE_FIGURE = 2;

    public static final int S_FIGURE = 3;

    public static final int Z_FIGURE = 4;

    public static final int RIGHT_ANGLE_FIGURE = 5;

    public static final int LEFT_ANGLE_FIGURE = 6;

    public static final int TRIANGLE_FIGURE = 7;

    private SquareBoard board = null;

    private int xPos = 0;

    private int yPos = 0;

    private int orientation = 0;

    private int maxOrientation = 4;

    private int[] shapeX = new int[4];

    private int[] shapeY = new int[4];

    private Color color = Color.white;

    public Figure(int type) throws IllegalArgumentException {
        initialize(type);
    }

    private void initialize(int type) throws IllegalArgumentException {

        // 기본 변수 초기화
        board = null;
        xPos = 0;
        yPos = 0;
        orientation = 0;

        // 도형 유형 변수 초기화
        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);
        }
    }

    public boolean isAttached() {
        return board != null;
    }

    public boolean attach(SquareBoard board, boolean center) {
        int newX;
        int newY;
        int i;

        // 이전 연결 여부 확인
        if (isAttached()) {
            detach();
        }

        // 위치 초기화 (올바른 조작을 위해)
        xPos = 0;
        yPos = 0;

        // 위치 계산
        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);
                }
            }
        }

        // 위치 확인
        this.board = board;
        if (!canMoveTo(newX, newY, orientation)) {
            this.board = null;
            return false;
        }

        // 도형 그리기
        xPos = newX;
        yPos = newY;
        paint(color);
        board.update();

        return true;
    }

    public void detach() {
        board = null;
    }

    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;
    }

    public boolean hasLanded() {
        return !isAttached() || !canMoveTo(xPos, yPos + 1, orientation);
    }

    public void moveLeft() {
        if (isAttached() && canMoveTo(xPos - 1, yPos, orientation)) {
            paint(null);
            xPos--;
            paint(color);
            board.update();
        }
    }

    public void moveRight() {
        if (isAttached() && canMoveTo(xPos + 1, yPos, orientation)) {
            paint(null);
            xPos++;
            paint(color);
            board.update();
        }
    }

    public void moveDown() {
        if (isAttached() && canMoveTo(xPos, yPos + 1, orientation)) {
            paint(null);
            yPos++;
            paint(color);
            board.update();
        }
    }

    public void moveAllWayDown() {
        int y = yPos;

        // 보드 확인
        if (!isAttached()) {
            return;
        }

        // 가장 낮은 위치 찾기
        while (canMoveTo(xPos, y + 1, orientation)) {
            y++;
        }

        // 업데이트
        if (y != yPos) {
            paint(null);
            yPos = y;
            paint(color);
            board.update();
        }
    }

    public int getRotation() {
        return orientation;
    }

    public void setRotation(int rotation) {
        int newOrientation;

        // 새로운 방향 설정
        newOrientation = rotation % maxOrientation;

        // 새 위치 확인
        if (!isAttached()) {
            orientation = newOrientation;
        } else if (canMoveTo(xPos, yPos, newOrientation)) {
            paint(null);
            orientation = newOrientation;
            paint(color);
            board.update();
        }
    }

    public void rotateRandom() {
        setRotation((int) (Math.random() * 4.0) % maxOrientation);
    }

    public void rotateClockwise() {
        if (maxOrientation == 1) {
            return;
        } else {
            setRotation((orientation + 1) % maxOrientation);
        }
    }

    public void rotateCounterClockwise() {
        if (maxOrientation == 1) {
            return;
        } else {
            setRotation((orientation + 3) % 4);
        }
    }

    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;
    }

    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;
    }

    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; // 발생하지 않아야 함
        }
    }

    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; // 발생하지 않아야 함
        }
    }

    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);
        }
    }
}

 

 

 

Game.java

import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

public 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;


    private final PropertyChangeSupport PCS = new PropertyChangeSupport(this);

    private final SquareBoard board;

    private final SquareBoard previewBoard = new SquareBoard(5, 5);

    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)
    };

    private final GameThread thread;

    private int level = 1;

    private int score = 0;

    private Figure figure = null;

    private Figure nextFigure = null;

    private int nextRotation = 0;

    private boolean preview = true;

    private boolean moveLock = false;

    private int state;

    public Game() {
        this(10, 20);
    }

    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);
            }
        });
    }

    public void addPropertyChangeListener(PropertyChangeListener l) {
        PCS.addPropertyChangeListener(l);
    }

    public void removePropertyChangeListener(PropertyChangeListener l) {
        PCS.removePropertyChangeListener(l);
    }

    public int getState() {
        return state;
    }

    public int getLevel() {
        return level;
    }

    public int getScore() {
        return score;
    }

    public int getRemovedLines() {
        return board.getRemovedLines();
    }

    public Component getSquareBoardComponent() {
        return board.getComponent();
    }

    public Component getPreviewBoardComponent() {
        return previewBoard.getComponent();
    }

    public void init() {
        if (state == STATE_GAMEOVER) {
            handleGetReady();
        }
    }

    public void start() {
        handleStart();
    }

    public void pause() {
        if (state == STATE_PLAYING) {
            handlePause();
        }
    }

    public void resume() {
        if (state == STATE_PAUSED) {
            handleResume();
        }
    }

    public void terminate() {
        handleGameOver();
    }

    private void handleStart() {

        level = 1;
        score = 0;
        figure = null;
        nextFigure = randomFigure();
        nextFigure.rotateRandom();
        nextRotation = nextFigure.getRotation();

        state = STATE_PLAYING;
        board.setMessage(null);
        board.clear();
        previewBoard.clear();
        handleLevelModification();
        handleScoreModification();

        PCS.firePropertyChange("state", -1, STATE_PLAYING);
        thread.reset();
    }

    private void handleGameOver() {

        thread.setPaused(true);

        if (figure != null) {
            figure.detach();
        }
        figure = null;
        if (nextFigure != null) {
            nextFigure.detach();
        }
        nextFigure = null;

        state = STATE_GAMEOVER;
        board.setMessage("Game Over");
        PCS.firePropertyChange("state", -1, STATE_GAMEOVER);
    }

    private void handleGetReady() {
        board.setMessage("Get Ready");
        board.clear();
        previewBoard.clear();
        state = STATE_GETREADY;
        PCS.firePropertyChange("state", -1, STATE_GETREADY);
    }

    private void handlePause() {
        thread.setPaused(true);
        state = STATE_PAUSED;
        board.setMessage("Paused");
        PCS.firePropertyChange("state", -1, STATE_PAUSED);
    }

    private void handleResume() {
        state = STATE_PLAYING;
        board.setMessage(null);
        thread.setPaused(false);
        PCS.firePropertyChange("state", -1, STATE_PLAYING);
    }

    private void handleLevelModification() {
        PCS.firePropertyChange("level", -1, level);
        thread.adjustSpeed();
    }

    private void handleScoreModification() {
        PCS.firePropertyChange("score", -1, score);
    }

    private void handleFigureStart() {
        int rotation;

        figure = nextFigure;
        moveLock = false;
        rotation = nextRotation;
        nextFigure = randomFigure();
        nextFigure.rotateRandom();
        nextRotation = nextFigure.getRotation();

        if (preview) {
            previewBoard.clear();
            nextFigure.attach(previewBoard, true);
            nextFigure.detach();
        }

        figure.setRotation(rotation);
        if (!figure.attach(board, false)) {
            previewBoard.clear();
            figure.attach(previewBoard, true);
            figure.detach();
            handleGameOver();
        }
    }

    private void handleFigureLanded() {

        if (figure.isAllVisible()) {
            score += 10;
            handleScoreModification();
        } else {
            handleGameOver();
            return;
        }
        figure.detach();
        figure = null;

        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();
        }
    }

    private synchronized void handleTimer() {
        if (figure == null) {
            handleFigureStart();
        } else if (figure.hasLanded()) {
            handleFigureLanded();
        } else {
            figure.moveDown();
        }
    }

    private synchronized void handlePauseOnOff() {
        if (nextFigure == null) {
            handleStart();
        } else if (thread.isPaused()) {
            handleResume();
        } else {
            handlePause();
        }
    }

    private synchronized void handleKeyEvent(KeyEvent e) {
        if (state == STATE_GETREADY) {
            handleStart();
            return;
        }

        if (e.getKeyCode() == KeyEvent.VK_P) {
            handlePauseOnOff();
            return;
        }

        if (figure == null || moveLock || thread.isPaused()) {
            return;
        }

        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;
        }
    }

    private Figure randomFigure() {
        return figures[(int) (Math.random() * figures.length)];
    }


    private class GameThread extends Thread {

        private boolean paused = true;

        private int sleepTime = 500;

        public GameThread() {
        }

        public void reset() {
            adjustSpeed();
            setPaused(false);
            if (!isAlive()) {
                this.start();
            }
        }

        public boolean isPaused() {
            return paused;
        }

        public void setPaused(boolean paused) {
            this.paused = paused;
        }

        public void adjustSpeed() {
            sleepTime = 4500 / (level + 5) - 250;
            if (sleepTime < 50) {
                sleepTime = 50;
            }
        }

        public void run() {
            while (thread == this) {
                handleTimer();

                try {
                    Thread.sleep(sleepTime);
                } catch (InterruptedException ignore) {
                }

                while (paused && thread == this) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ignore) {
                    }
                }
            }
        }
    }
}

 

 

 

 

SquareBoard.java

import javax.swing.*;
import java.awt.*;
import java.util.Hashtable;

public class SquareBoard extends Object {

    private final int width;

    private final int height;

    private Color[][] matrix = null;

    private String message = null;

    private int removedLines = 0;

    private final SquareBoardComponent component;

    public SquareBoard(int width, int height) {
        this.width = width;
        this.height = height;
        this.matrix = new Color[height][width];
        this.component = new SquareBoardComponent();
        clear();
    }

    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;
        }
    }

    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;
    }

    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;
    }

    public boolean hasFullLines() {
        for (int y = height - 1; y >= 0; y--) {
            if (isLineFull(y)) {
                return true;
            }
        }
        return false;
    }

    public Component getComponent() {
        return component;
    }

    public int getBoardHeight() {
        return height;
    }

    public int getBoardWidth() {
        return width;
    }

    public int getRemovedLines() {
        return removedLines;
    }

    public Color getSquareColor(int x, int y) {
        if (x < 0 || x >= width || y < 0 || y >= height) {
            return null;
        } else {
            return matrix[y][x];
        }
    }

    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);
        }
    }

    public void setMessage(String message) {
        this.message = message;
        if (component != null) {
            component.redrawAll();
        }
    }

    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();
        }
    }

    public void removeFullLines() {
        boolean repaint = false;

        for (int y = height - 1; y >= 0; y--) {
            if (isLineFull(y)) {
                removeLine(y);
                removedLines++;
                repaint = true;
                y++;
            }
        }

        if (repaint && component != null) {
            component.redrawAll();
        }
    }

    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;
        }
    }

    public void update() {
        component.redraw();
    }

    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;

        private Insets insets = new Insets(0, 0, 0, 0);

        private Dimension squareSize = new Dimension(0, 0);

        private Image bufferImage = null;

        private Rectangle bufferRect = new Rectangle();

        private Color messageColor = Color.white;

        private Hashtable lighterColors = new Hashtable();

        private Hashtable darkerColors = new Hashtable();

        private boolean updated = true;

        private Rectangle updateRect = new Rectangle();

        public SquareBoardComponent() {
            setBackground(Configuration.getColor("board.background",
                    "#000000"));
            messageColor = Configuration.getColor("board.message",
                    "#ffffff");
        }

        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;
                }
            }
        }


        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);
            }
        }


        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);
        }

        public boolean isDoubleBuffered() {
            return true;
        }

        public Dimension getPreferredSize() {
            return new Dimension(width * 20, height * 20);
        }

        public Dimension getMinimumSize() {
            return getPreferredSize();
        }


        public Dimension getMaximumSize() {
            return getPreferredSize();
        }

        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;
        }

        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;
        }

        public synchronized void paint(Graphics g) {
            Graphics bufferGraphics;
            Rectangle rect;

            if (size == null || !size.equals(getSize())) {
                size = getSize();
                squareSize.width = size.width / width;
                squareSize.height = size.height / height;

                //if (squareSize.width <= squareSize.height) {
                //} else {
                //}

                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);
            }

            rect = g.getClipBounds();
            bufferGraphics = bufferImage.getGraphics();
            bufferGraphics.setClip(rect.x - insets.left,
                    rect.y - insets.top,
                    rect.width,
                    rect.height);
            doPaintComponent(bufferGraphics);

            g.drawImage(bufferImage,
                    insets.left,
                    insets.top,
                    getBackground(),
                    null);
        }

        private void doPaintComponent(Graphics g) {

            g.setColor(getBackground());
            g.fillRect(0,
                    0,
                    width * squareSize.width,
                    height * squareSize.height);

            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++) {
                    if (matrix[y][x] != null) {
                        paintSquare(g, x, y);
                    }
                }
            }

            if (message != null) {
                paintMessage(g, message);
            }
        }

        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;

            bufferRect.x = xMin;
            bufferRect.y = yMin;
            bufferRect.width = squareSize.width;
            bufferRect.height = squareSize.height;
            if (!bufferRect.intersects(g.getClipBounds())) {
                return;
            }

            g.setColor(color);
            g.fillRect(xMin, yMin, squareSize.width, squareSize.height);

            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);
            }

            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);
            }
        }

        private void paintMessage(Graphics g, String msg) {
            int fontWidth;
            int offset;
            int x;
            int y;

            g.setFont(new Font("SansSerif", Font.BOLD, squareSize.width + 4));
            fontWidth = g.getFontMetrics().stringWidth(msg);

            x = (width * squareSize.width - fontWidth) / 2;
            y = height * squareSize.height / 2;

            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);

            g.setColor(messageColor);
            g.drawString(msg, x, y);
        }
    }
}