Programming

[JAVA]숫자 야구 - 숫자 맞추기 논리 게임 그리고 트러블 슈팅

물에빠진사람 2024. 6. 21. 00:52
반응형

 

처음 프로그래밍 공부를 시작할 때 자바스크립트를 배우면서 만들어봤던 숫자야구를 약 1년 만에 새로운 언어 자바를 통해 다시 만들어보게 되었다.

 

1. 게임 규칙

  • 컴퓨터는 0과 9 사이의 서로 다른 숫자 3개를 무작위로 뽑습니다. (ex) 123, 759
  • 사용자는 컴퓨터가 뽑은 숫자를 맞추기 위해 시도합니다.
  • 컴퓨터는 사용자가 입력한 세 자리 숫자에 대해서, 아래의 규칙대로 스트라이크(S)와 볼(B)을 알려줍니다.
    • 숫자의 값과 위치가 모두 일치하면 S
    • 숫자의 값은 일치하지만 위치가 틀렸으면 B
  • 기회는 무제한이며, 몇 번의 시도 후에 맞췄는지 기록됩니다.
  • 숫자 3개를 모두 맞춘 경우, 게임을 종료합니다. 

2. 설계 및 구현

제일 먼저 떠오른 방법은 Main.java에 난수 생성을 통해 맞춰야 하는 정답 생성 후 사용자 입력값을 비교하는 것이었다. 하지만 이전에 한 번 만들어본 게임이기도 하고 Java를 공부하면서 객체지향에 대해서도 공부한 만큼 조금 더 객체지향적으로 만들어보고 싶었다.

 

객체로 나누는 기준은 속성과 기능이다. 야구게임에서 속성은 게임을 하는 플레이어, 정답을 생성하는 컴퓨터, 야구게임의 세상이다. 게임을 하는 플레이어와 컴퓨터는 같은 속성을 공유한다. 둘 다 숫자를 보유하기 때문이다. 이를 통해 파일을 3개로 나누기로 하였다.

  • Main.java
  • Player.java
  • Game.java

Main클래스에서는 실행만하고, Player객체를 통해 사용자 입력 객체와 컴퓨터의 난수 생성을 하고, Game 클래스에서 게임 필요한 매소드를 만들기로 하였다.

 

//Main.java
public class Main {
    public static void main(String[] args) {
        Game game = new Game();
        game.startGame();
    }
}

실행만 하는 Main.java.

//Player.java
import java.util.LinkedHashSet;

public class Player {

    private int[] numbers;

    public Player(boolean checkComputer) {
        this.numbers = new int[Game.STRIKE_NUMBER];
        if (checkComputer) {
            computerNumberCreate();
        }
    }

    private void computerNumberCreate() {
        LinkedHashSet<Integer> comNum = new LinkedHashSet<>();
        while (comNum.size() < Game.STRIKE_NUMBER) {
            int tempNum = (int)(Math.random() * 10);
            comNum.add(tempNum);
        }
        int i = 0;
        for (Integer num : comNum)
            numbers[i++] = num;
    }

    public void setNumbers(int[] numbers) {
        this.numbers = numbers;
    }

    public int[] getNumbers() {
        return numbers;
    }
}

컴퓨터의 난수생성과 사용자의 입력값을 받기 위한 객체 생성을 담당한다.

 

//Game.java

import java.util.LinkedHashSet;
import java.util.Set;
import java.util.Scanner;


public class Game {

    public static final int STRIKE_NUMBER = 3;

    private final Scanner input;
    private final Player computer;
    private final Player user;
    private int strike, ball;
    private int attempts = 1;
    private boolean onPlay = true;

    public Game() {
        this.computer = new Player(true);
        this.user = new Player(false);
        this.input = new Scanner(System.in);
    }

    public void startGame() {
        //정답 출력 코드
        //System.out.println("정답: " + convertArrayToString(computer.getNumbers()));
        System.out.println("컴퓨터가 숫자를 생성하였습니다. 답을 맞춰보세요!");
        while (onPlay) {
            user.setNumbers(getUserNumbers());
            checkNumbers();
            printResult();
            if (strike == STRIKE_NUMBER) {
                endGame();
                continue;
            }
            attempts++;
            resetCounts();
        }
    }

    private void checkNumbers() {
        for (int i = 0; i < STRIKE_NUMBER; i++) {
            for (int j = 0; j < STRIKE_NUMBER; j++) {
                if (computer.getNumbers()[i] == user.getNumbers()[j]) {
                    if (i == j)
                        strike++;
                    else
                        ball++;
                }
            }
        }
    }

    private void endGame() {
        System.out.println(attempts + "번만에 맞히셨습니다.");
        System.out.println("게임을 종료합니다.");
        input.close();
        onPlay = false;

    }

    private void resetCounts() {
        strike = 0;
        ball = 0;
    }

    private void printResult() {
        if (strike == STRIKE_NUMBER) {
            System.out.println(strike + "S");
        } else if (ball == STRIKE_NUMBER) {
            System.out.println(ball + "B");
        } else {
            System.out.println(ball + "B" + strike + "S");
        }
    }

    //정답을 출력시킬때만 사용
    private String convertArrayToString(int[] numbers) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int number : numbers)
            stringBuilder.append(number);
        return stringBuilder.toString();
    }


    private int[] getUserNumbers() {
        int[] numbers = new int[STRIKE_NUMBER];
        Set<Integer> checkSame = new LinkedHashSet<>();

        while (true) {
            System.out.print(attempts + "번째 시도: ");
            String[] userNumber = input.nextLine().split("");

            if (userNumber.length != STRIKE_NUMBER) {
                System.out.println("숫자를 " + STRIKE_NUMBER + "개만 입력해주세요");
                continue;
            }

            boolean isValid = true;
            for (String chNum : userNumber) {
                if (!chNum.matches("\\d")) {
                    System.out.println("숫자만 입력해주세요.");
                    isValid = false;
                    break;
                }

                int num = Integer.parseInt(chNum);
                if (!checkSame.add(num)) {
                    System.out.println("중복된 숫자가 있습니다. 다시 입력해주세요");
                    isValid = false;
                    break;
                }
            }

            if (isValid && checkSame.size() == STRIKE_NUMBER) {
                int i = 0;
                for (int num : checkSame) {
                    numbers[i++] = num;
                }
                return numbers;
            } else {
                checkSame.clear();
            }
        }
    }


}

Game.java 사실상 제일 중요한 파일이다.

개발 과정에서 정답을 출력시켜 예외처리가 잘 되는지 확인하기 위해 정답 출력문과 convertArrayToString 함수가 있는 걸 확인할 수 있다.

 

출력 결과

Intellij로 실행시킨 숫자야구 게임


⚾ 3. 트러블 슈팅 

처음 만들었을 때는 위 이미지와는 결과물이 많이 달랐다. 

처음 완성했을때의 모습

 

3-1. convertArrayToString함수

이미지와 같이 정수를 하나씩 입력받고 이를 배열에 입력받아 정답과 비교하는 식이었다. 이 당시에 만들어진 게 convertArrayToString함수이다. 처음 이 함수의 이름은 convertArrayToInt, 사용자 및 컴퓨터가 가지고 있는 정수 배열의 값을 출력해 주는 역할이었다. 하지만 정수를 하나씩 엔터를 통해 입력받는 것을 수정하기 위해 입력값을 문자열로 바꾸게 되면서 함수의 이름도 변경되었고 convertArrayToString를 사용하던 함수도 수정을 통해 B과 S만을 출력해 주는 것으로 기능을 변경하게 되면서 convertArrayToString함수는 정답만을 출력해 주는 함수로 전락하게 되었다. 

 

최종적으로 push하는 과정에서 통째로 주석처리 시킬까 했지만(itellij에서 사용되지 않는 함수라는 작은 경고 "⚠️" 가 떴기 때문) 프로젝트로 진행하는 것도 아니었고 크고 작은 리팩토링을 3번이나 하는 과정 속에서 코드의 리팩토링 흔적으로 남겨두는 것도 나쁘지 않겠다는 생각으로 남겨두었다. 즉, 내가 올린 코드를 참고하는 사람들은 저 함수를 무시하셔야 한다는 뜻이다. 

 

3-2. HashSet과 LinkedHashSet

숫자야구는 반드시 사용자와 컴퓨터가 중복된 숫자를 생성 및 입력을 방지해주어야 한다. 간단한 예시를 통해 왜 막아야 하는지 알아보자.

사용자 입력값 중복값 처리 안해주었을 경우

 

위 이미지와 같이 컴퓨터가 생성한 정답 743을 기준으로 2B1S가 정답으로 출력되게 된다. 사용자는 분명 숫자 3개 중에 하나가 정답임을 확인할 수 있겠지만 명확하게 0B1S로 표기하는 게 더 나을 것이다. 

컴퓨터 난수 생성에 중복값 처리를 안해주었을 경우

 

역시 위 예시와 같이 정답이 제대로 출력되지 않는 것을 확인할 수 있다. 컴퓨터 난수 중복이 허용될 경우 사용자가 정답을 맞히는 난이도는 더 올라갈 것이다.

 

이 해결법으로 생각해 낸 것은 Set 알고리즘이다. set 자료구조는 중복되는 값을 원소로 절대 받지 않는다. 즉 set자료구조 안에 이미 3이라는 숫자가 존재할 경우 3을 몇 번을 추가로 넣어주더라도 절대로 원소 3의 개수가 한 개 이상이 되지 않는다는 것이다. 

자바에서는 set이 HashSet으로 존재했다. 그래서 HashSet을 사용하였는데, 문제가 이상한 곳에서 발생하였다. 사용자와 컴퓨터 난수 생성에 있어 중복값은 처리가 됐지만 출력이 무조건 정렬된 상태로 된다는 것이었다. 중복값 방지에만 너무 몰두했던 나머지 set자료구조는 저장된 원소의 순서를 보장해주지 않는다는 점이었다. 이 과정을 통해 새롭게 알게 된 자료구조가 LinkedHashSet이다. 

 

LinkedHashSet은 HashSet의 모든 특징을 가져가면서 동시에 저장된 순서를 보장해 준다. 다만 Linked 특징은 인덱스로 접근 하는(set은 인덱스 접근이 안되지만) 것보다 느리고 다음 node의 주소값을 보관하면서 메모리를 더 사용하게 된다.

 

3-3 StringBuilder

 

처음 사용자의 입력값을 문자열로 받기 위해 리팩토링 하였을 때, 사용자의 입력값을 출력하기 위해 아래 예시처럼 사용했다.

여러 String 출력 예시

문자열을 +로 합치면서 코드가 길어지게 되자 이에 대한 편의성으로 만들어진 무언가가 있지 않을까 검색해 본 결과 StringBuilder에 대해 알아내게 되었다. StringBuilder의 사용법은 new StringBuilder() 통해 생성하고 append(), insert(), delete(), reverse()등의 메소드가 있다. 특징으로는 StringBuilder로 사용하던 문자를 String 객체로 변환할 때, 반드시 toString()을 붙여줘야 한다는 점이다. 이렇게 해야 하는 이유는 Stringbuilder 자체는 String이 아니기 때문이다.

불변상태의 String 형태로 반환할때는 toString()을 붙혀줘야 한다.

 

StringBuilder 왜 만들어진 것일까?

Java에서의 String 객체 특징에 있는데, 자바에서의 String은 기본적으로 불변이다. 즉, 한번 String word1 = "hello"를 실행하게 되면 이 객체의 내용은 수정될 수 없다는 뜻이다. 하지만 우리는 word1의 값을 새롭게 대입해서 변경하면 쓰고 있지 않은가? 이는 java 내에서 새로운 String객체를 수정할 때마다 새롭게 객체를 생성해서 반환해 주고 있기 때문이다.

다시 말하자면, String 타입의 변수의 값을 수정하면 처음 생성한 String은 그대로 유지되고 수정된 내용의 새로운 String 객체가 생성되는 것이다. 

 

자바는 왜 String을 불변으로 만들었을까?

이 이유로는 스레드의 안전과 보안이 주된 이유이다. 불변이 갖는 장점은 공유되더라도 변경될 위험이 없다는 점이다. 이 장점 덕분에 String은 네트워크 통신, 파일 경로, 데이터베이스 연결 정보 등 중요한 정보를 저장하는 데 사용된다. 그렇기 때문에 반복적인 문자열 변경 작업을 할 경우 StringBuilder와 같은 가변적인 문자열 처리 클래스를 사용하는 것이 좋다.

 

3-4 hasNext()란?

나의 코드와는 직접적인 연관은 없는 함수이다. 하지만 hasNext()를 조건으로 while문을 사용할 수 있는 것을 발견하였다. hasNext()는 반환값으로 true 혹은 false를 반환한다. iterater를 사용할 때 리스트나 셋 자료구조의 끝을 쉽게 판단할 때 사용하기 좋은 함수이다. 즉, 다음 읽어올 요수가 남아 있는지 확인하는 메서드이다. 요소가 있으면 true, 없으면 false를 반환하게 된다.

반응형