본문 바로가기
개발관련/이것저것

우테코 프리코스 풀어보기 - 숫자 야구 게임

by yjoo_ 2023. 10. 24.

회사에 출근하고 잠깐 시간이 비어있길래 뭔가 할 게 없을까 하다가

 

Github에서 지인이 우테코 프리코스를 fork한 것을 보게 되었다.

 

심심해서 한번 풀어보았다.

 

만약 제가 작성한 내용이 문제가 된다면 얼마든지 삭제할 용의가 있으니 연락 부탁드립니다.


1. 문제 파악

대충 과제를 보아하니 숫자 야구 게임을 만드는 것이 목적으로 보였다.

우째 코테 연습문제 같네-_-

입력이 서로 다른 3자리 수로 제한 된 것으로 보아

 

131, 222 같은 입력도 IllegalArgumentException을 발생시켜야 하나?

 

볼과 스트라이크 개수를 어떻게 처리할지?

 

랜덤이나 값 입력은 어떻게 받는지?

 

평소에 자바를 잘 사용하지도 않으니 찾아볼 것이 많았다.


2. 풀이와 과정

친절하게도 우테코에서 지원해주는 라이브러리인 Randoms와 Console 있었다.

 

Console에는  scanner를 이용해 입력을 받는 readline() 메서드가 작성되어있었고,

 

Randoms에는 범위 내의 랜덤 숫자를 뽑아주는 pickNumberInRange()

 

범위 값과 count값을 주면 count만큼 범위 내 값을 리스트로 뽑아주는 pickUniqueNumberInRange()

 

등이 있었다.

 

사실 이것만 보고 뭐야? 다 만들어져있네? 갖다쓰기만 하면 되겠네? 생각했다.

하지만 문서를 읽어보니 사용할 수 있는건, readline()pickNumberInRange()정도 인듯 했다.

 

어쩔 수 없지. 그래도 사용 예시에 답이 나와있는듯 했고, 어차피 난 직장인이라 문제 풀어봤자 큰 의미도 없으니

 

편하게 풀기로 했다. 그래도 최대한 규칙은 지켰다.

 

public class Application {
    static final String START = "숫자 야구 게임을 시작합니다.";
    static final String INPUT = "숫자를 입력해주세요 : ";
    static final String END = "3개의 숫자를 모두 맞히셨습니다! 게임 종료";
    static final String RESTART = "게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.";

    public static void main(String[] args) {
        boolean isGameOver = true;
        System.out.println(START);
        try {
            while (isGameOver) {
                Game game = new Game(new NumberGenerator());
                isGameOver = game.play();
            }
        } catch (IllegalArgumentException e){
            throw new IllegalArgumentException();
        }

    }
}

먼저 메인 메서드, 다른 클래스를 선언하면 안된다는 제약이 없었기에 클래스를 나눠서 작성하기로 했다.

 

대신 출력하는 메시지들은 모두 static 상수로 class 레벨에 선언해서 사용했다.


타 클래스에서도 Application 인스턴스를 생성하지 않아도 Application.START 같은 방식으로 호출할 수 있다.

 

class를 3개정도로 나누기로 했다.

  1. 게임을 진행할 Game 클래스.
  2. 플레이어의 입출력을 처리할 Player 클래스
  3. 게임의 답을 만들어내는 NumberGenerator 클래스

NumberGenerator를 왜 나눠두었냐는 의문이 들 수도 있는데, 객체지향적 설계를 위해서 나눠두었다.

 

무슨 뜻이냐면, 객체지향에서 하나의 클래스가 하나의 책임만 지게하는 것이 좋다.

 

숫자 생성 로직 자체를 하나의 클래스로 관리하게 만든 것이다.

 

하나만 대표적으로 얘기해주자면, 만약 코드를 디버깅할 때를 생각해보자.

 

NumberGenerator를 그대로 냅두고, 새로운 로직을 수행하는 클래스로 변경하는 것이 간단해진다.

 

class NumberGenerator {
    public List<Integer> generate(){
        List<Integer> numbers = new ArrayList<>();
        while (numbers.size() < 3){
            int number= Randoms.pickNumberInRange(1, 9);
            if (!numbers.contains(number)){
                numbers.add(number);
            }
        }
        return numbers;
    }
}

단순히 숫자를 제너레이트 하는 역할을 수행한다.

 

매 게임을 굴릴 때 마다 해당 클래스 인스턴스를 Game 인스턴스에 넘겨주었다.

이 부분

그리고 game의 play 메서드를 호출했다.

class Game{
    private NumberGenerator numberGenerator;

    public Game(NumberGenerator numberGenerator){
        this.numberGenerator = numberGenerator;
    }
//	(...생략...)
    public boolean play(){
        List<Integer> answer = numberGenerator.generate();
        while (true){
            System.out.print(Application.INPUT);
            Player player = new Player(Console.readLine());
            if (guess(answer, player.getNumbers())){
                break;
            }
        }
        System.out.println(Application.END);
        System.out.println(Application.RESTART);
        return restart(Console.readLine());
    }
}

 

클래스 생성자에서 numberGenerator 인스턴스를 받아 Game클래스에 할당한다.

 

그리고 바로 play메서드에서 numberGenerator로 정답을 생성시킨 뒤 사용자에게 입력을 받는다.

 

Player에 생성자 매개변수로 readline메서드를 주게되면, 입력을 받은 후 String값이 생성자로 넘어가게 된다.

 

class Player {
    private List<Integer> numbers;

    public Player(String input) {
        this.numbers=parseInput(input);
        if (this.numbers.size() != 3){
            throw new IllegalArgumentException();
        }
    }

    private List<Integer> parseInput(String input){
        String[] split = input.split("");
        List<Integer> numbers = new ArrayList<>();
        for (String number : split){
            if (!isNumric(number)){
                throw new IllegalArgumentException();
            }
            if (numbers.contains(Integer.parseInt(number))) {
                throw new IllegalArgumentException();
            }
            numbers.add(Integer.parseInt(number));
        }
        return numbers;
    }
    public static boolean isNumric(String num){
        try {
            Integer.parseInt(num);
            return true;
        } catch (NumberFormatException error){
            return false;
        }
    }
    public List<Integer> getNumbers(){
        return numbers;
    }
}

그러면 생성자에서 int로 변환 가능한지 검사한 뒤 값을 집어넣게 될 것이다.

 

이렇게 받아온 값을 guess에서 정답인지 아닌지 검사를 수행한다.

 

    private boolean guess(List<Integer>answer, List<Integer>input){
        int strike = 0;
        int ball = 0;
        for (int i = 0; i < 3; i++){
            if (Objects.equals(answer.get(i), input.get(i))){
                strike++;
            } else if (answer.contains(input.get(i))){
                ball++;
            }
        }
        if (strike == 3){
            System.out.println(strike + "스트라이크");
        } else if (strike == 0 && ball == 0) {
            System.out.println("낫싱");
        } else if (strike == 0) {
            System.out.println(ball + "볼");
        } else if (ball == 0) {
            System.out.println(strike + "스트라이크");
        } else {
            System.out.println(ball + "볼 " + strike + "스트라이크");
        }
        return strike == 3;
    }

이 부분은 조금 처리가 아쉽긴한데, 바꾸기도 뭐해서 수정하진 않았다.

 

guess 메서드에서 정답 검사와 출력까지 함께 해버리기 때문이다.

 

검사와 출력으로 나누는 것이 훨씬 좋다.

 

하여튼 3strike(정답)이 아니라면, 모두 false를 리턴하게 될거고, 다시 입력받는 반복문이 돌게 될 것이다.

 

만약 정답이라면 반복문이 break;에 의해 종료되고

 

문구를 출력한 뒤 restart 메서드의 결과 값에 따라 게임을 지속할지 종료할지 결정하게 된다.

 

    private boolean restart(String input) {
        int inputNum;
        if (!Player.isNumric(input)) {
            throw new IllegalArgumentException();
        } else {
            inputNum = Integer.parseInt(input);
            if (inputNum != 1 && inputNum != 2) {
                throw new IllegalArgumentException();
            }
        }
        return inputNum == 1;
    }

이 때도 마찬가지로 사용자의 입력을 주의해야 한다.

 

재시작하는 경우인 1이 아니라면 false를 return해야한다


3. 테스트

의외로 꽤 애먹었던 구간인데 gradlew clean test로 테스트를 해보라길래 수행해봤는데...

 

에러가 나기 시작했다.

어 잘굴러가는데 왜??

이제보니까 출력 문구에 에러가 있었다.

스트라이크보다 볼이 먼저 출력되어야 하는데, 스트라이크부터 출력된 것. 그리고 게임종료도 띄워쓰기 에러가 있었다.

 

그래도 금방 깨닫고 수정했는데 문제는 그 다음.

 

게임 다시 시작을 입력했을 때 예기치 않은 값을 입력할 경우였는데

 

아무리 뜯어봐도 논리적 오류가 없는데 테스트 실패가 떴다.

하지만 이것도 의외의 복병이었다...

 

위의 코드에는 수정되었지만, 난 이 부분이 에러를 출력시키라는 줄 알고 해당 에러를 인자로 받아 출력시켰다.

 

그랬더니 에러가 출력되서 틀린 것이었다.

 

throw로 수정해주고 테스트 했다.

테스트 빌드 성공

물론 내가 github에 PR를 날릴 순 없지만, 재밌게 풀었던 것 같다.

 

대략 30분 정도 걸렸는데, 겨우 숫자 야구 게임이지만 자주 해본 적은 없던지라

 

로직을 생각하는데 시간을 거의 쓴 것 같다.

 

아무래도 요즘 만지는게 C언어고, 애초에 C를 주력으로 했다보니 아직 난 C 스타일을 많이 버리지 못했다.

 

막상 우테코 깃허브 들어가서 다른 사람들이 푼 걸 보니 조금 부끄럽긴 했다.

 

다음에는 좀 더 구조가 명확하고, 기능별로 분리된 클래스를 작성해봐야겠다.