본문 바로가기
안드로이드 자바

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

by teamnova 2023. 7. 17.

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

 

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

import java.awt.*;

/**
 * Tetris의 사각형 도형을 나타내는 클래스입니다. 각 도형은 일곱 가지 가능한 형태 중 하나인 네 개의 연결된 사각형으로 구성됩니다.
 * 도형은 90도 단위로 회전할 수 있으며 옆으로나 아래로 움직일 수 있습니다.
 * <p>
 * 각 도형 인스턴스는 두 가지 상태를 가질 수 있습니다. 사각형 보드에 연결된 경우와 그렇지 않은 경우입니다.
 * 연결된 경우, 모든 이동 및 회전 작업은 보드의 다른 사각형과 충돌하지 않도록 확인됩니다.
 * 연결되지 않은 경우 어떤 회전도 가능하며 (보드에 다시 연결될 때 유지됨).
 */
public class Figure extends Object {

    /**
     * 사각형 도형을 생성하는 데 사용되는 상수입니다.
     */
    public static final int SQUARE_FIGURE = 1;

    /**
     * 선 모양을 형성하는 도형을 생성하는 데 사용되는 상수입니다.
     */
    public static final int LINE_FIGURE = 2;

    /**
     * "S" 모양을 형성하는 도형을 생성하는 데 사용되는 상수입니다.
     */
    public static final int S_FIGURE = 3;

    /**
     * "Z" 모양을 형성하는 도형을 생성하는 데 사용되는 상수입니다.
     */
    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;

    /**
     * 도형이 연결된 사각형 보드입니다. 이 변수가 null로 설정되면 도형은 연결되지 않습니다.
     */
    private SquareBoard board = null;

    /**
     * 보드에서 도형의 수평 위치입니다. 이 값은 도형이 사각형 보드에 연결되지 않은 경우 의미가 없습니다.
     */
    private int xPos = 0;

    /**
     * 보드에서 도형의 수직 위치입니다. 이 값은 도형이 사각형 보드에 연결되지 않은 경우 의미가 없습니다.
     */
    private int yPos = 0;

    /**
     * 도형의 방향 (또는 회전)입니다. 이 값은 보통 0부터 3 사이지만 maxOrientation 값보다 작아야 합니다.
     *
     * @see #maxOrientation
     */
    private int orientation = 0;

    /**
     * 허용되는 최대 방향 번호입니다. 이 값은 사각형 도형과 같은 일부 도형에 대해 가능한 회전 수를 줄이는 데 사용됩니다.
     * 사용하지 않는 경우 사각형 도형은 하나의 사각형 주위로 회전할 수 있어 잘못된 효과를 줄 수 있습니다.
     *
     * @see #orientation
     */
    private int maxOrientation = 4;

    /**
     * 도형 모양의 수평 좌표입니다. 좌표는 현재 도형 위치와 방향에 상대적입니다.
     */
    private int[] shapeX = new int[4];

    /**
     * 도형 모양의 수직 좌표입니다. 좌표는 현재 도형 위치와 방향에 상대적입니다.
     */
    private int[] shapeY = new int[4];

    /**
     * 도형의 색상입니다.
     */
    private Color color = Color.white;

    /**
     * 사각형 보드에 연결되지 않은 상태에서 일곱 가지 사전 정의된 유형 중 하나의 도형을 생성합니다.
     * 도형에는 기본 색상과 방향이 할당됩니다.
     *
     * @param type 도형 유형 (도형 상수 중 하나)
     * @throws IllegalArgumentException 지정된 도형 유형이 인식되지 않는 경우
     * @see #SQUARE_FIGURE
     * @see #LINE_FIGURE
     * @see #S_FIGURE
     * @see #Z_FIGURE
     * @see #RIGHT_ANGLE_FIGURE
     * @see #LEFT_ANGLE_FIGURE
     * @see #TRIANGLE_FIGURE
     */
    public Figure(int type) throws IllegalArgumentException {
        initialize(type);
    }

    /**
     * 지정된 도형 유형에 대해 인스턴스 변수를 초기화합니다.
     *
     * @param type 도형 유형 (도형 상수 중 하나)
     * @throws IllegalArgumentException 지정된 도형 유형이 인식되지 않는 경우
     * @see #SQUARE_FIGURE
     * @see #LINE_FIGURE
     * @see #S_FIGURE
     * @see #Z_FIGURE
     * @see #RIGHT_ANGLE_FIGURE
     * @see #LEFT_ANGLE_FIGURE
     * @see #TRIANGLE_FIGURE
     */
    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);
        }
    }

    /**
     * 이 도형이 사각형 보드에 연결되었는지 확인합니다.
     *
     * @return 도형이 이미 연결되어 있는 경우 true, 그렇지 않은 경우 false
     */
    public boolean isAttached() {
        return board != null;
    }

    /**
     * 지정된 사각형 보드에 도형을 연결합니다. 도형은 보드의 상단에 절대적으로 위치하며 하단 줄만 표시되거나
     * 보드의 가운데에 중앙 정렬되어 그려집니다. 양쪽 경우 모두 새 보드의 사각형은 충돌을 확인하기 위해 검사됩니다.
     * 사각형이 이미 차있는 경우에는 false를 반환하고 연결이 이루어지지 않습니다.<p>
     * <p>
     * 도형이 새 보드에 중앙 정렬되는 경우, 도형의 수평 및 수직 좌표가 재설정됩니다. 그러나 도형의 방향(회전)은 유지됩니다.
     * 이전에 다른 보드에 연결되어있는 경우에는 해당 보드와의 연결이 끊기고 새 보드에 연결됩니다.
     *
     * @param board  연결할 사각형 보드
     * @param center 중앙 정렬 플래그
     * @return 도형이 연결되었으면 true, 그렇지 않으면 false
     */
    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;
    }

    /**
     * 도형이 사각형 보드에서 완전히 표시되는지 확인합니다. 도형이 보드에 연결되지 않은 경우 false를 반환합니다.
     *
     * @return 도형이 완전히 표시되면 true, 그렇지 않으면 false
     */
    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;
    }

    /**
     * 도형이 착륙한지 확인합니다. 이 메서드가 true를 반환하면 moveDown() 또는 moveAllWayDown() 메서드는
     * 효과가 없어야 합니다. 사각형 보드가 연결되지 않은 경우 이 메서드는 true를 반환합니다.
     *
     * @return 도형이 착륙한 경우 true, 그렇지 않으면 false
     */
    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();
        }
    }

    /**
     * 현재 도형의 회전(방향)을 반환합니다.
     *
     * @return 현재 도형의 회전
     */
    public int getRotation() {
        return orientation;
    }

    /**
     * 도형의 회전(방향)을 설정합니다. 사각형 보드에 대해 원하는 회전이 불가능한 경우 아무 작업도 수행하지 않습니다.
     * 도형이 이동하면 사각형 보드가 변경되어 이전 셀이 지워집니다. 사각형 보드가 연결되지 않은 경우 회전이 직접 수행됩니다.
     *
     * @param rotation 새로운 도형의 방향
     */
    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);
        }
    }

    /**
     * 지정된 (사각형) 좌표 쌍이 도형 내부에 있는지 여부를 확인합니다.
     *
     * @param x 수평 위치
     * @param y 수직 위치
     * @return 좌표가 도형 내부에 있으면 true, 그렇지 않으면 false
     */
    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;
    }

    /**
     * 도형이 새 위치로 이동할 수 있는지 확인합니다. 현재 도형 위치는 충돌을 확인할 때 고려됩니다.
     * 충돌이 감지되면 이 메서드는 false를 반환합니다.
     *
     * @param newX           새로운 수평 위치
     * @param newY           새로운 수직 위치
     * @param newOrientation 새로운 방향 (회전)
     * @return 도형이 이동 가능하면 true, 그렇지 않으면 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;
    }

    /**
     * 지정된 사각형의 상대적인 수평 위치를 반환합니다.
     * 지정된 방향에 따라 사각형이 회전됩니다.
     *
     * @param square      회전할 사각형 (0-3)
     * @param orientation 사용할 방향 (0-3)
     * @return 회전된 상대적인 수평 위치
     */
    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; // 발생하지 않아야 함
        }
    }

    /**
     * 지정된 사각형의 상대적인 수직 위치를 회전합니다.
     * 지정된 방향에 따라 사각형이 회전됩니다.
     *
     * @param square      회전할 사각형 (0-3)
     * @param orientation 사용할 방향 (0-3)
     * @return 회전된 상대적인 수직 위치
     */
    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; // 발생하지 않아야 함
        }
    }

    /**
     * 도형을 지정된 색으로 보드에 그립니다.
     *
     * @param color 그릴 색상, 지우기를 위해 null을 사용합니다.
     */
    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);
        }
    }
}