끄적끄적/TIL

TIL #8 숫자 야구 게임 최종 회고

hubaek 2024. 9. 24. 13:39

 

숫자 야구 게임이란?

- 1~9 사이의 서로 다른 3자리 수가 정답으로 주어지면 정답을 유추해서 맞추는 게임

- 야구 용어로 스트라이크, 볼, 아웃으로 힌트가 주어진다

  * 정답과 자리, 수가 같으면 스트라이크, 자리는 다르고 수가 같으면 볼, 다 다르면 아웃으로 표시

 

이번에 숫자야구게임을 진행하면서 객체지향 설계에 신경을 써보려고 했는데,, 아직 실력이 많이 부족한지 떠오르지 않는다

 - 객체의 상태, 행위, 책임을 생각해서 객체간 협력하는 이미지를 그릴 줄 알아야한다. (그리고 싶어요,,)

 

초기에 생각했던 객체

계산기와는 달리 메인에서 게임 진행을 하는 로직을 쓰기 싫어서 플레이어를 두고 플레이어가 게임을 진행하는 쪽으로 생각을 했다가 요구사항을 보고, 플레이어 객체는 안쓰기로 했다.

그리고 힌트와 게임 기록을 보여주는 BaseballGameDisplay 객체를 추가하였다.

 

처음에 어떻게 시작해야하나 막막해서 클래스설계할때 힌트를 보니 BaseballGame클래스의 기본생성자에서 정답을 만들어내는 로직이 들어갔었다 (이것으로 인해 여러가지 배우게 됨)

 

public BaseballGame() {
        // 1~9의 숫자를 리스트에 넣음
        // 단점? List 배열사이즈가 1~9까지 만들어야함 거기서 앞에서 3개의 값을 꺼내는 것.
        List<Integer> numbers = new ArrayList<>();
        for (int i=1; i <= 9; i++){
            numbers.add(i);
        }

        Collections.shuffle(numbers);

        int digit1 = numbers.get(0);
        int digit2 = numbers.get(1);
        int digit3 = numbers.get(2);
		
        // 3자리 수의 정답 만들기
        this.answer = digit1*100 + digit2 * 10 + digit3;

    }

BaseballGame의 기본생성자.  1~9의 3자리 수 생성(각 자리의 수는 중복이 되지 않게)

처음엔 1~9의 각 자리수가 중복이 되지 않게 정답을 어떻게 만들지란 생각을 하였고, List의 특성이 순서가 있지만, 중복은 없다

List를 쓰면서 단점은 1~9의 숫자를 넣고 Collections.shuffle을 하고 맨앞의 인덱스부터 3개만 활용하기때문에 List의 길이를 9까지 만드는 점이 걸리긴 했다. 

 

입력 유효성 검사

    // 입력값 유효성 검사 메서드
    // 유효성 검사 메서드인데.. 여기에 모든 유효성 검사를 다 넣는게 맞는건가??
    private boolean validateInput(String inputAnswer) {
        // 정답 길이가 3인지 확인
        if(inputAnswer.length() == 3) {
            // 입력한 값이 모두 숫자인지 확인하는 로직
            try {
                // 입력한 값 int 타입으로 변환
                Integer.parseInt(inputAnswer);
            } catch (NumberFormatException e) {
                System.out.println("3자리 숫자만 입력해주세요.");
                return false;
            }

            // 입력한 값에 숫자가 중복인지 체크
            Set<Character> duplicateAnswer = new HashSet<>();
            for (int i = 0; i < inputAnswer.length(); i++) {
                char digit = inputAnswer.charAt(i);
                if (!(duplicateAnswer.add(digit))) {
                    System.out.println("중복입니다.");
                    return false;
                }
            }
            return true;
        }
        System.out.println("3자리 아님");
        return false;
    }

주석에도 보다싶이 정답을 입력한 입력값을 유효성을 검사하는 메서드 validateInput(String inputAnswer)를 만들었고 

거기에 입력값의 유효성 검사 로직을 다 넣었는데, 처음 inputAnswer(이하, 입력값)이 3자리인지 확인하고, 숫자인지 확인하는 로직까지는 괜찮다고 생각했다.

입력값의 각 자리의 수는 중복으로 들어가면 안되기 때문에 중복인지 체크하는 로직을 추가하면서 중복 체크 로직은 다른 메서드로 빼고 validateInput에서 해당 메서드를 호출을 해야하나? 아니면 그냥 적는게 맞나 고민이 되었다. 

 

 

 

많이 해멨던 로직 - 게임 기록 보기

게임 시작 전 1. 게임시작하기 2. 게임 기록 보기 3. 종료하기 

3가지 메뉴가 있는데 2. 게임 기록 보기를 선택하면 n번째 게임 - n번 시도 를 출력해준다.


개요

여기서 BaseballGame 클래스에서 게임횟수와 시도 횟수 두가지가 필요한데 두개를 Map으로 담아두고

BaseballGameDisplay의 gameLog의 매개변수로 담아서 호출을 해야하는데 호출은 Main에서 2번을 눌렀을때 해야 했다. 

- 여기서 BaseballGame의 gameLogMap을 Main에서 baseballGameDisplay.gameLog(gameLogMap)으로 호출을 시키고 싶은데 map을 main으로 어떻게 옮겨야하는지 고민을 했다.. (기초가 부족한 이유)

 ** 단순하게 BaseballGame 객체를 생성해서 게임을 하기때문에 baseballGame.gameLogMap으로 인스턴스 변수를 활용할 수 있었다.

그래서 호출도 가능하겠다. 2번을 누르면 게임 기록 보기가 돼야하지만? Map에 담긴 값이 없는 것이다. 

이유를 찾아보니 

public class Main {
    public static void main(String[] args) {

        BaseballGameDisplay baseballGameDisplay = new BaseballGameDisplay();

        Scanner sc = new Scanner(System.in);
        
        while (true) {
            System.out.println("1. 게임시작하기 2. 게임기록보기 3. 종료하기");

            BaseballGame baseballGame = new BaseballGame();

            int menuSelection = sc.nextInt();

            try {
                // 1. 게임시작하기 2. 게임기록보기 3. 종료하기
                if (menuSelection == 1) {
                    baseballGame.play(); // 게임시작
                } else if (menuSelection == 2) {
                    // 인스턴스 변수일때 사용
                    baseballGameDisplay.gameLog(baseballGame.gameLogMap);
                } else if (menuSelection == 3) {
                    System.out.println("게임을 종료합니다.");
                    break;
                } else {
                    System.out.println("올바른 숫자를 입력해주세요.");
                }
            } catch (NumberFormatException e) {
                System.out.println("1~3의 숫자를 입력하세요");;
            } catch (Exception e) {
                System.out.println("예기치 않은 오류가 발생했습니다." + e.getMessage());
            }

        }
    }
}

 기본생성자에서 정답을 새로 만들어주기 때문에, 게임을 계속 반복하기 위해선 객체 생성을 while문 안에 넣고 했었기 때문이다.

그래서 BaseballGame의 play메서드에서 while문이 끝나고 정답을 맞추고, Main에서 2번을 눌렀을 땐, 새 객체가 생성이 되기 때문에 게임이 진행이 안된 것으로 되어 gameLogMap에는 아무 값이 담기지 않는 것이다..

 

여기서 생각한 것은 2가지 방법인데

1. 객체생성을 while문 밖으로 빼고, BaseballGame기본생성자의 정답을 만드는 로직을 play()메서드 안에 넣는 방법

2. 튜터님의 도움으로 생각한 것인데, 그대로 두고 BaseballGame에서 map에 값을 담기 때문에 gameLogMap을 인스턴스 변수가 아닌

static변수로 해서 저장하는 방법이다.

public class BaseballGame {

    // 인스턴스 변수 선언(필드) - 다른 메서드에서 정답을 비교하기 위함
    private final int answer;
    Scanner sc = new Scanner(System.in);

    BaseballGameDisplay baseballGameDisplay = new BaseballGameDisplay();

    // 시도횟수
    int tryCount = 0;
    // 게임 횟수
    static int roundNumber = 0;

    // 게임횟수와 시도횟수를 담을 map
    static Map<Integer, Integer> gameLogMap = new HashMap<>();
}

iv(인스턴스변수) - tryCount

cv(클래스변수, static 변수) - roundNumber , gameLogMap

여기서 시도횟수 tryCount는 iv로 둔 이유는 매 게임의 시도횟수를 보기 위함이고, 시도횟수를 cv로 두게되면

n번째 게임에 시도횟수가 누적해서 쌓이게 된다. (누적을 원한다면 cv로)

게임횟수는 cv가 필수인게, main에서 객체를 새로 생성하기때문에 iv로 두면 매번 1만 담겨서 한 게임의 시도횟수만 저장된다(사실상 마지막 게임한 기록만 남게됨)

Map은 cv로 하지않으면 애초에 게임 기록을 보는 변수인데, Main에서 게임 기록을 보기전 객체를 새로 생성하기에, Map은 비어있는 상태이고 cv여야 게임횟수와 시도횟수가 담긴 게임 기록을 확인 할 수 있다.

 

배운 점

해당 로직을 구현하면서 인스턴스 변수와 클래스 변수에 대해서 조금 더 자세히 공부를 하게 되었고

어떻게 보면 기초적인 부분인데, while문에서 객체가 반복 생성되고, map이 들어가면서 조금 더 헷갈려했던 부분이었던 것 같다.

이래서 기초를 무시할 수 없는 것 같았다.

 

이번에는 컬렉션이 들어가면서 컬렉션을 활용한 로직이 많이 들어가면서 chatGPT의 도움을 많이 받았는데 시간 부족으로 인해, 직접적으로 많이 물어봐서 조금 아쉬웠던 것 같다. 

앞으로 활용은 하되 직접적인 코드 질문은 지양하고, 개념적인 부분으로 질문하면서 스스로 생각을 하게끔 활용해야겠단 생각을 했다.