부루마블 보드게임 과제

윤뿔소·2024년 6월 25일
0

Agorithm 문제풀이

목록 보기
3/7
post-thumbnail

부루마블 보드게임

참가자 A, B, C, D가 시작 칸부터 지정된 칸 수 만큼 총 16칸을 돌며 소유되지 않는 칸을 얻고, 입력값이 끝나거나 모든 칸이 채워진다면 끝나는 게임입니다.

즉, 게임 보드와 참가자의 이동 및 소유 규칙을 명확히 이해하고, 종료 조건까지 확실히 끝내야 하는 알고리즘 문제입니다.

의사코드

먼저 의사코드를 작성해보겠습니다. 어제와 달리 간단명료하면서 그렇게 생각했던 이유를 들어보겠습니다.

0. 입력값 / 출력값 정의

  • 입력값
    함수 play()에 들어오는 파라미터는 숫자가 4개 씩 들어오는 배열입니다. 예시에도 "1,2,3,4" 이렇게 쓴 걸 보니, 입력값 정의를 ["1,2,3,4", "1,2,3,4"] 이런 식으로 정해보겠습니다.
  • 출력값
    조건에 Map 혹은 Dict(JS-객체) 자료구조를 사용하면서, A ~ D 참가자의 소유 장소 개수를 정의해 log로 출력합니다. 직접 의사코드를 써보면서 어떤 자료구조를 쓸 지 후술하겠습니다.

테스트 케이스 작성

간단한 것만 직접 눈으로 작성해보고, 예상 출력값을 그려봤습니다.

같은 수가 연속, 임의의 난수, 2개 만 숫자가 같은 것이 연속일 때 등등을 다양한 상황을 걸러내기 위해 입력했습니다.

console.log('1.', play(['1,2,3,4', '1,1,1,1', '2,2,2,2', '3,3,3,3']));
// 예상 출력: {A: 1, B: 2, C: 3, D: 4}
console.log('2.', play(['1,1,1,1', '1,1,1,1', '1,1,1,1', '1,1,1,1']));
// 예상 출력: {A: 1, B: 2, C: 3, D: 4}
console.log('3.', play(['4,2,1,3', '2,1,2,2', '3,2,1,3', '2,1,3,2']));
console.log('4.', play(['3,3,3,3', '2,2,2,2', '1,1,1,1']));
console.log('5.', play(['1,3,2,4', '2,1,4,3']));
// 예상 출력: { A: 1, B: 1, C: 2, D: 2 }
console.log('6.', play(['1,2,1,2', '2,1,2,1', '1,2,1,2', '2,1,2,1']));
console.log('7.', play(['1,2,3,4']));
// 예상 출력: { A: 1, B: 1, C: 1, D: 1 }
console.log('8.', play(['1,4,2,1', '1,1,3,4', '2,1,1,4', '2,4,2,4']));
console.log('9.', play(['4,3,2,1', '1,2,3,4', '4,3,2,1', '1,2,3,4']));
console.log(
  '10.',
  play(['1,1,1,1', '2,2,2,2', '3,3,3,3', '4,4,4,4', '5,5,5,5', '6,6,6,6']),
);

1. 필요한 변수 선언하기

  1. board
    • 시작 칸과 순서대로 진행되는 15개의 칸으로 구성됩니다.
    • 이 구조는 선형적으로 이뤄져 있고, 각 칸을 쉽게 접근하고 갱신할 수 있어야 하므로 배열(Array)에 적합하다고 생각해 배열로 선언했습니다.
    • 길이가 16개인 배열을 선언하고, 초기 상태를 명확하게 비워놓기 위해 null로 초기화 했습니다.
      • 시작 칸은 소유할 수 있는지 없는지 나와있지 않아 다른 칸과 조건을 동일하게 했습니다.
    • 이후 참가자가 이동하면서 시작 칸을 포함해 소유주를 할당할 예정입니다.
  2. partPosition
    • board의 선형적 구조를 탐색하고, 각각의 참가자가 서있는 위치가 필요하다고 생각해 정의했습니다.
    • 입력값에 의해 각 참가자의 위치가 쉽게 변경돼야하므로 객체가 적합하다 생각해 객체로 선언했습니다.
    • Key는 String - 참가자명, Value는 board의 인덱스 - 위치인 Number로 초기화 했습니다.
  3. partOwned
    • boardpartPosition가 진행되면서 참가자가 장소를 소유하게 됐다면, 개수를 추가할 변수도 필요하다 생각해 정의했습니다.
    • 참가자 A ~ D의 장소 개수를 정의하고 쉽게 변경해야하기 때문에 partPosition와 마찬가지로 객체로 선언했습니다.
    • Key는 참가자명으로 초기화하고, Value는 개수를 나타내는 Number로 초기화 했습니다.
    • 추후 출력값에 사용할 예정입니다.

2. 입력값 반복문 돌기

  1. 반복문 돌기
    • 입력값이 배열이고, 4개의 숫자와 ,을 가지는 문자열로 이뤄져 있습니다.
    • 따라서 각각의 입력값을 사용하기 위해 반복문을 사용해야겠다 생각했습니다.
    • 반복문 안에서 증가하는 i로 입력값을 추출합니다.
  2. 입력값 문자열 가공하기
    • 입력값의 각 요소가 "1,2,3,4" 이런 식으로 이뤄져 있기 때문에 split(',')을 이용해 각 문자열인 이동 숫자를 추출합니다.
    • 각 숫자는 아직 문자열이고, 형태가 Number인 위 변수의 인덱스나 위치를 수정해야한다고 생각했기 때문에 Number로 가공합니다.
  3. 이동 및 결과 반영하기
    • A 참가자부터 로직을 실행합니다.
    • 이동을 해야하기에 위치 지정을 먼저 합니다.
      • 위치 지정 시 보드 크기인 16이 넘어간다면 나눠주고, 그 나머지를 사용해 위치 지정합니다.
    • 이동 후 board의 해당 인덱스 - 요소가 null이면 소유 로직을 실행합니다. null이 아니라면 넘어갑니다.
      • 그 참가자의 위치를 인덱스로 삼아 board에 참가자를 할당합니다.
      • 소유 개수가 늘어났으니 partOwned의 해당 참가자의 Value에 1을 추가합니다.
    • 위치 지정 및 결과가 끝나면 B 참가자로 넘어갑니다.
  4. 리턴하기
    • partOwned를 리턴합니다.

실제 코드 작성 및 구현

의사코드로 썼던 부분을 참고해 실제로 코드를 작성하겠습니다.

1. 필요한 변수 선언

function play(param0) {
  // 0. 변수명을 잘 알아볼 수 있게 파라미터 재 할당
  const moves = param0;

  // 1-1. board: 시작 칸 포함 16칸짜리 보드 초기화
  const board = Array(16).fill(null);
  // 1-2. partPosition: 각 참가자의 초기 위치
  const partPosition = { A: 0, B: 0, C: 0, D: 0 };
  // 1-3. partOwned: 각 참가자의 소유한 칸 수
  const partOwned = { A: 0, B: 0, C: 0, D: 0 };
...

우선 의사코드 1번을 작성했습니다. 변수명과 배열, 객체 등 해당 자료구조, 초기화까지 작성했습니다.

참고로 참가자가 4명이 있어서 긴 느낌이지만 을 좋게 만드려고 생각해 moves로 재할당 해줬습니다.

2. 반복문 돌기

// 2. 반복문 돌기
for (let i = 0; i < moves.length; i++) {
  // 2-1. moves 추출하기
  const move = moves[i];
  // 2-2. 입력값 문자열 가공
  const moveSplit = move.split(',').map(Number);

  /// A 참가자
  // 위치 지정
  partPosition.A = (partPosition.A + moveSplit[0]) % board.length;
  // board 소유주 확인
  if (board[partPosition.A] === null) {
    // 소유 없음 확인 후 할당
    board[partPosition.A] = 'A';
    partOwned.A += 1;
  }

  /// B 참가자
  // 위치 지정
  partPosition.B = (partPosition.B + moveSplit[1]) % board.length;
  // board 소유주 확인
  if (board[partPosition.B] === null) {
    // 소유 없음 확인 후 할당
    board[partPosition.B] = 'B';
    partOwned.B += 1;
  }

  /// C 참가자
  // 위치 지정
  partPosition.C = (partPosition.C + moveSplit[2]) % board.length;
  // board 소유주 확인
  if (board[partPosition.C] === null) {
    // 소유 없음 확인 후 할당
    board[partPosition.C] = 'C';
    partOwned.C += 1;
  }

  /// D 참가자
  // 위치 지정
  partPosition.D = (partPosition.D + moveSplit[3]) % board.length;
  // board 소유주 확인
  if (board[partPosition.D] === null) {
    // 소유 없음 확인 후 할당
    board[partPosition.D] = 'D';
    partOwned.D += 1;
  }
  console.log(
    `현재 보드 상태 ${i + 1}번째: ${board.map((v) => {
      if (!v) return '_';
      return v;
    })}`,
  );
}
  • 반복문 작성i를 선언해 파라미터(moves)를 분리했습니다.
  • movesi로 쪼개고, splitNumber로 쓰기 편하게 재가공했습니다.
  • A 참가자부터 위치 지정 - board 소유주 확인 - 확인 후 할당 순으로 구현 했습니다. 로직이 끝나면 B, C, D로 넘어가게 순서대로 작성했습니다.
  • 과정을 더 면밀히 알고 싶어서 현재 board의 상태를 확인하기 위해 console.log도 작성해봤습니다.

3. 리턴하기

  return partOwned;
}

출력값이 원하는 partOwned를 리턴합니다.

결과

테스트 케이스를 실행시켜보겠습니다.
image
아주 잘나옵니다. 실제로는 틀린 답이 나올 수도 있으니 이게 실제로 맞는지 디버깅 및 board 진행 상태를 log에 찍어보고 눈으로 검사 해봤을 때, 옳은 답인지 확인하겠습니다.

# node ./boost-marbel.js
현재 보드 상태 1번째: _,A,B,C,D,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,B,C,D,D,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,A,B,C,D,D,C,D,_,_,_,_,_,_,_,_
현재 보드 상태 4번째: _,A,B,C,D,D,C,D,B,C,D,_,_,_,_,_
1. {A: 1, B: 2, C: 3, D: 4}
현재 보드 상태 1번째: _,A,_,_,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,A,_,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,A,A,A,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 4번째: _,A,A,A,A,_,_,_,_,_,_,_,_,_,_,_
2. {A: 4, B: 0, C: 0, D: 0}
현재 보드 상태 1번째: _,C,B,D,A,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,C,B,D,A,D,A,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,C,B,D,A,D,A,_,D,A,_,_,_,_,_,_
현재 보드 상태 4번째: _,C,B,D,A,D,A,C,D,A,D,A,_,_,_,_
3. {A: 4, B: 1, C: 2, D: 4}
현재 보드 상태 1번째: _,_,_,A,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,_,_,A,_,A,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,_,_,A,_,A,A,_,_,_,_,_,_,_,_,_
4. {A: 3, B: 0, C: 0, D: 0}
현재 보드 상태 1번째: _,A,C,B,D,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,C,B,D,_,C,D,_,_,_,_,_,_,_,_
5. {A: 1, B: 1, C: 2, D: 2}
현재 보드 상태 1번째: _,A,B,_,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,B,A,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,A,B,A,A,B,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 4번째: _,A,B,A,A,B,A,_,_,_,_,_,_,_,_,_
6. {A: 4, B: 2, C: 0, D: 0}
현재 보드 상태 1번째: _,A,B,C,D,_,_,_,_,_,_,_,_,_,_,_
7. {A: 1, B: 1, C: 1, D: 1}
현재 보드 상태 1번째: _,A,C,_,B,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,C,_,B,B,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,A,C,_,B,B,B,_,_,D,_,_,_,_,_,_
현재 보드 상태 4번째: _,A,C,_,B,B,B,_,C,D,B,_,_,D,_,_
8. {A: 1, B: 4, C: 2, D: 2}
현재 보드 상태 1번째: _,D,C,B,A,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,D,C,B,A,A,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,D,C,B,A,A,D,C,B,A,_,_,_,_,_,_
현재 보드 상태 4번째: _,D,C,B,A,A,D,C,B,A,A,_,_,_,_,_
9. {A: 4, B: 2, C: 2, D: 2}
현재 보드 상태 1번째: _,A,_,_,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,_,A,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,A,_,A,_,_,A,_,_,_,_,_,_,_,_,_
현재 보드 상태 4번째: _,A,_,A,_,_,A,_,_,_,A,_,_,_,_,_
현재 보드 상태 5번째: _,A,_,A,_,_,A,_,_,_,A,_,_,_,_,A
현재 보드 상태 6번째: _,A,_,A,_,A,A,_,_,_,A,_,_,_,_,A
10. {A: 6, B: 0, C: 0, D: 0}

각각 보드 상태를 봤을 때, 과정도 틀린 게 없고 예상 출력값도 똑같이 나오고 있습니다.
즉, 문제가 없다고 판단했습니다.

디버깅

제 판단은 항상 틀릴 수도 있다는 것을 염두에 뒀지만 오늘 다시 증명이 되니 마음이 아픕니다. 테스트 케이스에 부족한 유형을 더 채우는 과정에서 문제가 발생했습니다.
image
바로 board가 다 채워질 때의 종료 조건이 없어 같은 결과가 여러번 반복 돼 요구사항을 충족시키지 못했습니다. 곧바로 디버깅에 들어가겠습니다.

의사 코드

먼저 간단히 의사 코드를 생각했습니다.

  • board에 소유할 장소가 없어지면, 즉, 소유할 장소가 없어지는 순간반복 종료 및 리턴
  • 그 순간을 캐치해야하기 때문에 각 A ~ D 참가자가 끝날 때마다 종료 로직 추가

이후 이러한 과정을 코드에 담으려고 생각했습니다.
처음에는 배열 메소드인 every를 사용해 할까 싶었지만, 순서가 끝날 때마다 한 배열을 순회하기 때문에 시간 복잡도가 엄청 증가한다고 생각해 다른 방법을 모색했습니다.
차라리 현재 전체 소유 장소 개수를 알 수 있는 변수를 추가로 선언 및 초기화 후, board의 길이랑 같다면 종료 로직이 실행되게끔 조건문을 각각 추가해야겠다 생각했습니다.

구현 코드

// 전체 장소 소유 개수
let ownedCount = 0;

...
console.log(`반복 ${i + 1}번째`);
/// A 참가자
// 위치 지정
partPosition.A = (partPosition.A + moveSplit[0]) % board.length;
// board 소유주 확인
if (board[partPosition.A] === null) {
  // 소유 없음 확인 후 할당
  board[partPosition.A] = 'A';
  partOwned.A += 1;
  ownedCount += 1;
  console.log(`현재 보드 상태 ${i + 1}번째: ${board}`);
  if (ownedCount === board.length) {
    break;
  }
}

이런 식으로 break 문을 넣어서 전체 장소 소유 개수, ownedCountboard.length랑 같다면 반복을 종료하고, 그 결과값을 반환하도록 했습니다.
또다른 부작용이 없는지 반복 시작할 때, log를 찍어 반복이 얼마나 됐는지 기록하게 했습니다.
이번엔 board를 더 면밀히 보기 위해 각 참가자의 소유가 진행될 때 boardlog를 찍어 또다른 버그는 없는지 확인했습니다.

image

사진을 보시면 쭉 반복 과정이 진행되다가 9번째에 소유된 장소가 15개가 되고, 소유되기 전까지 반복이 되다가 12번째에 16개가 되어 반복이 끝난 것을 볼 수 있습니다.
즉, param0의 길이가 15임에도 불구하고, 12번째에 반복이 끝났음을 확인 할 수 있었습니다.

결과

제출 시 과정을 보기 위한 console.log는 다 주석 처리 했습니다.

function play(param0) {
  // 0. 변수명을 잘 알아볼 수 있게 파라미터 재 할당
  const moves = param0;

  // 1-1. board: 시작 칸 포함 16칸짜리 보드 초기화
  const board = Array(16).fill(null);
  // 1-2. partPosition: 각 참가자의 초기 위치
  const partPosition = { A: 0, B: 0, C: 0, D: 0 };
  // 1-3. partOwned: 각 참가자의 소유한 칸 수
  const partOwned = { A: 0, B: 0, C: 0, D: 0 };
  // 디버깅: 전체 장소 소유 개수
  let ownedCount = 0;

  // 2. 반복문 돌기
  for (let i = 0; i < moves.length; i++) {
    // console.log(`반복 ${i + 1}번째`);

    // 2-1. moves 추출하기
    const move = moves[i];
    // 2-2. 입력값 문자열 가공
    const moveSplit = move.split(',').map(Number);

    /// A 참가자
    // 위치 지정
    partPosition.A = (partPosition.A + moveSplit[0]) % board.length;
    // board 소유주 확인
    if (board[partPosition.A] === null) {
      // 소유 없음 확인 후 할당
      board[partPosition.A] = 'A';
      partOwned.A += 1;
      // 디버깅: 전체 소유 장소 + 1
      ownedCount += 1;
      // console.log(`현재 보드 상태 ${i + 1}번째: ${board}`);
      // 디버깅: 소유할 장소가 없다면 break
      if (ownedCount === board.length) {
        break;
      }
    }

    /// B 참가자
    // 위치 지정
    partPosition.B = (partPosition.B + moveSplit[1]) % board.length;
    // board 소유주 확인
    if (board[partPosition.B] === null) {
      // 소유 없음 확인 후 할당
      board[partPosition.B] = 'B';
      partOwned.B += 1;
      // 디버깅: 전체 소유 장소 + 1
      ownedCount += 1;
      // console.log(`현재 보드 상태 ${i + 1}번째: ${board}`);
      // 디버깅: 소유할 장소가 없다면 break
      if (ownedCount === board.length) {
        break;
      }
    }

    /// C 참가자
    // 위치 지정
    partPosition.C = (partPosition.C + moveSplit[2]) % board.length;
    // board 소유주 확인
    if (board[partPosition.C] === null) {
      // 소유 없음 확인 후 할당
      board[partPosition.C] = 'C';
      partOwned.C += 1;
      // 디버깅: 전체 소유 장소 + 1
      ownedCount += 1;
      // console.log(`현재 보드 상태 ${i + 1}번째: ${board}`);
      // 디버깅: 소유할 장소가 없다면 break
      if (ownedCount === board.length) {
        break;
      }
    }

    /// D 참가자
    // 위치 지정
    partPosition.D = (partPosition.D + moveSplit[3]) % board.length;
    // board 소유주 확인
    if (board[partPosition.D] === null) {
      // 소유 없음 확인 후 할당
      board[partPosition.D] = 'D';
      partOwned.D += 1;
      // 디버깅: 전체 소유 장소 + 1
      ownedCount += 1;
      // console.log(`현재 보드 상태 ${i + 1}번째: ${board}`);
      // 디버깅: 소유할 장소가 없다면 break
      if (ownedCount === board.length) {
        break;
      }
    }

    // 보드 상태를 보기 위한 log
    // console.log(
    //   `현재 보드 상태 ${i + 1}번째: ${board.map((v) => {
    //     if (!v) return '_';
    //     return v;
    //   })}`,
    // );
  }

  return partOwned;
}

// 테스트 케이스 실행: 각 턴마다 4개의 이동 값 (A, B, C, D 순서)
console.log('1.', play(['1,2,3,4', '1,1,1,1', '2,2,2,2', '3,3,3,3']));
// 1. 예상 출력: {A: 1, B: 2, C: 3, D: 4}
console.log('2.', play(['1,1,1,1', '1,1,1,1', '1,1,1,1', '1,1,1,1']));
// 2. 예상 출력: {A: 4, B: 0, C: 0, D: 0}
console.log('3.', play(['4,2,1,3', '2,1,2,2', '3,2,1,3', '2,1,3,2']));
// 3. 예상 출력: {A: 4, B: 1, C: 2, D: 4}
console.log('4.', play(['3,3,3,3', '2,2,2,2', '1,1,1,1']));
// 4. 예상 출력: {A: 3, B: 0, C: 0, D: 0}
console.log('5.', play(['1,3,2,4', '2,1,4,3']));
// 5. 예상 출력: {A: 1, B: 1, C: 2, D: 2}
console.log('6.', play(['1,2,1,2', '2,1,2,1', '1,2,1,2', '2,1,2,1']));
// 6. 예상 출력: {A: 4, B: 2, C: 0, D: 0}
console.log('7.', play(['1,2,3,4']));
// 7. 예상 출력: {A: 1, B: 1, C: 1, D: 1}
console.log('8.', play(['1,4,2,1', '1,1,3,4', '2,1,1,4', '2,4,2,4']));
// 8. 예상 출력: {A: 4, B: 4, C: 4, D: 3}
console.log('9.', play(['4,3,2,1', '1,2,3,4', '4,3,2,1', '1,2,3,4']));
// 9. 예상 출력: {A: 4, B: 2, C: 2, D: 2}
console.log(
  '10.',
  play(['1,1,1,1', '2,2,2,2', '3,3,3,3', '4,4,4,4', '5,5,5,5', '6,6,6,6']),
);
// 10. 예상 출력: {A: 6, B: 0, C: 0, D: 0}
console.log(
  '11.',
  play([
    '4,3,2,1',
    '2,3,4,1',
    '3,4,1,2',
    '4,1,2,3',
    '1,3,2,4',
    '2,4,3,1',
    '3,1,4,2',
    '4,2,1,3',
    '1,4,3,2',
    '2,1,4,3',
    '3,2,1,4',
    '4,3,2,1',
    '1,2,4,3',
    '2,3,1,4',
    '3,4,2,1',
    '4,1,3,2',
  ]),
);
// 11. 예상 출력: {A: 7, B: 5, C: 2, D: 2}

리팩토링

리팩토링도 진행해보겠습니다. 제가 먼저 거슬렸던 부분은 입력값 문자열 가공 부분과 중복 코드가 너무 많다는 부분입니다.

입력값 문자열 가공

입력값 문자열 가공 후 moveSplit[0] 이런 식으로 써서 위치 지정 계산을 썼었는데 구현 중간에 '이게 뭐였지?' 하는 생각이 들었어서 리팩토링을 진행해보려고 합니다.

...
// 2-2. 입력값 문자열 가공
const moveSplit = move.split(',').map(Number);

/// A 참가자
// 위치 지정
partPosition.A = (partPosition.A + moveSplit[0]) % board.length;
...

변수에 의미를 넣을 수 있는 직관적 방법이 const moveA = moveSplit[0] 이런 식으로 해서 의미를 부여하려고 했습니다. 하지만 코드가 더 난잡해지고, 의미 부여로 4줄이 더 생기는 거는 원치 않았습니다.
더 분해해 재사용에 용이할 수 있는 또다른 방법이 있을까 하고 생각해보니 moveSplit은 배열이라는 생각이 들었고, 구조 분해 할당 생각이 났습니다. 바로 착수했습니다.

...
// 2-2. 입력값 문자열 가공
const [moveA, moveB, moveC, moveD] = move.split(',').map(Number);

/// A 참가자
// 위치 지정
partPosition.A = (partPosition.A + moveA) % board.length;
...

전과 달리 1줄로 변수 선언이 되고, 또한 A가 움직이는 거리라고 생각돼 더욱 직관적으로 변했습니다.
나중에 알고리즘에서 1분 1초가 아까우니 구조 분해 할당을 더 활용해야겠다 생각했습니다.

중복 코드 제거

참가자 관련 중복 코드가 4개나 있습니다. 중복 코드는 시간과 다투는 알고리즘 테스트에서 우선순위는 떨어지지만 참을 수 없어서 리팩토링을 진행해보겠습니다.

코드를 보면 참가자 A ~ D의 로직이 참가자 이름만 다를 뿐이지 나머지는 다 같은 로직임을 발견했습니다. 이를 줄여줄 사용자 정의 함수를 추가해보겠습니다.

// 리팩토링: 참가자 이동 및 소유 체크 함수 선언
const moveCheckOwn = (participant, move) => {
  partPosition[participant] = (partPosition[participant] + move) % board.length;

  if (board[partPosition[participant]] === null) {
    board[partPosition[participant]] = participant;
    partOwned[participant] += 1;
    ownedCount += 1;

    // 리팩토링: 모든 칸이 채워졌음을 나타냄
    if (ownedCount === board.length) {
      return true;
    }
  }
  // 리팩토링: 아직 모든 칸이 채워지지 않음
  return false;
};
  1. 'A', 'B'moveB처럼 참가자마다 변경되는 부분을 파라미터화 했습니다.
  2. 직접 하드코딩했던 부분들을 전부 파라미터로 받게 수정했습니다. ex) partPosition.C => partPosition[participant]
  3. break문은 반복문 안에서만 사용 가능하기에 빼고 Boolean 데이터를 반환 시켜 호출부에서 break가 일어나도록 조정했습니다.
    image

결과 코드

function play(param0) {
  const moves = param0;
  const board = Array(16).fill(null);
  const partPosition = { A: 0, B: 0, C: 0, D: 0 };
  const partOwned = { A: 0, B: 0, C: 0, D: 0 };
  let ownedCount = 0;

  // 리팩토링: 참가자 이동 및 소유 체크 함수 선언
  const moveCheckOwn = (participant, move) => {
    partPosition[participant] =
      (partPosition[participant] + move) % board.length;

    if (board[partPosition[participant]] === null) {
      board[partPosition[participant]] = participant;
      partOwned[participant] += 1;
      ownedCount += 1;

      // 리팩토링: 모든 칸이 채워졌음을 나타냄
      if (ownedCount === board.length) {
        return true;
      }
    }
    // 리팩토링: 아직 모든 칸이 채워지지 않음
    return false;
  };

  // 2. 반복문 돌기
  for (let i = 0; i < moves.length; i++) {
    const move = moves[i];
    const [moveA, moveB, moveC, moveD] = move.split(',').map(Number);

    // 리팩토링: 새로 만든 함수로 수정 및 반환값으로 break
    // A 참가자
    if (moveCheckOwn('A', moveA)) break;
    // B 참가자
    if (moveCheckOwn('B', moveB)) break;
    // C 참가자
    if (moveCheckOwn('C', moveC)) break;
    // D 참가자
    if (moveCheckOwn('D', moveD)) break;
  }

  return partOwned;
}

if문의 조건이어도 함수는 실행이 되는 걸 이용해 바로 조건문의 조건으로 넣어주고, break 문도 실행되게 했습니다.
기능적으로는 달라진 부분은 없지만 훨씬 깔끔해진 모습입니다.

결론

테스트 케이스는 항상 모자란 거 같습니다. 당연한 말이지만 사이트 이펙트나 버그는 제가 모르는 사이에 들어와 자꾸 괴롭힙니다 ㅠ
더더욱 테스트 케이스를 견고하면서 다양한 케이스를 시도할 수 있도록 노력해야겠습니다.

  1. 경계값 입력 : 최소/최대 길이 입력
  2. 같은 조건 및 다른 세부 정보, 수열, 난수 반복
  3. 결과를 예측한 테스트 케이스

등등 여러가지 할 게 많다고 더욱 느낀 오늘이었습니다.

profile
코뿔소처럼 저돌적으로

0개의 댓글