본문 바로가기
Java

[Java] awt 테트리스 모듈화 - SquareBoard

by teamnova 2023. 8. 16.

저번 글 1: https://stickode.tistory.com/822
저번 글 2: https://stickode.tistory.com/835
저번 글 3: https://stickode.tistory.com/852

저번 글 4: https://stickode.tistory.com/866

저번 글 5: https://stickode.tistory.com/875

 

안녕하세요. 저번 시간에 이어서, 오늘은 SquareBoard 코드에 대한 설명을 업로드하겠습니다. 주석으로 설명을 달아두었습니다.

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;

    /**
     * 그래픽 스퀘어 보드 컴포넌트입니다. 이 그래픽 표현은
     * getComponent()가 처음 호출될 때 생성됩니다.
     */
    private final SquareBoardComponent component;

    /**
     * 지정된 크기로 새로운 스퀘어 보드를 생성합니다. 스퀘어 보드는
     * 초기에 비어있을 것입니다.
     *
     * @param width  보드의 너비 (스퀘어 수)
     * @param height 보드의 높이 (스퀘어 수)
     */
    public SquareBoard(int width, int height) {
        this.width = width;
        this.height = height;
        this.matrix = new Color[height][width];
        this.component = new SquareBoardComponent();
        clear();
    }

    /**
     * 지정된 스퀘어가 비어있는지 확인합니다. 즉, 해당 스퀘어가 색상으로
     * 표시되지 않은 경우입니다. 스퀘어가 보드의 바깥에 있는 경우,
     * 스퀘어가 보드의 바로 위에 있는 경우를 제외하고 모든 경우에
     * false가 반환됩니다.
     *
     * @param x 가로 위치 (0 <= x < width)
     * @param y 세로 위치 (0 <= y < height)
     * @return 스퀘어가 비어있는 경우 true, 그렇지 않으면 false
     */
    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;
        }
    }

    /**
     * 지정된 라인이 비어있는지 확인합니다. 즉, 비어있는 스퀘어만
     * 포함하는 경우입니다. 라인이 보드의 바깥에 있는 경우는 항상
     * false가 반환됩니다.
     *
     * @param y 세로 위치 (0 <= y < height)
     * @return 전체 라인이 비어있는 경우 true, 그렇지 않으면 false
     */
    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;
    }

    /**
     * 지정된 라인이 가득 찼는지 확인합니다. 즉, 빈 스퀘어가 없는 경우입니다.
     * 라인이 보드의 바깥에 있는 경우는 항상 true가 반환됩니다.
     *
     * @param y 세로 위치 (0 <= y < height)
     * @return 전체 라인이 가득 찬 경우 true, 그렇지 않으면 false
     */
    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;
    }

    /**
     * 보드에 가득 찬 라인이 있는지 확인합니다.
     *
     * @return 보드에 가득 찬 라인이 있는 경우 true, 그렇지 않으면 false
     */
    public boolean hasFullLines() {
        for (int y = height - 1; y >= 0; y--) {
            if (isLineFull(y)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 보드를 그리기 위한 그래픽 컴포넌트를 반환합니다. 이 메소드를
     * 여러 번 호출하면 동일한 컴포넌트가 반환됩니다. 스퀘어 보드는
     * 하나의 그래픽 표현만 가질 수 있기 때문입니다.
     *
     * @return 이 보드를 그리는 그래픽 컴포넌트
     */
    public Component getComponent() {
        return component;
    }

    /**
     * 보드의 높이(스퀘어 수)를 반환합니다. 이 메소드는 보드에
     * 맞는 수직 스퀘어의 개수를
     * <p>
     * 반환합니다.
     *
     * @return 보드의 높이 (스퀘어 수)
     */
    public int getBoardHeight() {
        return height;
    }

    /**
     * 보드의 너비(스퀘어 수)를 반환합니다. 이 메소드는 보드에
     * 맞는 수평 스퀘어의 개수를 반환합니다.
     *
     * @return 보드의 너비 (스퀘어 수)
     */
    public int getBoardWidth() {
        return width;
    }

    /**
     * 마지막 clear() 호출 이후에 제거된 라인 수를 반환합니다.
     *
     * @return 마지막 clear 호출 이후에 제거된 라인 수
     */
    public int getRemovedLines() {
        return removedLines;
    }

    /**
     * 보드의 개별 스퀘어의 색상을 반환합니다. 스퀘어가 비어있거나
     * 보드 밖에 있는 경우 null이 반환됩니다.
     *
     * @param x 가로 위치 (0 <= x < width)
     * @param y 세로 위치 (0 <= y < height)
     * @return 스퀘어 색상 또는 null (없음)
     */
    public Color getSquareColor(int x, int y) {
        if (x < 0 || x >= width || y < 0 || y >= height) {
            return null;
        } else {
            return matrix[y][x];
        }
    }

    /**
     * 보드의 개별 스퀘어의 색상을 변경합니다. 스퀘어는 다시 그려져야
     * 함을 표시하기 위해 invalidateSquare() 메소드가 호출되지만,
     * 그래픽 컴포넌트는 update() 메소드가 호출될 때까지 다시 그려지지
     * 않습니다.
     *
     * @param x     가로 위치 (0 <= x < width)
     * @param y     세로 위치 (0 <= y < height)
     * @param color 새로운 스퀘어 색상 또는 비어있음(null)
     */
    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);
        }
    }

    /**
     * 스퀘어 보드에 표시할 메시지를 설정합니다. 이는 보드가 활성화된 그림으로
     * 사용되지 않을 때 사용해야 하며, 그렇지 않으면 그리기 속도가 크게 느려집니다.
     *
     * @param message 표시할 메시지 또는 이전 메시지를 제거하려면 null
     */
    public void setMessage(String message) {
        this.message = message;
        if (component != null) {
            component.redrawAll();
        }
    }

    /**
     * 보드를 지웁니다. 즉, 모든 색상이
     * <p>
     * 지워집니다. 부작용으로
     * 제거된 라인 수가 0으로 재설정되고, 컴포넌트가 즉시 다시 그려집니다.
     */
    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();
        }
    }

    /**
     * 모든 가득 찬 라인을 제거합니다. 제거된 라인 위의 모든 라인이
     * 한 단계 아래로 이동되고, 맨 위에 새로운 빈 라인이 추가됩니다.
     * 모든 가득 찬 라인을 제거한 후에 컴포넌트가 다시 그려집니다.
     *
     * @see #hasFullLines
     */
    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();
        }
    }

    /**
     * 단일 라인을 제거합니다. 위의 모든 라인이 한 단계 아래로 이동되고,
     * 맨 위에 새로운 빈 라인이 추가됩니다. 라인을 제거한 후에는
     * 다시 그리지 않습니다.
     *
     * @param y 세로 위치 (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;
        }
    }

    /**
     * 그래픽 컴포넌트를 업데이트합니다. 이 메소드는 이전에 변경된
     * 스퀘어를 다시 그립니다.
     */
    public void update() {
        component.redraw();
    }

    /**
      * 정사각형 보드를 그리는 그래픽 컴포넌트입니다. 이는 내부 클래스로 구현되어 있어
      * 보드 사이의 자세한 정보를 더 잘 추상화하기 위해 구현되었습니다.
      */
    private class SquareBoardComponent extends JComponent {

        /**
         * 컴포넌트의 크기입니다. 컴포넌트가 크기가 조정된 경우에는
         * paint 메서드가 실행될 때 감지됩니다. 이 값이 null로 설정되면
         * 컴포넌트의 크기가 알려지지 않은 것입니다.
         */
        private Dimension size = null;

        /**
         * 컴포넌트의 여백입니다. 여백 값은 왜곡된 가로세로비를 보정하기 위해
         * 보드 주위에 테두리를 생성하는 데 사용됩니다. 컴포넌트의 크기가 조정된 경우,
         * 여백 값은 paint 메서드가 실행될 때 재계산됩니다.
         */
        private Insets insets = new Insets(0, 0, 0, 0);

        /**
         * 픽셀 단위의 정사각형 크기입니다. 이 값은
         * 컴포넌트의 크기가 변경될 때, 즉 <code>size</code>
         * 변수가 수정될 때 업데이트됩니다.
         */
        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");
        }

        /**
         * 다시 그려야 할 사각형 세트에 사각형을 추가합니다.
         *
         * @param x 가로 위치 (0 <= x < width)
         * @param y 세로 위치 (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;
                }
            }
        }

        /**
         * 모든 무효화된 사각형을 다시 그립니다. 다시 그려야 할 사각형이
         * 표시되지 않은 경우 다시 그리기가 발생하지 않습니다.
         */
        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);
        }

        /**
         * 이 컴포넌트가 더블

         버퍼링을 사용하기 때문에 true를 반환합니다.
         *
         * @return 더블 버퍼링을 사용하기 때문에 true를 반환합니다.
         */
        public boolean isDoubleBuffered() {
            return true;
        }

        /**
         * 이 컴포넌트의 선호 크기를 반환합니다.
         *
         * @return 선호하는 컴포넌트 크기
         */
        public Dimension getPreferredSize() {
            return new Dimension(width * 20, height * 20);
        }

        /**
         * 이 컴포넌트의 최소 크기를 반환합니다.
         *
         * @return 최소 컴포넌트 크기
         */
        public Dimension getMinimumSize() {
            return getPreferredSize();
        }

        /**
         * 이 컴포넌트의 최대 크기를 반환합니다.
         *
         * @return 최대 컴포넌트 크기
         */
        public Dimension getMaximumSize() {
            return getPreferredSize();
        }

        /**
         * 지정된 색상의 밝은 버전을 반환합니다. 밝은 색상은
         * 해시 테이블에서 찾아보고, 이 메서드는 빠르게 실행됩니다.
         * 색상이 발견되지 않으면 밝은 색상이 계산되어
         * 차후에 사용하기 위해 룩업 테이블에 추가됩니다.
         *
         * @param c 기본 색상
         * @return 색상의 밝은 버전
         */
        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;
        }

        /**
         * 지정된 색상의 어두운 버전을 반환합니다. 어두운 색상은
         * 해시 테이블에서 찾아보고, 이 메서드는 빠르게 실행됩니다.
         * 색상이 발견되지 않으면 어두운 색상이 계산되어
         * 차후에 사용하기 위해 룩업 테이블에 추가됩니다.
         *
         * @param c 기본 색상
         * @return 색상의 어두운 버전
         */
        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;
        }

        /**
         * 이 컴포넌트를 간접적으로 그립니다. 그림은 먼저
         * 버퍼 이미지에 그려지고, 그 이미지가 지정된 그래픽
         * 컨텍스트에 직접 그려집니다.
         *
         * @param g 사용할 그

        래픽 컨텍스트
         */
        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) {
                //    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);
            }

            // 버퍼 이미지에 컴포넌트 그리기
            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);
        }

        /**
         * 이 컴포넌트를 직접 그립니다. 보드의 모든 사각형이
         * 지정된 그래픽 컨텍스트에 직접 그려집니다.
         *
         * @param g 사용할 그래픽 컨텍스트
         */
        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);
            }
        }

        /**
         * 하나의 보드 사각형을 그립니다. 지정된 위치에는
         * 색상 객체가 있어야 합니다.
         *
         * @param g 그래픽 컨텍스트를 사용합니다.
         * @param x 가로 위치 (0 <= x < width)
         * @param y 세로 위치 (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;

            // 보이지 않는 경우 그리기 건너뛰기
            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);
            }
        }

        /**
         * 보드 메시지를 그립니다. 메시지는 컴포넌트의 중앙에 그려집니다.
         *
         * @param g   사용할 그래픽 컨텍스트
         * @param msg 문자열 메시지
         */
        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);
        }
    }
    }