본문 바로가기
Programming/JavaScript

'숫자 야구게임 만들기' 토이 프로젝트(2) - [Javascript 입문 _8]

by Muko 2020. 4. 12.

저번 포스팅에서 보여주는 부분은 완성했지만, 숫자 야구게임을 완성시키기는 다음과 같은 5가지 함수를 구현해야 했습니다.

  • 제출 숫자는 4자리 체크
  • 숫자 중복 체크
  • 정답인지 아닌지 체크
  • 힌트 제공 함수
  • 정답 시도 횟수 10번 이내인지 체크

지금부터 하나씩 구현해보도록 해보죠!

 

4자리 체크 함수

우선은 사용자가 입력한 숫자가 4자리인지 체크하는 함수를 만들어보겠습니다. 현재 제가 숫자 야구게임 1편에서 작성한 javascript 코드에는 입력한 숫자가 String으로 넘어오게끔 되어있습니다. 만약 숫자라면 1000이상 10000미만 인지 체크하는 조건문을 사용하면 되고, 지금과 같은 상황이면 length가 4인지 체크하면 됩니다. 아래 코드처럼요!

const isFourDigits = (number) => {
  return number.length === 4;
}

 

코드 자체는 매우 심플합니다. 거기다가 isFourDigits라는 함수가 하는 일은 결국 number.length === 4인지에 대한 결과를 반환하는 것인데, 굳이 함수로 만들어야 하는 의문이 들 수 있습니다. 의문 그대로 저 조건문을 기존 askQuestion에 넣어서 작동시켜도 되는데 왜 함수로 분리했을까요? 정답은 바로 '모듈화'입니다. 작은 기능을 동작하는 함수들을 여러개 만들고, 이 함수들을 이용해서 코드를 동작시키게끔 코드를 구현하게 된다면 나중에 어디서 문제가 발생했는지 확인하기도 편하고, 유지보수가 훨씬 편해집니다. 고쳐야 할 부분이 이미 나눠져있기 때문에 조금만 손 봐도 되니까요! 특히나 자바스크립트는 더욱이 한 개의 함수가 많은 역할을 가지고 있으면 오류 검사, 즉 디버깅이 힘들어 지는데요, 이렇게 처음부터 각 기능을 함수로 잘게 나누어 놓으면 나중에가 편해집니다.



숫자 중복 체크 함수

그럼 다음으로 숫자가 중복되어있는지 체크하는 함수를 만들어보겠습니다. 이 함수는 1편에서 작성한 숫자 생성 함수와 비슷한 방법을 사용하면 쉽게 구현할 수 있습니다. 저는 첫 번째로 제시했던 flags 배열을 이용한 중복 체크를 이용해서 함수를 만들어 보겠습니다.

const isOverlap = (number) => {
  const digits = number.split('');
  const flags = Array(10).fill(false);

  return digits.map(digit => {
    const index = parseInt(digit);
    if (!flags[index]) {
      flags[index] = true;
    } else {
      return true;
    }
  }).includes(true);
}

 

먼저 입력으로 들어온 4자리 수가 String으로 들어온다는 점을 이용해서 첫 번째 줄에서 split 메소드를 이용해 각 자리 별로 분리했습니다. split('');을 사용하면 공백이 없더라도 글자 하나 단위로 분리가 가능하다는 것을 기억하시면 종종 사용하시게 될 겁니다. 그 다음에는 이 숫자가 존재했는지 체크를 할 수 있도록 불리언 배열을 만들었습니다. 이 배열은 총 길이가 10인 배열로, false로 초기화 시켰습니다.

우리가 봐야할 부분은 return 뒤에 붙어있는 코드입니다. 함수 인자인 number를 분리한 digits에 map을 이용해서 각 배열 요소에 접근했습니다. 예를 들어 digits가 ['5', '1', '2', '9']라고 했을 때 digits.map(digit => { 여기!! }) 에서 여기 부분에서 사용하는 digit은 4번 Loop가 돌면서 각각 '5', '1', '2', '9'가 됩니다. 저 map 메소드는 배열의 요소 갯수만큼 반복문 loop를 돌게 됩니다. 지금 4자리 수를 나눈 배열이므로 length가 4라서 4번 반복하게 됩니다. 그렇게 받아온 각 digit를 숫자값으로 바꿔주고, if 문을 이용해서 방문한적이 있는지를 체크하는 코드를 작성했습니다.



정답 체크 함수

정답인지 아닌지를 체크하는 것은 간단합니다. 현재 들어온 입력값과, 답이 일치하는지를 판단해서 그 결과를 발환하면 됩니다. 코드 예시를 보시면 간단하다는 것이 바로 이해가 되실겁니다.

const isCorrect = (number, answer) => {
  return number == answer;
}

 

제가 연산자 포스팅에서 자바스크립트는 비교를 할 때 자료형까지 비교하기 위해서는 === 을 사용해야한다고 설명했었습니다. 그런데 위의 코드에서는 == 를 사용했는데 왜 그럴까요?

이유는 자료형이 다르기 때문입니다. 입력으로 들어오는 number는 String이지만, 우리가 가지고 있을 answer는 숫자입니다. 그래서 같은 값이지만 자료형이 달라서 === 을 사용하게 되면 반드시 false를 반환하게 되죠. 그래서 number를 parseInt 함수를 이용해서 숫자로 바꾸던가, answer값을 String으로 변환시켜줘야 하는 번거로움이 있습니다. 하지만 자료형이 상관없이 값만을 비교해도 된다면 위와 같이 간단하게 쓸 수 있기 때문에 == 을 사용했습니다. 하지만 제일 좋은 것은 아무래도 자료형을 일치시킨 후 === 을 사용하는 것이겠죠?

만약 answer를 처음부터 String 형태로 가지고 있게 구현한다면 다음과 같이 구현하면 됩니다.

const isCorrect = (number, answer) => {
  return number === answer;
}

 

작성하다보니 answer를 String으로 처음부터 가지고 있는게 다음 함수를 작성할 때도 편하겠다는 생각이 들었습니다. 연산자에 대해서 조금 알았으니, 코드를 작성하기 쉽게끔 answer를 만드는 함수는 뒤에서 String으로 반환하게 구현하겠습니다.



힌트 제공 함수

이 함수가 숫자 야구게임의 핵심입니다. 이 함수가 제 역할을 하지 못하면 이 프로그램은 망할 수 밖에 없습니다. 추리를 해야하는데 뭔가 힌트가 이상해서 계속해서 틀린다면 아무도 이 프로그램을 사용하지 않을테니까요! 그래서 이 함수가 핵심이라고 볼 수 있습니다.

이 함수는 결국 스트라이크(S)와 볼(B)을 잘 찾아내야 하는데요, 둘의 조건이 다른 만큼 함수를 분리해서 작성하겠습니다. 먼저 스트라이크 판별 함수입니다.

const findStrikes = (number, answer) => {
  let strikes = 0;
  const digits = number.split('');

  digits.map((digit, index) => {
    if (digit === answer[index]) {
      strikes++;
    }
  })
  return strikes;
}


다음은 볼 판별 함수입니다. 볼은 스트라이크가 아니면서 같은 숫자가 사용된 것을 카운트해야하므로 조건이 추가됩니다. 

const findBalls = (number, answer) => { 
  let balls = 0; 
  const digits = number.split('');
  const flags = Array(10).fill(false);

  // answer에 포함되어있는 숫자 체크
  answer.split('').map(digit => {
    flags[parseInt(digit)] = true;
  });

  // ball 카운트
  digits.map((digit, index) => {
    if (answer[index] !== digit && flags[parseInt(digit)]) {
      balls++;
    }
  });
  return balls;
}


그래서 그런지 코드가 조금 깁니다. answer에 포함된 숫자가 무엇이 있는지 저장하는 파트, 그리고 number와 answer를 비교하면서 스트라이크가 아니지만 숫자가 동일한게 있을 경우 ball 카운트를 증가하는 파트로 구성되어 있습니다. 아래 부분에 if문을 보시면 `&&` 라는 연산자를 사용했는데, 이 의미는 AND 입니다. 그래서 앞의 조건과 뒤의 조건 모두 truthy한 결과를 가지고 있어야 조건을 만족한다고 프로그램이 인식하게 됩니다. 이 연산자의 또다른 특징 하나는 두 조건 모두 turthy해야하기 때문에, 왼쪽부터 읽으면서 하나라도 falsy한 값이 나오면 그 뒤의 조건은 판단하지 않고 바로 끝나게 됩니다.

스트라이크와 볼 판별 함수를 모두 만들었으니, 이제는 힌트 제공 함수를 만들어 봅시다.

const giveHint = (number, answer) => {
  if (isCorrect(number, answer)) {
    gameClaer();
    return '정답입니다!';
  }

  const strikes = findStrikes(number, answer);
  const balls = findBalls(number, answer);
  return 'S: ' + strikes + ',B: ' + balls;
}



정답 시도 횟수 10번 이내인지 체크하는 함수

아주 간단합니다.

const isValidToAttempt = (attempts) => {
 return attempts < 10;
}



메인 함수 구현

자 이제 필요한 함수 구현은 모두 끝났습니다. 이제 이 함수들을 이용해서 실제로 게임이 동작할 수 있게끔 구현을 해야하는데요, 일단 정답을 만들어내는 함수와 게임의 진행사항을 저장할 수 있는 객체 하나를 먼저 만들겠습니다. 정답을 만드는 함수는 첫 번째 포스팅에서 설명했으니 참고하시길 바랍니다! baseballs.js 가장 상단에다가 아래 코드를 작성해주세요.

const initAnswer = () => {
  const flags = Array(10).fill(false);

  let answer = '';
  while (answer.length < 4) {
    const random = parseInt(Math.random() * 10);

    if (!flags[random]) {
      answer += random;
      flags[random] = true;
    }
  }
  return answer;
}

const gameObject = {
  answer: initAnswer(),
  attempts: 0,
  isClear: false,
  end: false
};

 

그리고 게임의 종료와 성공에 대한 함수를 가장 아래 부분에 작성해주세요.

const gameClaer = () => {
  const answerBox = document.querySelector('.answerBox');
  answerBox.classList = answerBox.classList[0] + ' solved';
  gameObject.isClear = true;
}

const gameFailed = () => {
  const answerBox = document.querySelector('.answerBox');
  answerBox.classList += ' failed';
  gameObject.end = true;
}

 

이제 진짜 필요한 모든 재료는 준비가 끝났습니다. 이제 여러분들이 이 재료들을 어떻게 하면 순서를 잘 조절해서 제대로 동작하게끔 만들 수 있는지 고민하면서 직접 해보시길 바랍니다. 정말 모르시겠다 하시는 분들은 아래 코드를 펼쳐서 확인해주세요.

코드 접기/펼치기
const initAnswer = () => {
  const flags = Array(10).fill(false);

  let answer = '';
  while (answer.length < 4) {
    const random = parseInt(Math.random() * 10);

    if (!flags[random]) {
      answer += random;
      flags[random] = true;
    }
  }
  return answer;
}

const gameObject = {
  answer: initAnswer(),
  attempts: 0,
  isClear: false,
  end: false
};

const makeHintStr = (number, hint) => {
  return `<strong>시도 ${gameObject.attempts}</strong>: ${number}, <strong>결과</strong>: ${hint}`;
}

const askQuestion = (event) => {
  event.preventDefault();

  if (!gameObject.isClear && !gameObject.end) {
    const question = event.target[0].value;
    const questionBox = document.querySelector('.questionBox');

    if (!isFourDigits(question)) {
      alert('네 자리 숫자를 입력해주세요.');
    } else if (isOverlap(question)) {
      alert('각 자리수는 숫자가 중복되면 안됩니다.');
    } else {
      gameObject.attempts++;
      const result = makeHintStr(question, getHint(question, gameObject.answer));
      questionBox.innerHTML += `<div>${result}</div>`

      if (isInvalidToAttempt(gameObject.attempts) && !isCorrect(question, gameObject.answer)) {
        alert('도전 실패!');
        gameFailed();
      }
    }
  }

  event.target[0].value = '';
}

const isFourDigits = (number) => {
  return number.length === 4;
}

const isOverlap = (number) => {
  const digits = number.split('');
  const flags = Array(10).fill(false);

  return digits.map(digit => {
    const index = parseInt(digit);
    if (!flags[index]) {
      flags[index] = true;
    } else {
      return true;
    }
  }).includes(true);
}

const isCorrect = (number, answer) => {
  return number === answer;
}

const findStrikes = (number, answer) => {
  let strikes = 0;
  const digits = number.split('');

  digits.map((digit, index) => {
    if (digit === answer[index]) {
      strikes++;
    }
  })
  return strikes;
}

const findBalls = (number, answer) => {
  let balls = 0;
  const digits = number.split('');
  const flags = Array(10).fill(false);

  // answer에 포함되어있는 숫자 체크
  answer.split('').map(digit => {
    flags[parseInt(digit)] = true;
  });

  // ball 카운트
  digits.map((digit, index) => {
    if (answer[index] !== digit && flags[parseInt(digit)]) {
      balls++;
    }
  });
  return balls;
}

const getHint = (number, answer) => {
  if (isCorrect(number, answer)) {
    gameClaer();
    return '정답입니다!';
  }

  const strikes = findStrikes(number, answer);
  const balls = findBalls(number, answer);
  return 'S: ' + strikes + ',B: ' + balls;
}

const isInvalidToAttempt = (attempts) => {
  return attempts >= 10;
}

const gameClaer = () => {
  const answerBox = document.querySelector('.answerBox');
  answerBox.classList = answerBox.classList[0] + ' solved';
  gameObject.isClear = true;
  answerBox.innerHTML = gameObject.answer;
}

const gameFailed = () => {
  const answerBox = document.querySelector('.answerBox');
  answerBox.classList += ' failed';
  gameObject.end = true;
  answerBox.innerHTML = gameObject.answer;
}



모두 구현이 완료되면 화면은 다음과 같이 동작합니다.

 

여기까지 따라오시느라 수고하셨습니다!
다음 시간에는 반복문에 대해서 포스팅하겠습니다.

댓글2