사다리 게임 만들기

윤뿔소·2024년 6월 27일
0

Agorithm 문제풀이

목록 보기
4/7
post-thumbnail

사다리 게임 만들기

3가지 발판 유형을 가지는 사다리 게임을 구현하는 알고리즘입니다. 사다리를 리턴할 수 있는 각각의 함수 4개를 구현해 가져옵니다.
main.js을 만들어 한꺼번에 작성합니다.

요구사항 분석

  1. 참가자는 5명으로, 사다리 높이는 5칸으로 고정합니다.
  2. 기능 요구사항
    • reset(), randomFill(), analyze(), display() 함수를 구현합니다.
    • 각각 사다리 초기화, 발판 랜덤 채우기, 사다리 유효성 분석, 문자열 가공 및 출력 기능을 구현합니다.

입출력값 분석

  1. 입력값 : 사용자의 입력값은 함수 자체를 실행시킵니다.
  2. 출력값 : display() 함수로 출력합니다. 5줄을 가진 문자열을 출력합니다.

의사코드 작성

  1. 사다리 구조 관련 변수 초기화 : 먼저 고정된 데이터들을 상수로 정의하고, 사다리 정보를 담은 변수를 정의했습니다.

    • 상수: Row를 나타내는 PLAYERS, Column을 나타내는 HEIGHT, 발판 정보를 담은 STEP_TYPES(수평, 우하향 등)
    • row, col와 발판을 나타내는 step을 객체로 저장하는 배열 ladderSteps
  2. 함수 구현 : 이번엔 실제 코드 구현한다 생각하고, 비슷하게 써봤습니다.

    • reset(): 사다리의 행과 열을 초기화합니다.

    • randomFill(): 랜덤하게 3가지 발판 종류를 선택해 사다리 데이터 구조를 채웁니다. 총 발판의 개수도 랜덤하게 결정합니다.

    함수 randomFill():
      난수 + 인덱스로 발판을 뽑아내기 위해 배열에 3가지 종류의 발판 (HORIZON, RIGHT_DOWN, LEFT_DOWN)을 정의합니다.
      각 행을 반복:
        각 열을 반복:
          발판 삽입 조건을 위해 난수를 생성합니다.
          발판 삽입 난수가 0.x 이상이면:
            발판 종류 선택을 위한 난수를 생성합니다.
            발판 종류를 정의한 배열에 발판 종류 선택 난수로 랜덤한 발판 유형을 선택합니다.
            현재 행, 열 위치와 선택된 발판의 유형을 ladderSteps 배열에 추가합니다.
    • analyze(): 사다리 데이터 구조를 분석해 결과를 리턴합니다. 수평 발판 연속 또는 상반된 발판 조합이 연속된 경우 false, 그렇지 않으면 true를 반환합니다. 좌우만 비교하면 되기에 배열 안에 이웃하는 step이 있는지 확인하면 됩니다.
    함수 analyze():
      ladderSteps 배열의 각 요소에 대해:
        이웃하는 발판들을 찾습니다. 찾았다면:
        현재 발판과 다음 발판이 모두 HORIZON이면:
          false를 반환합니다.
        현재 발판이 RIGHT_DOWN이고 다음 발판이 LEFT_DOWN이면:
          false를 반환합니다.
        현재 발판이 LEFT_DOWN이고 다음 발판이 RIGHT_DOWN이면:
          false를 반환합니다.
      반복이 끝나고 여기까지 온다면 true를 반환합니다.
    • display(): ladderladderSteps를 조합해 문자열을 가공하고, 출력합니다.
    함수 display():
      결과값을 담을 문자열 변수를 선언 및 초기화합니다.
      ladder의 각 열을 반복합니다.
        반복이 시작할 때 결과값에 '|'를 추가합니다.
        ladder의 각 행을 반복합니다.
          현재 ladderSteps에서 행, 열에 위치한 데이터가 있는지 찾습니다.(find?)
          ladderSteps에 해당되는 발판이 있다면?
            해당 발판의 step을 결과값에 추가합니다.
          없다면?
            공백 3개를 결과값에 추가합니다.
          조건이 끝나고 '|'를 추가합니다.
        행의 반복이 끝나면 줄바꿈 문자열 `\\n`을 추가합니다.
      각 열의 반복이 모두 끝나면 log에 출력합니다.

추가로 직접 입력해서 그 결과값만 봐도 되지만 어제 배운 readlineoutput을 넣어서 질문 형태와 결과값을 받는 형태로도 구현해보도록 하겠습니다.

구현

1. 사다리 구조 관련 변수 초기화

// 상수(전부 UPPER + SNAKE)
// 사다리 배경, PLAYERS는 사다리의 Row-행, HEIGHT는 사다리의 Col-열로 생각하기.
const PLAYERS = 5;
const HEIGHT = 5;
// 발판 정보
const STEP_TYPES = {
  HORIZON: '---',
  RIGHT_DOWN: '\\-\\',
  LEFT_DOWN: '/-/',
  NOTHING: '   ',
};

// 발판 존재 및 위치 정의 배열: 사다리 데이터에 직접 넣지 말라했으니 발판만 정의함. 행, 열, 발판으로 이루어진 객체가 들어갈 예정.
let ladderSteps = [];
  • 고정된 값, 사다리 리셋 및 발판 배열 등을 선언했습니다. 추후 PLAYERS, HEIGHT를 사다리의 행과 열로 판단합니다.
  • 따로 사다리를 정의해 구현하진 않았고, step의 종류와 위치 정보가 객체로 속하게끔하고, 문자열 가공에 쓸 수 있도록 ladderSteps만 구현했습니다.

2. 함수 구현

1. reset()

작성했던 발판을 빈 배열로 재할당합니다.

// 1. 리셋 함수: 발판을 재정의.
const reset = () => {
  ladderSteps = [];
};

2. randomFill()

// 2. 발판 채우기 함수.
// JS 메소드인 random을 이용할 것임.
const randomFill = () => {
  const stepTypesArr = [
    STEP_TYPES.HORIZON,
    STEP_TYPES.RIGHT_DOWN,
    STEP_TYPES.LEFT_DOWN,
  ];
  // 사다리의 각 행과 각 열을 순회
  for (let row = 0; row < HEIGHT; row++) {
    // 실수: 사다리가 5개이니, 사이에 발판을 놓을 수 있는 자리는 4개. 그래서 -1로 수정..
    for (let col = 0; col < PLAYERS - 1; col++) {
      // 발판을 놓을지 결정하는 난수 생성 (0.0 ~ 1.0)
      const stepCondiRandom = Math.random();
      // 발판을 놓을 확률을 0.5 이상으로 설정 (0.5 이상일 경우에만 발판을 놓음)
      if (stepCondiRandom >= 0.5) {
        // 발판의 유형을 결정하는 난수 생성 (0.0 ~ 1.0)
        const stepTypeRandom = Math.random();
        // 발판을 놓을 위치와 유형을 ladderSteps 배열에 추가
        ladderSteps.push({
          row,
          col,
          // stepTypesArr 배열에서 난수로 결정된 발판 유형 선택
          // 0.0xx ~ 2.xxx 범위의 값을 floor로 0, 1, 2 중 하나 선택, 발판 지정
          step: stepTypesArr[Math.floor(stepTypeRandom * 3)],
        });

        // console.log(
        //   stepCondiRandom - 0.7,
        //   stepTypesArr[Math.floor(stepTypeRandom * 3)],
        // );
      }
      // console.log(ladderSteps);
    }
  }
};
  • PLAYERS, HEIGHT를 사다리의 행과 열로 판단하기에 각 행, 열의 위치를 가지고 순회합니다.
  • 발판 유무 조건 난수를 만들고, 발판의 개수 조정을 위해 0.5보다 높은 난수가 나왔을 때만 그 위치에 발판을 놓습니다.
  • 발판 종류 선택 난수에 * 3을 해 0 초과 3 미만의 수를 반환하게 하고, floor를 이용해 0, 1, 2만 나오게 했습니다.
  • 실수1: 사다리가 5개이니, 사이에 발판을 놓을 수 있는 자리는 4개. 그래서 -1로 수정했습니다.
  • 실수2: 행과 열을 헷갈려 for문에 반대로 써놨었습니다. ij 대신 row로 for문 변수를 직관적이게 만들었습니다.

analyze()

// 3. 발판 유효 검사 함수.
const analyze = () => {
  // console.log('사다리 구조 분석 시작...');
  for (let i = 0; i < ladderSteps.length - 1; i++) {
    const curStep = ladderSteps[i];
    // const nextStep = ladderSteps[i + 1];
    const nextStep = ladderSteps.find(
      (step) => step.row === curStep.row && step.col === curStep.col + 1,
    );
    if (nextStep) {
      // console.log(
      //   `현재 발판 단계: ${i} (row: ${curStep.row}, col: ${curStep.col}, step: ${curStep.step}) | 비교 발판 단계 (row: ${nextStep.row}, col: ${nextStep.col}, step: ${nextStep.step})`,
      // );
      if (curStep.row === nextStep.row) {
        if (
          curStep.step === STEP_TYPES.HORIZON &&
          nextStep.step === STEP_TYPES.HORIZON
        ) {
          // console.log('유효하지 않은 발판 발견: HORIZON 옆 HORIZON');
          return false;
        }
        if (
          curStep.step === STEP_TYPES.RIGHT_DOWN &&
          nextStep.step === STEP_TYPES.LEFT_DOWN
        ) {
          // console.log('유효하지 않은 발판 발견: RIGHT_DOWN 옆 LEFT_DOWN');
          return false;
        }
        if (
          curStep.step === STEP_TYPES.LEFT_DOWN &&
          nextStep.step === STEP_TYPES.RIGHT_DOWN
        ) {
          // console.log('유효하지 않은 발판 발견: LEFT_DOWN 옆 RIGHT_DOWN');
          return false;
        }
      }
    }
  }
  // console.log('사다리가 유효합니다!');
  return true;
};
  • ladderSteps를 검사합니다. 반복문을 돌며 현재 발판과 다음 발판의 유효성을 검사합니다.
  • 실수: nextStepladderSteps[i + 1]로 단순하게 설정해 다음 인덱스의 발판을 검사했습니다. 이렇게 하면 실제 이웃하는 행인지 검사하지 못합니다. find 메서드를 사용해 정확하게 같은 행(row)에 있고 이웃한 열(col)에 있는지 조건을 따져 고쳤습니다.
  • 에러를 고치기 위해 과정을 탐색하려고 log를 찍어 관찰했습니다.

display()

// 4. 사다리 출력 함수.
// 가독성을 위해 결과값 줄바꿈을 나타내기 위해서 한 줄 씩 나오도록 수정.
const display = () => {
  // let resultLadder = '';
  for (let row = 0; row < HEIGHT; row++) {
    let resultLadder = '';
    // 반복이 시작할 때 결과값에 '|'를 추가.
    resultLadder += '|';
    // ladderSteps을 반복해 행, 열에 위치한 데이터가 있는지 찾음.
    for (let col = 0; col < PLAYERS - 1; col++) {
      // 현재 스텝이 있는지 찾음. find
      const curStep = ladderSteps.find(
        (ladderStep) => ladderStep.row === row && ladderStep.col === col,
      );
      // console.log(curStep);

      // 현재 스텝이 있다면 추가. 아니면 공백 추가.
      resultLadder += curStep ? curStep.step : STEP_TYPES.NOTHING;

      // 마무리 할 때 버티컬 바 추가
      resultLadder += '|';
    }
    resultLadder += '\\n';
    // 반환
    console.log(resultLadder);
  }
  // console.log(resultLadder);
};
  • rowcol을 돌며 실제 사다리를 만듭니다.
  • 행의 반복 시작 시 |를 만들고, 열이 반복되면서 스텝 여부 및 |를 추가합니다. 행의 반복이 끝난 후 \n을 출력합니다.
  • 원래는 반복이 끝나고 한꺼번에 출력하려고 했지만 줄바꿈이 일어나지 않으니 가독성이 좋지 않아 한 줄 씩 출력하도록 했습니다.

3. readline 구현

따로 입력값을 주지 않는 알고리즘 테스트이긴 하지만 테스트를 용이하게 하려는 것도 있었고, 사용자와의 상호작용도 중요하다 생각해서 추가해봤습니다.

const readline = require('readline');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

// 사용자 입력을 받아 함수 실행
const handleUserInput = (input) => {
  switch (input.trim()) {
    case 'reset':
      reset();
      display();
      break;
    case 'randomFill':
      randomFill();
      display();
      break;
    case 'analyze':
      console.log('사다리 구조 유효성:', analyze());
      break;
    case 'display':
      display();
      break;
    case 'exit':
      rl.close();
      break;
    default:
      console.log(
        '올바르지 않는 명령어입니다. 사용 가능한 명령어: reset, randomFill, analyze, display, exit',
      );
  }
};

rl.on('line', handleUserInput);

console.log(
  '명령어를 입력해주세요.: reset, randomFill, analyze, display, exit',
);
  • 단순 switch로 입력값을 받게 했습니다. 리셋하거나 랜덤 채우기 등을 할 때, 결과값이 바로 보였으면 좋겠어서 display()를 붙였습니다.
  • exit를 입력해야 명령어 입력이 끝납니다.
  • 유효성 검사 : 다른 명령어를 입력할 경우 문구가 뜨게 하고, 접근성을 상승하기 위해 사용 가능 명령어도 작성했습니다.
  • rl.on으로 한 줄 씩, handleUserInput 함수를 사용하게 했습니다.

결과

화면 기록 2024-06-27 20 32 36

리팩토링

JS의 random() 메소드는 사실 안전하지 않습니다. 참고 : MDN Math.random()
암호학적으로 안전하지 않아 노력만 한다면 패턴이 파악돼 뚫린다는 얘기가 있습니다.

MDN에서도 권장하는 방식인 Math.random() 대신 crypto 모듈을 사용해 난수를 생성하도록 리팩토링하겠습니다.

리팩토링 코드

const readline = require('readline');
const crypto = require('crypto');

...

// JS 메소드인 random을 이용할 것임.
// => random은 안전한 난수가 아니므로 crypto를 사용함.
const getRandom = () => {
  // 난수 생성 함수 (0 ~ 1 사이의 난수를 반환)
  return crypto.randomBytes(4).readUInt32LE() / 0xffffffff;
};
// 2. 발판 채우기 함수.
const randomFill = () => {
  const stepTypesArr = [
    STEP_TYPES.HORIZON,
    STEP_TYPES.RIGHT_DOWN,
    STEP_TYPES.LEFT_DOWN,
  ];
  for (let row = 0; row < HEIGHT; row++) {
    for (let col = 0; col < PLAYERS - 1; col++) {
      // 발판을 놓을지 결정하는 난수 생성 (0.0 ~ 1.0)
      // const stepCondiRandom = Math.random();
      const stepCondiRandom = getRandom();
      if (stepCondiRandom >= 0.5) {
        // 발판의 유형을 결정하는 난수 생성 (0.0 ~ 1.0)
        // const stepTypeRandom = Math.random();
        const stepTypeRandom = getRandom();
        ladderSteps.push({
          row,
          col,
          step: stepTypesArr[Math.floor(stepTypeRandom * 3)],
        });
      }
    }
  }
};

수정 사항 설명

  1. crypto 모듈 추가:

    • crypto 모듈을 사용해 암호학적으로 안전한 난수를 생성해줍니다.
    • crypto.randomBytes()readUInt32LE()를 사용해 0 ~ 1 사이의 난수를 생성하는 getRandom 함수를 구현했습니다.
      1. crypto.randomBytes(4): 4바이트(32비트)의 무작위 데이터를 생성하여 버퍼 객체로 반환합니다.
      2. .readUInt32LE(): 반환된 버퍼 객체에서 32비트 부호 없는 정수를 Little Endian 방식으로 읽어옵니다.
      3. / 0xffffffff: 생성된 무작위 값을 0과 1 사이의 부동 소수점 숫자로 변환합니다.
  2. randomFill 함수 교체:

    • Math.random() 대신 getRandom() 함수를 사용해 난수를 생성해줍니다.

결론

  1. 요구사항 분석
    • 참가자 수와 사다리 높이 결정
    • 구현할 함수 목록과 기능 정의
  2. 입출력값 분석
  3. 의사코드 작성
    • 사다리 구조 관련 변수 데이터 정의
    • 각 함수의 의사코드 작성
  4. 코드 구현
  5. 상수와 변수 초기화 : 고정된 상수와 사다리 데이터를 담을 변수 초기화
  6. reset() 함수 : 사다리 데이터 초기화
  7. randomFill() 함수 : 발판 랜덤 배치 로직 구현
  8. analyze() 함수 : 사다리 유효성 검사 로직 구현
  9. display() 함수 : 사다리 구조를 문자열로 변환해 출력
  10. 입력값 처리(readline 구현) : 사용자 입력을 받아 함수 실행
  11. 리팩토링 : Math.random() 대신 crypto 모듈 사용해 안전한 난수 생성
  1. 새롭게 배운 점

    • crypto 모듈을 사용해 안전한 난수 생성 방법
    • 행과 열이 있는 알고리즘이라면 무조건 row, col 쓰기
  2. 문제 해결에 신경 쓴 부분

    • 코드 가독성: 변수 명과 함수 명을 직관적으로 작성해 가독성을 높임.
    • 유효성 검사: 사다리의 발판 배치가 유효한지 검증하는 로직을 꼼꼼하게 작성함.
  3. 가장 어려웠던 부분과 극복 방법

    • 이웃 발판 유효성 검사: 처음에는 단순히 인덱스로만 비교했지만, 정확하게 이웃 발판인지 검증하기 위해 find 메소드를 사용해 문제를 해결.

어제 클래스를 쓰면서 공부했었는데 오늘 접근법을 보면서 클래스형을 보니 되게 반가우면서도, 오늘 연습했어야했네 생각이 들기도 했습니다. 문제 보면서도, 이거 '완전 클래스네' 싶었는데 함수형으로 작성해도 무방하다는 글을 보고 '함수형이 더 어렵구나!' 라고 생각이 들어서 함수형으로 작성한 게 다르긴 했습니다.
또, 저는 '사다리 데이터에 출력용 문자열을 집어넣지 말아라'를 발판도 집어넣으면 안된다라고 보고, 굳이 사다리 전체를 만들지 않고, 발판 데이터만 만들어 display() 부분에 만들어서 하도록 했습니다. 직관성을 생각하면 수료생님처럼 만드는 게 맞았는지도 모르겠습니다. ㅠ

물론 핵심적인 기능은 다 구현했고, random대신 안전한 난수 crypto를 써서 난수를 생성했고, readline을 써서 사용자의 input과 질문을 작성하기 위한 output을 제작해서 동적으로 구현도 했습니다! 재밌는 구현이었습니다.

profile
코뿔소처럼 저돌적으로

0개의 댓글