2021 마스터즈 코스 테스트 - 루빅스 큐브 구현하기 (3)

Jane·2020년 12월 16일
4
post-thumbnail

3단계: 루빅스 큐브

문제 설명

  • 큐브는 W, B, G, Y, O, R의 6가지 색깔을 가지고 있다.
  • 입력: 각 조작법을 한 줄로 입력받는다.
  • 출력: 큐브의 6면을 펼친 상태로 출력한다.
  • Q를 입력받으면 프로그램을 종료하고, 조작 받은 명령의 갯수를 출력시킨다.

큐브의 초기 상태

                B B B
                B B B
                B B B

 W W W     O O O     G G G     Y Y Y
 W W W     O O O     G G G     Y Y Y
 W W W     O O O     G G G     Y Y Y

                R R R
                R R R
                R R R

프로그램 예시

(초기 상태 출력)

CUBE> FRR'U2R

F
(큐브상태)

R
(큐브상태)

...

R
(큐브상태)

CUBE> Q
경과시간: 00:31 //추가 구현 항목
조작갯수: 6
이용해주셔서 감사합니다. 뚜뚜뚜.

추가 구현 기능

  • 프로그램 종료 시 경과 시간 출력
  • 큐브의 무작위 섞기 기능
  • 모든 면을 맞추면 축하 메시지와 함께 프로그램을 자동 종료

3단계 코딩 요구사항

  • 가능한 한 커밋을 자주 하고 구현의 의미가 명확하게 전달되도록 커밋 메시지를 작성할 것
  • 함수나 메소드는 한 번에 한 가지 일을 하고 가능하면 20줄이 넘지 않도록 구현한다.
  • 함수나 메소드의 들여쓰기를 가능하면 적게(3단계까지만) 할 수 있도록 노력해 본다.
function main() {
      for() { // 들여쓰기 1단계
          if() { // 들여쓰기 2단계
              return; // 들여쓰기 3단계
          }
      }
  }

👉 상세 문제 조건 및 구현 코드 확인


구조

프롬프트를 실행하는 Main class와 Prompt 관련 메서드를 저장하는 Prompt class, 큐브에 관련된 정보와 push 메서드들이 들어있는 Cube class를 구현하였다.

Main 클래스

  1. 큐브의 동작과 직접적으로 관련된 메서드들은 Prompt 클래스 안에 담아주었고, Main class는 Prompt 객체를 생성하고 runPrompt()를 실행하는 형태로 간단하게 구현하였다.
  2. runPrompt()실행 전에 startTime 변수에 시간을 저장하고, 실행을 마친 후에 endTime 변수에 시간을 저장함으로써 프로그램 종료 시 경과 시간 출력 기능을 구현했다.
  static long startTime, endTime, totalTime;

    public static void main(String[] args) {
        startTime = System.nanoTime();

        Prompt p = new Prompt();
        p.runPrompt();

        endTime   = System.nanoTime();
        totalTime = endTime - startTime;

        double seconds = (double) totalTime / 1_000_000_000.0;
        long t = Math.round(seconds);

        System.out.printf("경과시간: %02d:%02d\n", t / 60, t % 60);
        System.out.println("조작갯수: " + Cube.count);
        System.out.println("이용해주셔서 감사합니다. 뚜뚜뚜.");
    }

Prompt 클래스

메서드기능
getCommandKeyList(scanner)사용자로부터 입력받은 명령어를 저장하는 cmdQueue를 ArrayList의 형태로 반환
isInteger(cmd)문자열 cmd를 정수로 변환할 수 있을 경우 true, 변환이 불가능할 경우 false 반환
selectCommand(cube, cmdQueue)cmdQueue의 명령어를 차례로 실행
runPrompt()간단한 프롬프트 출력 및 사용자의 입력을 바탕으로 프로그램 실행

getCommandKeyList 메서드 : 사용자로부터 입력받은 명령어를 저장하는 cmdQueue를 ArrayList의 형태로 반환
isInteger 메서드 : 문자열 cmd를 정수로 변환할 수 있을 경우 true, 변환이 불가능할 경우 false 반환

  1. step-2의 getCommandKeyList 메서드와 거의 유사하지만, step-3에서는 F2와 같은 입력이 존재한다는 차이점이 있다.
  2. 먼저 문자열 cmd를 정수로 변환할 수 있을 경우 true, 변환이 불가능할 경우 false 반환하는 isInteger 메서드를 정의해주었다.
  3. isInteger 메서드 실행 결과 cmdKey가 정수라면 cmdKey를 정수로 변환하여 n에 저장한다.
  4. n회 만큼 숫자 바로 전에 나왔던 명령어 알파벳을 cmdQueue에 넣어주면 된다.
    ArrayList<String> getCommandKeyList(Scanner sc) {
        String cmd = sc.nextLine();
        ArrayList<String> cmdQueue = new ArrayList<>();
        int countSingleQuote = 0;
        for (int i = 0; i < cmd.length(); i++) {
            String cmdKey = Character.toString(cmd.charAt(i)).toUpperCase();

            if (cmdKey.equals("'")) {
                countSingleQuote++;
                cmdQueue.set(i - countSingleQuote, cmdQueue.get(i - countSingleQuote) + "'");
                continue;
            }

            int n = 0;
            if (isInteger(cmdKey)) n = Integer.parseInt(cmdKey);

            for (int j = 0; j < n - 1; j++) {
                cmdQueue.add(cmdQueue.get(i - 1 - countSingleQuote));
            }

            if (!isInteger(cmdKey)) cmdQueue.add(cmdKey);
        }
        return cmdQueue;
    }

selectCommand 메서드 : cmdQueue의 명령어를 차례로 실행

  1. step-2의 selectCommand 메서드와 거의 비슷하지만, step-3에서는 큐브를 무작위로 섞을 수 있는 "S"라는 명령어를 추가하였다.
  2. "S"가 나올 경우 scrambleCube()메서드를 실행하여 큐브를 섞고, "Q"가 나올 경우 프로그램을 종료한다.
  3. 이외에 회전과 관련된 명령어일 경우에는 rotateCube()와 printRubiksCube를 차례로 실행한다.
    void selectCommand(Cube cube, ArrayList<String> cmdQueue) {
        for (String x : cmdQueue) {
            if (x.equals("S")) {
                System.out.println("\nNewly Scrambled Rubik's Cube");
                cube.scrambleCube();
                cube.printRubiksCube();
                break;
            }
            if (x.equals("Q")) {
                isLoop = false;
                break;
            }
            System.out.println("\n" + x);
            cube.rotateCube(x);
            cube.printRubiksCube();
        }
    }

runPrompt 메서드 : 간단한 프롬프트 출력 및 사용자의 입력을 바탕으로 프로그램 실행

  1. step-2의 runPrompt 메서드와 유사하지만, 정답을 확인하고 축하메시지를 출력하는 부분을 추가하였다.
  2. checkAnswer 메서드 실행 결과가 true이면 축하메시지가 출력되고 프로그램은 자동으로 종료된다.
    void runPrompt() {
        Cube cube = new Cube();
        cube.printRubiksCube();

        Scanner sc = new Scanner(System.in);

        while (isLoop) {
            System.out.print("\n" + PROMPT);
            selectCommand(cube, getCommandKeyList(sc));
            if(cube.checkAnswer()) {
                System.out.println("\n🎉 정답입니다. 축하드립니다. 뚜뚜뚜.");
                isLoop = false;
            }
        }
        sc.close();
    }

Cube 클래스

  1. 루빅스 큐브의 6개 면을 저장할 2차원 배열을 인스턴스 변수로 선언해 주었다.
  2. COMMAND_KEYS는 scrambleCube 메서드를 위해, count는 총 조작갯수를 구하기 위해 선언해주었다.
    String[][] cubeLeft, cubeFront, cubeRight, cubeUp, cubeBack, cubeDown;
    private final String[] COMMAND_KEYS = { "L", "L'", "R", "R'", "U", "U'", "D", "D'", "F", "F'", "B", "B'" };
    static int count = 0;
  1. 생성자에서 2차원 배열 객체를 생성하고, initCube 메서드를 호출하여 초깃값을 부여하였다.
    public Cube() {
        cubeLeft = new String[3][3];
        cubeFront = new String[3][3];
        cubeRight = new String[3][3];
        cubeUp = new String[3][3];
        cubeBack = new String[3][3];
        cubeDown = new String[3][3];
        initCube();
    }
  1. 큐브와 관련된 다양한 메서드들을 정의하였다. 메서드의 크기를 줄이기 위해 정의한 하위 메서드 중 일부는 아래 목록에서 제외하였다.
메서드기능
initCube()큐브를 다 맞춰진 상태로 초기화
scrambleCube()큐브를 무작위로 섞기
checkAnswer()큐브가 완전히 맞춰졌다면 true, 아니라면 false를 반환
rotateCube(cmd)명령어 문자열에 따라 회전 메서드를 실행
rotateClockwise(cubeC)큐브의 한 면을 시계 방향으로 회전
rotateCounterclockwise(cubeC)큐브의 한 면을 시계 반대 방향으로 회전
rotateFront(cmd)큐브의 앞면을 90도 회전
rotateBack(cmd)큐브의 뒷면을 90도 회전
rotateLeft(cmd)큐브의 왼쪽 면을 90도 회전
rotateRight(cmd)큐브의 오른쪽 면을 90도 회전
rotateUp(cmd)큐브의 윗면을 90도 회전
rotateDown(cmd)큐브의 바닥을 90도 회전
printRubiksCube()큐브의 6면을 2차원으로 펼쳐진 상태로 출력

initCube 메서드 : 큐브를 다 맞춰진 상태로 초기화

2차원 배열로 선언된 큐브의 6개 면을 각각 다른 색상으로 초기화해 주었다.

    void initCube() {
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                cubeLeft[i][j] = "W";
                cubeFront[i][j] = "O";
                cubeRight[i][j] = "G";
                cubeBack[i][j] = "Y";
                cubeUp[i][j] = "B";
                cubeDown[i][j] = "R";
            }
        }
    }

scrambleCube 메서드 : 큐브를 무작위로 섞기

  1. 임의의 숫자 30개를 생성하여 COMMAND_KEYS 배열의 인덱스로 사용하였다.
  2. rotateCube메서드에 명령어를 넣어 임의의 명령이 30번 수행되어 큐브가 섞이도록 하였다.
    private final String[] COMMAND_KEYS = { "L", "L'", "R", "R'", "U", "U'", "D", "D'", "F", "F'", "B", "B'" };

    void scrambleCube() {

        for (int i = 0; i < 30; i++) {
            int randomNum = ThreadLocalRandom.current().nextInt(0, 12);
            rotateCube(COMMAND_KEYS[randomNum]);
        }
    }

checkAnswer : 큐브가 완전히 맞춰졌다면 true, 아니라면 false를 반환

  1. Cube의 6면을 순회하면서 초기값과 다른 값이 발견된다면 false를 반환한다.
  2. 모든 값이 큐브의 초기 상태와 동일하다면 true를 출력한다.
    boolean checkAnswer() {
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (cubeLeft[i][j].equals("W")==false) return false;
                if (cubeFront[i][j].equals("O")==false) return false;
                if (cubeRight[i][j].equals("G")==false) return false;
                if (cubeBack[i][j].equals("Y")==false) return false;
                if (cubeUp[i][j].equals("B")==false) return false;
                if (cubeDown[i][j].equals("R")==false) return false;
            }
        }
        return true;
    }

rotateCube 메서드 : 명령어 문자열에 따라 회전 메서드를 실행

step-2에서와 마찬가지로, rotateCube 메서드가 실행될 때마다 cmd 문자열을 키로 갖고있는 메서드가 실행된다.

    void rotateCube(String cmd) {
        Map<String, Runnable> commands = new HashMap<>();
        commands.put("L", () -> rotateLeft("L"));
        commands.put("L'", () -> rotateLeft("L'"));
        commands.put("R", () -> rotateRight("R"));
        commands.put("R'", () -> rotateRight("R'"));
        commands.put("U", () -> rotateUp("U"));
        commands.put("U'", () -> rotateUp("U'"));
        commands.put("D", () -> rotateDown("D"));
        commands.put("D'", () -> rotateDown("D'"));
        commands.put("F", () -> rotateFront("F"));
        commands.put("F'", () -> rotateFront("F'"));
        commands.put("B", () -> rotateBack("B"));
        commands.put("B'", () -> rotateBack("B'"));
        commands.get(cmd).run();
    }

rotateClockwise 메서드 : 큐브의 한 면을 시계 방향으로 회전
rotateCounterclockwise 메서드 : 큐브의 한 면을 시계 반대 방향으로 회전

  1. 루빅스 큐브의 경우 한 번의 회전 당 큐브 3개로 구성된 4개의 좁은 면과 큐브 9개로 구성된 한 개의 넓은 면이 돌아간다.
  2. 좁은 면은 Deque을 이용하여 회전시키고, 넓은 면은 rotateClockwise 및 rotateCounterclockwise 메서드를 사용하여 회전시켰다.
    String[][] rotateClockwise(String[][] cubeC) {
        String[][] updatedCube = new String[3][3];
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                updatedCube[j][2 - i] = cubeC[i][j];
            }
        }
        return updatedCube;
    }

rotateFront 메서드 : 큐브의 앞면을 90도 회전

  1. Deque 자료구조를 사용한 cmdFront 객체를 생성한다.
  2. cmdFront에 앞 면이 회전될 시 돌아가는 작은 면(큐브 3개로 구성)를 삽입한다.
  3. rotateInnerCube 메서드 호출을 통해 Deque의 원소들의 순서를 바꿔준다. 이 메서드 내부에서 rotateClockwise 메서드 또한 함께 실행된다.
    // rotateInnerCube 메서드 내부
    cmdC.addFirst(cmdC.removeLast());
    String[][] updatedCube = rotateClockwise(cubeC);
  4. 회전된 값을 큐브에 업데이트 해준다.
  5. rotateBack, rotateLeft, rotateRight, rotateUp, rotateDown 메서드도 동일한 로직으로 구현할 수 있다.
    void rotateFront(String cmd) {
        Deque<String> cmdFront = new ArrayDeque<>();
        cmdFront.add(cubeUp[2][0] + cubeUp[2][1] + cubeUp[2][2]);
        cmdFront.add(cubeRight[0][0] + cubeRight[1][0] + cubeRight[2][0]);
        cmdFront.add(cubeDown[0][2] + cubeDown[0][1] + cubeDown[0][0]);
        cmdFront.add(cubeLeft[2][2] + cubeLeft[1][2] + cubeLeft[0][2]);

        cubeFront = rotateInnerCube(cmd, cmdFront, cubeFront);
        String[] temp = saveCommandAsStringArray(cmdFront);

        for (int i = 0; i < 3; i++) {
            cubeUp[2][i] = Character.toString(temp[0].charAt(i));
            cubeRight[i][0] = Character.toString(temp[1].charAt(i));
            cubeDown[0][2 - i] = Character.toString(temp[2].charAt(i));
            cubeLeft[2 - i][2] = Character.toString(temp[3].charAt(i));
        }
        count++;
    }

printRubiksCube : 큐브의 6면을 2차원으로 펼쳐진 상태로 출력

  1. Cube 내에 인스턴스 변수로 선언된 6개의 면을 순회하며 원소를 출력한다.
  2. 들여쓰기를 줄이기 위해 printCubeSide라는 메서드를 사용하였다.
    void printRubiksCube() {

        printCubeUpORDown(cubeUp);
        System.out.println();
        for (int i = 0; i < 3; i++) {
            printCubeSide(i, cubeLeft);
            printCubeSide(i, cubeFront);
            printCubeSide(i, cubeRight);
            printCubeSide(i, cubeBack);
            System.out.println();
        }
        System.out.println();
        printCubeUpORDown(cubeDown);
    }

2개의 댓글

comment-user-thumbnail
2020년 12월 18일

코테는 이렇게 하는거구나 .. 저도 언젠가 공부해볼게요;

1개의 답글